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 { 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(),
|
||||||
|
|||||||
@@ -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 { 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,
|
||||||
@@ -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 { 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageReactionsCount = {
|
export type MessageReactionCount = {
|
||||||
reaction: string;
|
reaction: string;
|
||||||
count: number;
|
count: number;
|
||||||
reacted: boolean;
|
reacted: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user