mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Display users that reacted to a message
This commit is contained in:
36
apps/desktop/src/main/queries/emojis/emoji-get-by-skin-id.ts
Normal file
36
apps/desktop/src/main/queries/emojis/emoji-get-by-skin-id.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { emojiDatabase } from '@/main/lib/assets';
|
||||
import { mapEmoji } from '@/main/lib/mappers';
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
import { EmojiGetBySkinIdQueryInput } from '@/shared/queries/emojis/emoji-get-by-skin-id';
|
||||
import { Emoji } from '@/shared/types/emojis';
|
||||
import { Event } from '@/shared/types/events';
|
||||
|
||||
export class EmojiGetBySkinIdQueryHandler
|
||||
implements QueryHandler<EmojiGetBySkinIdQueryInput>
|
||||
{
|
||||
public async handleQuery(input: EmojiGetBySkinIdQueryInput): Promise<Emoji> {
|
||||
const data = await emojiDatabase
|
||||
.selectFrom('emojis')
|
||||
.innerJoin('emoji_svgs', 'emojis.id', 'emoji_svgs.emoji_id')
|
||||
.selectAll('emojis')
|
||||
.where('emoji_svgs.skin_id', '=', input.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!data) {
|
||||
throw new Error('Emoji not found');
|
||||
}
|
||||
|
||||
const emoji = mapEmoji(data);
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public async checkForChanges(
|
||||
_: Event,
|
||||
__: EmojiGetBySkinIdQueryInput,
|
||||
___: Emoji
|
||||
): Promise<ChangeCheckResult<EmojiGetBySkinIdQueryInput>> {
|
||||
return {
|
||||
hasChanges: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { EmojiGetQueryHandler } from '@/main/queries/emojis/emoji-get';
|
||||
import { EmojiListQueryHandler } from '@/main/queries/emojis/emoji-list';
|
||||
import { EmojiCategoryListQueryHandler } from '@/main/queries/emojis/emoji-category-list';
|
||||
import { EmojiSearchQueryHandler } from '@/main/queries/emojis/emoji-search';
|
||||
import { EmojiGetBySkinIdQueryHandler } from '@/main/queries/emojis/emoji-get-by-skin-id';
|
||||
import { FileListQueryHandler } from '@/main/queries/files/file-list';
|
||||
import { FileGetQueryHandler } from '@/main/queries/files/file-get';
|
||||
import { FileMetadataGetQueryHandler } from '@/main/queries/files/file-metadata-get';
|
||||
@@ -12,7 +13,8 @@ import { IconSearchQueryHandler } from '@/main/queries/icons/icon-search';
|
||||
import { IconCategoryListQueryHandler } from '@/main/queries/icons/icon-category-list';
|
||||
import { MessageGetQueryHandler } from '@/main/queries/messages/message-get';
|
||||
import { MessageListQueryHandler } from '@/main/queries/messages/message-list';
|
||||
import { MessageReactionsGetQueryHandler } from '@/main/queries/messages/message-reactions-get';
|
||||
import { MessageReactionsListQueryHandler } from '@/main/queries/messages/message-reaction-list';
|
||||
import { MessageReactionsAggregateQueryHandler } from '@/main/queries/messages/message-reactions-aggregate';
|
||||
import { EntryChildrenGetQueryHandler } from '@/main/queries/entries/entry-children-get';
|
||||
import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get';
|
||||
import { EntryTreeGetQueryHandler } from '@/main/queries/entries/entry-tree-get';
|
||||
@@ -38,7 +40,8 @@ type QueryHandlerMap = {
|
||||
export const queryHandlerMap: QueryHandlerMap = {
|
||||
account_list: new AccountListQueryHandler(),
|
||||
message_list: new MessageListQueryHandler(),
|
||||
message_reactions_get: new MessageReactionsGetQueryHandler(),
|
||||
message_reaction_list: new MessageReactionsListQueryHandler(),
|
||||
message_reactions_aggregate: new MessageReactionsAggregateQueryHandler(),
|
||||
message_get: new MessageGetQueryHandler(),
|
||||
entry_get: new EntryGetQueryHandler(),
|
||||
record_list: new RecordListQueryHandler(),
|
||||
@@ -49,6 +52,7 @@ export const queryHandlerMap: QueryHandlerMap = {
|
||||
file_list: new FileListQueryHandler(),
|
||||
emoji_list: new EmojiListQueryHandler(),
|
||||
emoji_get: new EmojiGetQueryHandler(),
|
||||
emoji_get_by_skin_id: new EmojiGetBySkinIdQueryHandler(),
|
||||
emoji_category_list: new EmojiCategoryListQueryHandler(),
|
||||
emoji_search: new EmojiSearchQueryHandler(),
|
||||
icon_list: new IconListQueryHandler(),
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
import { MessageReactionListQueryInput } from '@/shared/queries/messages/message-reaction-list';
|
||||
import { Event } from '@/shared/types/events';
|
||||
import { MessageReaction } from '@/shared/types/messages';
|
||||
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
|
||||
|
||||
export class MessageReactionsListQueryHandler
|
||||
extends WorkspaceQueryHandlerBase
|
||||
implements QueryHandler<MessageReactionListQueryInput>
|
||||
{
|
||||
public async handleQuery(
|
||||
input: MessageReactionListQueryInput
|
||||
): Promise<MessageReaction[]> {
|
||||
return this.fetchMessageReactions(input);
|
||||
}
|
||||
|
||||
public async checkForChanges(
|
||||
event: Event,
|
||||
input: MessageReactionListQueryInput,
|
||||
_: MessageReaction[]
|
||||
): Promise<ChangeCheckResult<MessageReactionListQueryInput>> {
|
||||
if (
|
||||
event.type === 'workspace_deleted' &&
|
||||
event.workspace.accountId === input.accountId &&
|
||||
event.workspace.id === input.workspaceId
|
||||
) {
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'message_reaction_created' &&
|
||||
event.accountId === input.accountId &&
|
||||
event.workspaceId === input.workspaceId &&
|
||||
event.messageReaction.messageId === input.messageId
|
||||
) {
|
||||
const newResult = await this.handleQuery(input);
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newResult,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'message_reaction_deleted' &&
|
||||
event.accountId === input.accountId &&
|
||||
event.workspaceId === input.workspaceId &&
|
||||
event.messageReaction.messageId === input.messageId
|
||||
) {
|
||||
const newResult = await this.handleQuery(input);
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasChanges: false,
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchMessageReactions(
|
||||
input: MessageReactionListQueryInput
|
||||
): Promise<MessageReaction[]> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
const offset = (input.page - 1) * input.count;
|
||||
const reactions = await workspace.database
|
||||
.selectFrom('message_reactions')
|
||||
.selectAll()
|
||||
.where('message_id', '=', input.messageId)
|
||||
.where('reaction', '=', input.reaction)
|
||||
.where('deleted_at', 'is', null)
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(input.count)
|
||||
.offset(offset)
|
||||
.execute();
|
||||
|
||||
return reactions.map((row) => {
|
||||
return {
|
||||
messageId: row.message_id,
|
||||
collaboratorId: row.collaborator_id,
|
||||
rootId: row.root_id,
|
||||
reaction: row.reaction,
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,32 @@
|
||||
import { sql } from 'kysely';
|
||||
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
import { MessageReactionsGetQueryInput } from '@/shared/queries/messages/message-reactions-get';
|
||||
import { MessageReactionsAggregateQueryInput } from '@/shared/queries/messages/message-reactions-aggregate';
|
||||
import { Event } from '@/shared/types/events';
|
||||
import { MessageReactionsCount } from '@/shared/types/messages';
|
||||
import { MessageReactionCount } from '@/shared/types/messages';
|
||||
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
|
||||
|
||||
interface MessageReactionsCountRow {
|
||||
interface MessageReactionsAggregateRow {
|
||||
reaction: string;
|
||||
count: number;
|
||||
reacted: number;
|
||||
}
|
||||
|
||||
export class MessageReactionsGetQueryHandler
|
||||
export class MessageReactionsAggregateQueryHandler
|
||||
extends WorkspaceQueryHandlerBase
|
||||
implements QueryHandler<MessageReactionsGetQueryInput>
|
||||
implements QueryHandler<MessageReactionsAggregateQueryInput>
|
||||
{
|
||||
public async handleQuery(
|
||||
input: MessageReactionsGetQueryInput
|
||||
): Promise<MessageReactionsCount[]> {
|
||||
input: MessageReactionsAggregateQueryInput
|
||||
): Promise<MessageReactionCount[]> {
|
||||
return this.fetchMessageReactions(input);
|
||||
}
|
||||
|
||||
public async checkForChanges(
|
||||
event: Event,
|
||||
input: MessageReactionsGetQueryInput,
|
||||
_: MessageReactionsCount[]
|
||||
): Promise<ChangeCheckResult<MessageReactionsGetQueryInput>> {
|
||||
input: MessageReactionsAggregateQueryInput,
|
||||
_: MessageReactionCount[]
|
||||
): Promise<ChangeCheckResult<MessageReactionsAggregateQueryInput>> {
|
||||
if (
|
||||
event.type === 'workspace_deleted' &&
|
||||
event.workspace.accountId === input.accountId &&
|
||||
@@ -72,11 +72,11 @@ export class MessageReactionsGetQueryHandler
|
||||
}
|
||||
|
||||
private async fetchMessageReactions(
|
||||
input: MessageReactionsGetQueryInput
|
||||
): Promise<MessageReactionsCount[]> {
|
||||
input: MessageReactionsAggregateQueryInput
|
||||
): Promise<MessageReactionCount[]> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
const result = await sql<MessageReactionsCountRow>`
|
||||
const result = await sql<MessageReactionsAggregateRow>`
|
||||
SELECT
|
||||
reaction,
|
||||
COUNT(reaction) as count,
|
||||
@@ -0,0 +1,87 @@
|
||||
import { MessageNode, MessageReactionCount } from '@/shared/types/messages';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { useQueries } from '@/renderer/hooks/use-queries';
|
||||
import { EmojiElement } from '@/renderer/components/emojis/emoji-element';
|
||||
|
||||
interface MessageReactionCountTooltipContentProps {
|
||||
message: MessageNode;
|
||||
reactionCount: MessageReactionCount;
|
||||
}
|
||||
|
||||
export const MessageReactionCountTooltipContent = ({
|
||||
message,
|
||||
reactionCount,
|
||||
}: MessageReactionCountTooltipContentProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const { data: emoji } = useQuery({
|
||||
type: 'emoji_get_by_skin_id',
|
||||
id: reactionCount.reaction,
|
||||
});
|
||||
|
||||
const { data: reactions } = useQuery({
|
||||
type: 'message_reaction_list',
|
||||
messageId: message.id,
|
||||
reaction: reactionCount.reaction,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
page: 0,
|
||||
count: 3,
|
||||
});
|
||||
|
||||
const userIds = reactions?.map((reaction) => reaction.collaboratorId) ?? [];
|
||||
|
||||
const results = useQueries(
|
||||
userIds.map((userId) => ({
|
||||
type: 'user_get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
}))
|
||||
);
|
||||
|
||||
const users = results
|
||||
.filter((result) => result.data !== null)
|
||||
.map((result) => result.data!.customName ?? result.data!.name);
|
||||
|
||||
const emojiName = `:${emoji?.code ?? reactionCount.reaction}:`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<EmojiElement id={reactionCount.reaction} className="h-5 w-5" />
|
||||
{users.length === 1 && (
|
||||
<p>
|
||||
<span className="font-semibold">{users[0]}</span>
|
||||
<span className="text-muted-foreground"> reacted with </span>
|
||||
<span className="font-semibold">{emojiName}</span>
|
||||
</p>
|
||||
)}
|
||||
{users.length === 2 && (
|
||||
<p>
|
||||
<span className="font-semibold">{users[0]}</span>
|
||||
<span className="text-muted-foreground"> and </span>
|
||||
<span className="font-semibold">{users[1]}</span>
|
||||
<span className="text-muted-foreground"> reacted with</span>
|
||||
<span className="font-semibold">{emojiName}</span>
|
||||
</p>
|
||||
)}
|
||||
{users.length === 3 && (
|
||||
<p>
|
||||
<span className="font-semibold">{users[0]}</span>
|
||||
<span className="text-muted-foreground">, </span>
|
||||
<span className="font-semibold">{users[1]}</span>
|
||||
<span className="text-muted-foreground"> and </span>
|
||||
<span className="font-semibold">{users[2]}</span>
|
||||
<span className="text-muted-foreground"> reacted with</span>
|
||||
<span className="font-semibold">{emojiName}</span>
|
||||
</p>
|
||||
)}
|
||||
{users.length > 3 && (
|
||||
<p className="text-muted-foreground">
|
||||
{users.length} people reacted with {emojiName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MessageNode, MessageReactionCount } from '@/shared/types/messages';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/renderer/components/ui/tooltip';
|
||||
import { MessageReactionCountTooltipContent } from '@/renderer/components/messages/message-reaction-count-tooltip-content';
|
||||
|
||||
interface MessageReactionCountTooltipProps {
|
||||
message: MessageNode;
|
||||
reactionCount: MessageReactionCount;
|
||||
children: React.ReactNode;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
export const MessageReactionCountTooltip = ({
|
||||
message,
|
||||
reactionCount,
|
||||
children,
|
||||
onOpen,
|
||||
}: MessageReactionCountTooltipProps) => {
|
||||
if (reactionCount.count === 0) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="bg-background text-primary p-2 shadow-md cursor-pointer"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<MessageReactionCountTooltipContent
|
||||
message={message}
|
||||
reactionCount={reactionCount}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
||||
import { MessageNode, MessageReactionCount } from '@/shared/types/messages';
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useQueries } from '@/renderer/hooks/use-queries';
|
||||
import { MessageReactionListQueryInput } from '@/shared/queries/messages/message-reaction-list';
|
||||
|
||||
const REACTIONS_PER_PAGE = 20;
|
||||
|
||||
interface MessageReactionCountsDialogListProps {
|
||||
message: MessageNode;
|
||||
reactionCount: MessageReactionCount;
|
||||
}
|
||||
|
||||
export const MessageReactionCountsDialogList = ({
|
||||
message,
|
||||
reactionCount,
|
||||
}: MessageReactionCountsDialogListProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const [lastPage, setLastPage] = React.useState<number>(1);
|
||||
const inputs: MessageReactionListQueryInput[] = Array.from({
|
||||
length: lastPage,
|
||||
}).map((_, i) => ({
|
||||
type: 'message_reaction_list',
|
||||
messageId: message.id,
|
||||
reaction: reactionCount.reaction,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
page: i + 1,
|
||||
count: REACTIONS_PER_PAGE,
|
||||
}));
|
||||
|
||||
const result = useQueries(inputs);
|
||||
const reactions = result.flatMap((data) => data.data ?? []);
|
||||
const isPending = result.some((data) => data.isPending);
|
||||
const hasMore =
|
||||
!isPending && reactions.length === lastPage * REACTIONS_PER_PAGE;
|
||||
|
||||
const userIds = reactions?.map((reaction) => reaction.collaboratorId) ?? [];
|
||||
|
||||
const results = useQueries(
|
||||
userIds.map((userId) => ({
|
||||
type: 'user_get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
}))
|
||||
);
|
||||
|
||||
const users = results
|
||||
.filter((result) => result.data !== null)
|
||||
.map((result) => result.data!);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{users.map((user) => (
|
||||
<div key={user.id} className="flex items-center space-x-3">
|
||||
<Avatar
|
||||
id={user.id}
|
||||
name={user.name}
|
||||
avatar={user.avatar}
|
||||
className="size-5"
|
||||
/>
|
||||
<p className="flex-grow text-sm font-medium leading-none">
|
||||
{user.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
></InView>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/renderer/components/ui/dialog';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/renderer/components/ui/tabs';
|
||||
import { MessageNode, MessageReactionCount } from '@/shared/types/messages';
|
||||
import { EmojiElement } from '@/renderer/components/emojis/emoji-element';
|
||||
import { MessageReactionCountsDialogList } from '@/renderer/components/messages/message-reaction-counts-dialog-list';
|
||||
|
||||
interface MessageReactionCountsDialogProps {
|
||||
message: MessageNode;
|
||||
reactionCounts: MessageReactionCount[];
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const MessageReactionCountsDialog = ({
|
||||
message,
|
||||
reactionCounts,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: MessageReactionCountsDialogProps) => {
|
||||
if (reactionCounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="overflow-hidden p-2 outline-none w-128 min-w-128 max-w-128 h-128 min-h-128 max-h-128">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Reactions</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<Tabs
|
||||
defaultValue={reactionCounts[0]!.reaction}
|
||||
className="flex flex-row gap-4"
|
||||
>
|
||||
<TabsList className="flex h-full max-h-full w-24 flex-col items-start justify-start gap-1 rounded-none border-r border-r-gray-100 bg-white pr-3">
|
||||
{reactionCounts.map((reactionCount) => (
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-${reactionCount.reaction}`}
|
||||
className="flex w-full flex-row items-center justify-start gap-2 p-2"
|
||||
value={reactionCount.reaction}
|
||||
>
|
||||
<EmojiElement id={reactionCount.reaction} className="h-5 w-5" />
|
||||
{reactionCount.count}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="flex-grow">
|
||||
{reactionCounts.map((reactionCount) => (
|
||||
<TabsContent
|
||||
key={`tab-content-${reactionCount.reaction}`}
|
||||
className="relative h-full focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value={reactionCount.reaction}
|
||||
>
|
||||
<div className="absolute bottom-0 left-0 right-0 top-0 h-full overflow-y-auto">
|
||||
<MessageReactionCountsDialogList
|
||||
message={message}
|
||||
reactionCount={reactionCount}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</div>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MessageNode } from '@/shared/types/messages';
|
||||
import { EmojiElement } from '@/renderer/components/emojis/emoji-element';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { MessageReactionCountTooltip } from '@/renderer/components/messages/message-reaction-count-tooltip';
|
||||
import { MessageReactionCountsDialog } from '@/renderer/components/messages/message-reaction-counts-dialog';
|
||||
|
||||
interface MessageReactionCountsProps {
|
||||
message: MessageNode;
|
||||
}
|
||||
|
||||
export const MessageReactionCounts = ({
|
||||
message,
|
||||
}: MessageReactionCountsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const [openDialog, setOpenDialog] = React.useState(false);
|
||||
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const { data } = useQuery({
|
||||
type: 'message_reactions_aggregate',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const reactionCounts = data ?? [];
|
||||
if (reactionCounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-1 flex flex-row gap-2">
|
||||
{reactionCounts.map((reaction) => {
|
||||
if (reaction.count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasReacted = reaction.reacted;
|
||||
return (
|
||||
<MessageReactionCountTooltip
|
||||
key={reaction.reaction}
|
||||
message={message}
|
||||
reactionCount={reaction}
|
||||
onOpen={() => {
|
||||
setOpenDialog(true);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
key={reaction.reaction}
|
||||
className={cn(
|
||||
'rouded flex flex-row items-center gap-2 px-1 py-0.5 shadow',
|
||||
'cursor-pointer text-sm text-muted-foreground hover:text-foreground',
|
||||
hasReacted ? 'bg-blue-100' : 'bg-gray-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasReacted) {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'message_reaction_delete',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
rootId: message.rootId,
|
||||
reaction: reaction.reaction,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to remove reaction',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'message_reaction_create',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
rootId: message.rootId,
|
||||
reaction: reaction.reaction,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to add reaction',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EmojiElement id={reaction.reaction} className="size-5" />
|
||||
<span>{reaction.count}</span>
|
||||
</div>
|
||||
</MessageReactionCountTooltip>
|
||||
);
|
||||
})}
|
||||
{openDialog && (
|
||||
<MessageReactionCountsDialog
|
||||
message={message}
|
||||
reactionCounts={reactionCounts}
|
||||
open={openDialog}
|
||||
onOpenChange={setOpenDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import { MessageNode } from '@/shared/types/messages';
|
||||
import { EmojiElement } from '@/renderer/components/emojis/emoji-element';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
|
||||
interface MessageReactionsProps {
|
||||
message: MessageNode;
|
||||
}
|
||||
|
||||
export const MessageReactions = ({ message }: MessageReactionsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const { data } = useQuery({
|
||||
type: 'message_reactions_get',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const reactionCounts = data ?? [];
|
||||
if (reactionCounts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-1 flex flex-row gap-2">
|
||||
{reactionCounts.map((reaction) => {
|
||||
if (reaction.count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasReacted = reaction.reacted;
|
||||
return (
|
||||
<div
|
||||
key={reaction.reaction}
|
||||
className={cn(
|
||||
'rouded flex flex-row items-center gap-2 px-1 py-0.5 shadow',
|
||||
'cursor-pointer text-sm text-muted-foreground hover:text-foreground',
|
||||
hasReacted ? 'bg-blue-100' : 'bg-gray-50'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
reactionCounts.some(
|
||||
(reactionCount) =>
|
||||
reactionCount.reaction === reaction.reaction &&
|
||||
reactionCount.reacted
|
||||
)
|
||||
) {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'message_reaction_delete',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
rootId: message.rootId,
|
||||
reaction: reaction.reaction,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to remove reaction',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'message_reaction_create',
|
||||
messageId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
rootId: message.rootId,
|
||||
reaction: reaction.reaction,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to add reaction',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EmojiElement id={reaction.reaction} className="size-5" />
|
||||
<span>{reaction.count}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { MessageActions } from '@/renderer/components/messages/message-actions';
|
||||
import { MessageAuthorAvatar } from '@/renderer/components/messages/message-author-avatar';
|
||||
import { MessageAuthorName } from '@/renderer/components/messages/message-author-name';
|
||||
import { MessageContent } from '@/renderer/components/messages/message-content';
|
||||
import { MessageReactions } from '@/renderer/components/messages/message-reactions';
|
||||
import { MessageReactionCounts } from '@/renderer/components/messages/message-reaction-counts';
|
||||
import { MessageTime } from '@/renderer/components/messages/message-time';
|
||||
import { MessageReference } from '@/renderer/components/messages/message-reference';
|
||||
import { useRadar } from '@/renderer/contexts/radar';
|
||||
@@ -74,7 +74,7 @@ export const Message = ({ message, previousMessage }: MessageProps) => {
|
||||
<MessageReference messageId={message.attributes.referenceId} />
|
||||
)}
|
||||
<MessageContent message={message} />
|
||||
<MessageReactions message={message} />
|
||||
<MessageReactionCounts message={message} />
|
||||
</InView>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,3 +46,6 @@ export const getDisplayedDates = (
|
||||
|
||||
return { first: firstDayDisplayed, last: lastDayDisplayed };
|
||||
};
|
||||
|
||||
export const pluralize = (count: number, singular: string, plural: string) =>
|
||||
count === 1 ? singular : plural;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Emoji } from '@/shared/types/emojis';
|
||||
|
||||
export type EmojiGetBySkinIdQueryInput = {
|
||||
type: 'emoji_get_by_skin_id';
|
||||
id: string;
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
emoji_get_by_skin_id: {
|
||||
input: EmojiGetBySkinIdQueryInput;
|
||||
output: Emoji;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { MessageReaction } from '@/shared/types/messages';
|
||||
|
||||
export type MessageReactionListQueryInput = {
|
||||
type: 'message_reaction_list';
|
||||
messageId: string;
|
||||
reaction: string;
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
page: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
message_reaction_list: {
|
||||
input: MessageReactionListQueryInput;
|
||||
output: MessageReaction[];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MessageReactionCount } from '@/shared/types/messages';
|
||||
|
||||
export type MessageReactionsAggregateQueryInput = {
|
||||
type: 'message_reactions_aggregate';
|
||||
messageId: string;
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
message_reactions_aggregate: {
|
||||
input: MessageReactionsAggregateQueryInput;
|
||||
output: MessageReactionCount[];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { MessageReactionsCount } from '@/shared/types/messages';
|
||||
|
||||
export type MessageReactionsGetQueryInput = {
|
||||
type: 'message_reactions_get';
|
||||
messageId: string;
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
message_reactions_get: {
|
||||
input: MessageReactionsGetQueryInput;
|
||||
output: MessageReactionsCount[];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export type MessageReaction = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type MessageReactionsCount = {
|
||||
export type MessageReactionCount = {
|
||||
reaction: string;
|
||||
count: number;
|
||||
reacted: boolean;
|
||||
|
||||
Reference in New Issue
Block a user