Display users that reacted to a message

This commit is contained in:
Hakan Shehu
2025-01-23 15:43:13 +01:00
parent 6e661894d2
commit cecfd6f17a
17 changed files with 613 additions and 137 deletions

View 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,
};
}
}

View File

@@ -4,6 +4,7 @@ import { EmojiGetQueryHandler } from '@/main/queries/emojis/emoji-get';
import { EmojiListQueryHandler } from '@/main/queries/emojis/emoji-list'; import { EmojiListQueryHandler } from '@/main/queries/emojis/emoji-list';
import { EmojiCategoryListQueryHandler } from '@/main/queries/emojis/emoji-category-list'; import { EmojiCategoryListQueryHandler } from '@/main/queries/emojis/emoji-category-list';
import { EmojiSearchQueryHandler } from '@/main/queries/emojis/emoji-search'; 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 { FileListQueryHandler } from '@/main/queries/files/file-list';
import { FileGetQueryHandler } from '@/main/queries/files/file-get'; import { FileGetQueryHandler } from '@/main/queries/files/file-get';
import { FileMetadataGetQueryHandler } from '@/main/queries/files/file-metadata-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 { IconCategoryListQueryHandler } from '@/main/queries/icons/icon-category-list';
import { MessageGetQueryHandler } from '@/main/queries/messages/message-get'; import { MessageGetQueryHandler } from '@/main/queries/messages/message-get';
import { MessageListQueryHandler } from '@/main/queries/messages/message-list'; 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 { EntryChildrenGetQueryHandler } from '@/main/queries/entries/entry-children-get';
import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get'; import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get';
import { EntryTreeGetQueryHandler } from '@/main/queries/entries/entry-tree-get'; import { EntryTreeGetQueryHandler } from '@/main/queries/entries/entry-tree-get';
@@ -38,7 +40,8 @@ type QueryHandlerMap = {
export const queryHandlerMap: QueryHandlerMap = { export const queryHandlerMap: QueryHandlerMap = {
account_list: new AccountListQueryHandler(), account_list: new AccountListQueryHandler(),
message_list: new MessageListQueryHandler(), message_list: new MessageListQueryHandler(),
message_reactions_get: new MessageReactionsGetQueryHandler(), message_reaction_list: new MessageReactionsListQueryHandler(),
message_reactions_aggregate: new MessageReactionsAggregateQueryHandler(),
message_get: new MessageGetQueryHandler(), message_get: new MessageGetQueryHandler(),
entry_get: new EntryGetQueryHandler(), entry_get: new EntryGetQueryHandler(),
record_list: new RecordListQueryHandler(), record_list: new RecordListQueryHandler(),
@@ -49,6 +52,7 @@ export const queryHandlerMap: QueryHandlerMap = {
file_list: new FileListQueryHandler(), file_list: new FileListQueryHandler(),
emoji_list: new EmojiListQueryHandler(), emoji_list: new EmojiListQueryHandler(),
emoji_get: new EmojiGetQueryHandler(), emoji_get: new EmojiGetQueryHandler(),
emoji_get_by_skin_id: new EmojiGetBySkinIdQueryHandler(),
emoji_category_list: new EmojiCategoryListQueryHandler(), emoji_category_list: new EmojiCategoryListQueryHandler(),
emoji_search: new EmojiSearchQueryHandler(), emoji_search: new EmojiSearchQueryHandler(),
icon_list: new IconListQueryHandler(), icon_list: new IconListQueryHandler(),

View File

@@ -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,
};
});
}
}

View File

@@ -1,32 +1,32 @@
import { sql } from 'kysely'; import { sql } from 'kysely';
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types'; 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 { 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'; import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
interface MessageReactionsCountRow { interface MessageReactionsAggregateRow {
reaction: string; reaction: string;
count: number; count: number;
reacted: number; reacted: number;
} }
export class MessageReactionsGetQueryHandler export class MessageReactionsAggregateQueryHandler
extends WorkspaceQueryHandlerBase extends WorkspaceQueryHandlerBase
implements QueryHandler<MessageReactionsGetQueryInput> implements QueryHandler<MessageReactionsAggregateQueryInput>
{ {
public async handleQuery( public async handleQuery(
input: MessageReactionsGetQueryInput input: MessageReactionsAggregateQueryInput
): Promise<MessageReactionsCount[]> { ): Promise<MessageReactionCount[]> {
return this.fetchMessageReactions(input); return this.fetchMessageReactions(input);
} }
public async checkForChanges( public async checkForChanges(
event: Event, event: Event,
input: MessageReactionsGetQueryInput, input: MessageReactionsAggregateQueryInput,
_: MessageReactionsCount[] _: MessageReactionCount[]
): Promise<ChangeCheckResult<MessageReactionsGetQueryInput>> { ): Promise<ChangeCheckResult<MessageReactionsAggregateQueryInput>> {
if ( if (
event.type === 'workspace_deleted' && event.type === 'workspace_deleted' &&
event.workspace.accountId === input.accountId && event.workspace.accountId === input.accountId &&
@@ -72,11 +72,11 @@ export class MessageReactionsGetQueryHandler
} }
private async fetchMessageReactions( private async fetchMessageReactions(
input: MessageReactionsGetQueryInput input: MessageReactionsAggregateQueryInput
): Promise<MessageReactionsCount[]> { ): Promise<MessageReactionCount[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId); const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await sql<MessageReactionsCountRow>` const result = await sql<MessageReactionsAggregateRow>`
SELECT SELECT
reaction, reaction,
COUNT(reaction) as count, COUNT(reaction) as count,

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -4,7 +4,7 @@ import { MessageActions } from '@/renderer/components/messages/message-actions';
import { MessageAuthorAvatar } from '@/renderer/components/messages/message-author-avatar'; import { MessageAuthorAvatar } from '@/renderer/components/messages/message-author-avatar';
import { MessageAuthorName } from '@/renderer/components/messages/message-author-name'; import { MessageAuthorName } from '@/renderer/components/messages/message-author-name';
import { MessageContent } from '@/renderer/components/messages/message-content'; 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 { MessageTime } from '@/renderer/components/messages/message-time';
import { MessageReference } from '@/renderer/components/messages/message-reference'; import { MessageReference } from '@/renderer/components/messages/message-reference';
import { useRadar } from '@/renderer/contexts/radar'; import { useRadar } from '@/renderer/contexts/radar';
@@ -74,7 +74,7 @@ export const Message = ({ message, previousMessage }: MessageProps) => {
<MessageReference messageId={message.attributes.referenceId} /> <MessageReference messageId={message.attributes.referenceId} />
)} )}
<MessageContent message={message} /> <MessageContent message={message} />
<MessageReactions message={message} /> <MessageReactionCounts message={message} />
</InView> </InView>
</div> </div>
</div> </div>

View File

@@ -46,3 +46,6 @@ export const getDisplayedDates = (
return { first: firstDayDisplayed, last: lastDayDisplayed }; return { first: firstDayDisplayed, last: lastDayDisplayed };
}; };
export const pluralize = (count: number, singular: string, plural: string) =>
count === 1 ? singular : plural;

View File

@@ -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;
};
}
}

View File

@@ -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[];
};
}
}

View File

@@ -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[];
};
}
}

View File

@@ -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[];
};
}
}

View File

@@ -21,7 +21,7 @@ export type MessageReaction = {
createdAt: string; createdAt: string;
}; };
export type MessageReactionsCount = { export type MessageReactionCount = {
reaction: string; reaction: string;
count: number; count: number;
reacted: boolean; reacted: boolean;