mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-20 23:21:06 +02:00
refactor: remove read tracking from mentions (prep for notification center)
This commit is contained in:
parent
25eb240539
commit
80e19a52cb
11 changed files with 7 additions and 227 deletions
|
|
@ -37,10 +37,5 @@ def upgrade() -> None:
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Remove author_id column from new_chat_messages table."""
|
"""Remove author_id column from new_chat_messages table."""
|
||||||
op.execute(
|
op.execute("DROP INDEX IF EXISTS ix_new_chat_messages_author_id")
|
||||||
"""
|
op.execute("ALTER TABLE new_chat_messages DROP COLUMN IF EXISTS author_id")
|
||||||
DROP INDEX IF EXISTS ix_new_chat_messages_author_id;
|
|
||||||
ALTER TABLE new_chat_messages
|
|
||||||
DROP COLUMN IF EXISTS author_id;
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ def upgrade() -> None:
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
comment_id INTEGER NOT NULL REFERENCES chat_comments(id) ON DELETE CASCADE,
|
comment_id INTEGER NOT NULL REFERENCES chat_comments(id) ON DELETE CASCADE,
|
||||||
mentioned_user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
mentioned_user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
||||||
read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE (comment_id, mentioned_user_id)
|
UNIQUE (comment_id, mentioned_user_id)
|
||||||
)
|
)
|
||||||
|
|
@ -31,9 +30,6 @@ def upgrade() -> None:
|
||||||
op.execute(
|
op.execute(
|
||||||
"CREATE INDEX IF NOT EXISTS idx_chat_comment_mentions_comment_id ON chat_comment_mentions(comment_id)"
|
"CREATE INDEX IF NOT EXISTS idx_chat_comment_mentions_comment_id ON chat_comment_mentions(comment_id)"
|
||||||
)
|
)
|
||||||
op.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_chat_comment_mentions_user_unread ON chat_comment_mentions(mentioned_user_id) WHERE read = FALSE"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,6 @@ class ChatCommentMention(BaseModel, TimestampMixin):
|
||||||
nullable=False,
|
nullable=False,
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
read = Column(Boolean, nullable=False, default=False)
|
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
comment = relationship("ChatComment", back_populates="mentions")
|
comment = relationship("ChatComment", back_populates="mentions")
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@ from app.services.chat_comments_service import (
|
||||||
delete_comment,
|
delete_comment,
|
||||||
get_comments_for_message,
|
get_comments_for_message,
|
||||||
get_user_mentions,
|
get_user_mentions,
|
||||||
mark_all_mentions_as_read,
|
|
||||||
mark_mention_as_read,
|
|
||||||
update_comment,
|
update_comment,
|
||||||
)
|
)
|
||||||
from app.users import current_active_user
|
from app.users import current_active_user
|
||||||
|
|
@ -90,28 +88,8 @@ async def remove_comment(
|
||||||
@router.get("/mentions", response_model=MentionListResponse)
|
@router.get("/mentions", response_model=MentionListResponse)
|
||||||
async def list_mentions(
|
async def list_mentions(
|
||||||
search_space_id: int | None = None,
|
search_space_id: int | None = None,
|
||||||
unread_only: bool = False,
|
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
):
|
):
|
||||||
"""List mentions for the current user."""
|
"""List mentions for the current user."""
|
||||||
return await get_user_mentions(session, user, search_space_id, unread_only)
|
return await get_user_mentions(session, user, search_space_id)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/mentions/{mention_id}/read")
|
|
||||||
async def read_mention(
|
|
||||||
mention_id: int,
|
|
||||||
session: AsyncSession = Depends(get_async_session),
|
|
||||||
user: User = Depends(current_active_user),
|
|
||||||
):
|
|
||||||
"""Mark a specific mention as read."""
|
|
||||||
return await mark_mention_as_read(session, mention_id, user)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/mentions/read-all")
|
|
||||||
async def read_all_mentions(
|
|
||||||
session: AsyncSession = Depends(get_async_session),
|
|
||||||
user: User = Depends(current_active_user),
|
|
||||||
):
|
|
||||||
"""Mark all mentions as read for the current user."""
|
|
||||||
return await mark_all_mentions_as_read(session, user)
|
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,6 @@ class MentionResponse(BaseModel):
|
||||||
"""Schema for a mention notification."""
|
"""Schema for a mention notification."""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
read: bool
|
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
comment: MentionCommentResponse
|
comment: MentionCommentResponse
|
||||||
context: MentionContextResponse
|
context: MentionContextResponse
|
||||||
|
|
@ -127,4 +126,4 @@ class MentionListResponse(BaseModel):
|
||||||
"""Response for listing user's mentions."""
|
"""Response for listing user's mentions."""
|
||||||
|
|
||||||
mentions: list[MentionResponse]
|
mentions: list[MentionResponse]
|
||||||
unread_count: int
|
total_count: int
|
||||||
|
|
|
||||||
|
|
@ -569,7 +569,6 @@ async def get_user_mentions(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
user: User,
|
user: User,
|
||||||
search_space_id: int | None = None,
|
search_space_id: int | None = None,
|
||||||
unread_only: bool = False,
|
|
||||||
) -> MentionListResponse:
|
) -> MentionListResponse:
|
||||||
"""
|
"""
|
||||||
Get mentions for the current user, optionally filtered by search space.
|
Get mentions for the current user, optionally filtered by search space.
|
||||||
|
|
@ -578,10 +577,9 @@ async def get_user_mentions(
|
||||||
session: Database session
|
session: Database session
|
||||||
user: The current authenticated user
|
user: The current authenticated user
|
||||||
search_space_id: Optional search space ID to filter mentions
|
search_space_id: Optional search space ID to filter mentions
|
||||||
unread_only: If True, only return unread mentions
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
MentionListResponse with mentions and unread count
|
MentionListResponse with mentions and total count
|
||||||
"""
|
"""
|
||||||
# Build query with joins for filtering by search_space_id
|
# Build query with joins for filtering by search_space_id
|
||||||
query = (
|
query = (
|
||||||
|
|
@ -599,9 +597,6 @@ async def get_user_mentions(
|
||||||
if search_space_id is not None:
|
if search_space_id is not None:
|
||||||
query = query.filter(NewChatThread.search_space_id == search_space_id)
|
query = query.filter(NewChatThread.search_space_id == search_space_id)
|
||||||
|
|
||||||
if unread_only:
|
|
||||||
query = query.filter(ChatCommentMention.read.is_(False))
|
|
||||||
|
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
mention_records = result.scalars().all()
|
mention_records = result.scalars().all()
|
||||||
|
|
||||||
|
|
@ -617,9 +612,6 @@ async def get_user_mentions(
|
||||||
else:
|
else:
|
||||||
threads_map = {}
|
threads_map = {}
|
||||||
|
|
||||||
# Count unread from fetched data
|
|
||||||
unread_count = sum(1 for m in mention_records if not m.read)
|
|
||||||
|
|
||||||
mentions = []
|
mentions = []
|
||||||
for mention in mention_records:
|
for mention in mention_records:
|
||||||
comment = mention.comment
|
comment = mention.comment
|
||||||
|
|
@ -645,7 +637,6 @@ async def get_user_mentions(
|
||||||
mentions.append(
|
mentions.append(
|
||||||
MentionResponse(
|
MentionResponse(
|
||||||
id=mention.id,
|
id=mention.id,
|
||||||
read=mention.read,
|
|
||||||
created_at=mention.created_at,
|
created_at=mention.created_at,
|
||||||
comment=MentionCommentResponse(
|
comment=MentionCommentResponse(
|
||||||
id=comment.id,
|
id=comment.id,
|
||||||
|
|
@ -665,75 +656,5 @@ async def get_user_mentions(
|
||||||
|
|
||||||
return MentionListResponse(
|
return MentionListResponse(
|
||||||
mentions=mentions,
|
mentions=mentions,
|
||||||
unread_count=unread_count,
|
total_count=len(mentions),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def mark_mention_as_read(
|
|
||||||
session: AsyncSession,
|
|
||||||
mention_id: int,
|
|
||||||
user: User,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Mark a specific mention as read.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: Database session
|
|
||||||
mention_id: ID of the mention to mark as read
|
|
||||||
user: The current authenticated user
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with mention_id and read status
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: If mention not found or doesn't belong to user
|
|
||||||
"""
|
|
||||||
result = await session.execute(
|
|
||||||
select(ChatCommentMention).filter(ChatCommentMention.id == mention_id)
|
|
||||||
)
|
|
||||||
mention = result.scalars().first()
|
|
||||||
|
|
||||||
if not mention:
|
|
||||||
raise HTTPException(status_code=404, detail="Mention not found")
|
|
||||||
|
|
||||||
if mention.mentioned_user_id != user.id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail="You can only mark your own mentions as read",
|
|
||||||
)
|
|
||||||
|
|
||||||
mention.read = True
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
return {"mention_id": mention_id, "read": True}
|
|
||||||
|
|
||||||
|
|
||||||
async def mark_all_mentions_as_read(
|
|
||||||
session: AsyncSession,
|
|
||||||
user: User,
|
|
||||||
) -> dict:
|
|
||||||
"""
|
|
||||||
Mark all mentions for the current user as read.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session: Database session
|
|
||||||
user: The current authenticated user
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with count of mentions marked as read
|
|
||||||
"""
|
|
||||||
from sqlalchemy import update
|
|
||||||
|
|
||||||
result = await session.execute(
|
|
||||||
update(ChatCommentMention)
|
|
||||||
.where(
|
|
||||||
ChatCommentMention.mentioned_user_id == user.id,
|
|
||||||
ChatCommentMention.read.is_(False),
|
|
||||||
)
|
|
||||||
.values(read=True)
|
|
||||||
.returning(ChatCommentMention.id)
|
|
||||||
)
|
|
||||||
marked_ids = result.scalars().all()
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
return {"message": "All mentions marked as read", "count": len(marked_ids)}
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import type {
|
||||||
CreateCommentRequest,
|
CreateCommentRequest,
|
||||||
CreateReplyRequest,
|
CreateReplyRequest,
|
||||||
DeleteCommentRequest,
|
DeleteCommentRequest,
|
||||||
MarkMentionReadRequest,
|
|
||||||
UpdateCommentRequest,
|
UpdateCommentRequest,
|
||||||
} from "@/contracts/types/chat-comments.types";
|
} from "@/contracts/types/chat-comments.types";
|
||||||
import { chatCommentsApiService } from "@/lib/apis/chat-comments-api.service";
|
import { chatCommentsApiService } from "@/lib/apis/chat-comments-api.service";
|
||||||
|
|
@ -71,39 +70,3 @@ export const deleteCommentMutationAtom = atomWithMutation(() => ({
|
||||||
toast.error("Failed to delete comment");
|
toast.error("Failed to delete comment");
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const markMentionReadMutationAtom = atomWithMutation(() => ({
|
|
||||||
mutationFn: async (request: MarkMentionReadRequest & { search_space_id?: number }) => {
|
|
||||||
return chatCommentsApiService.markMentionRead(request);
|
|
||||||
},
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: cacheKeys.mentions.all(variables.search_space_id),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: cacheKeys.mentions.unreadOnly(variables.search_space_id),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
console.error("Error marking mention as read:", error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const markAllMentionsReadMutationAtom = atomWithMutation(() => ({
|
|
||||||
mutationFn: async (request: { search_space_id?: number }) => {
|
|
||||||
return chatCommentsApiService.markAllMentionsRead();
|
|
||||||
},
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: cacheKeys.mentions.all(variables.search_space_id),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: cacheKeys.mentions.unreadOnly(variables.search_space_id),
|
|
||||||
});
|
|
||||||
toast.success("All mentions marked as read");
|
|
||||||
},
|
|
||||||
onError: (error: Error) => {
|
|
||||||
console.error("Error marking all mentions as read:", error);
|
|
||||||
toast.error("Failed to mark mentions as read");
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,3 @@ export const mentionsAtom = atomWithQuery((get) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const unreadMentionsAtom = atomWithQuery((get) => {
|
|
||||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
|
||||||
|
|
||||||
return {
|
|
||||||
queryKey: cacheKeys.mentions.unreadOnly(searchSpaceId ? Number(searchSpaceId) : undefined),
|
|
||||||
queryFn: async () => {
|
|
||||||
return chatCommentsApiService.getMentions({
|
|
||||||
search_space_id: searchSpaceId ? Number(searchSpaceId) : undefined,
|
|
||||||
unread_only: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ export const mentionComment = z.object({
|
||||||
|
|
||||||
export const mention = z.object({
|
export const mention = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
read: z.boolean(),
|
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
comment: mentionComment,
|
comment: mentionComment,
|
||||||
context: mentionContext,
|
context: mentionContext,
|
||||||
|
|
@ -116,32 +115,11 @@ export const deleteCommentResponse = z.object({
|
||||||
*/
|
*/
|
||||||
export const getMentionsRequest = z.object({
|
export const getMentionsRequest = z.object({
|
||||||
search_space_id: z.number().optional(),
|
search_space_id: z.number().optional(),
|
||||||
unread_only: z.boolean().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getMentionsResponse = z.object({
|
export const getMentionsResponse = z.object({
|
||||||
mentions: z.array(mention),
|
mentions: z.array(mention),
|
||||||
unread_count: z.number(),
|
total_count: z.number(),
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark mention as read
|
|
||||||
*/
|
|
||||||
export const markMentionReadRequest = z.object({
|
|
||||||
mention_id: z.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const markMentionReadResponse = z.object({
|
|
||||||
mention_id: z.number(),
|
|
||||||
read: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark all mentions as read
|
|
||||||
*/
|
|
||||||
export const markAllMentionsReadResponse = z.object({
|
|
||||||
message: z.string(),
|
|
||||||
count: z.number(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Author = z.infer<typeof author>;
|
export type Author = z.infer<typeof author>;
|
||||||
|
|
@ -162,6 +140,3 @@ export type DeleteCommentRequest = z.infer<typeof deleteCommentRequest>;
|
||||||
export type DeleteCommentResponse = z.infer<typeof deleteCommentResponse>;
|
export type DeleteCommentResponse = z.infer<typeof deleteCommentResponse>;
|
||||||
export type GetMentionsRequest = z.infer<typeof getMentionsRequest>;
|
export type GetMentionsRequest = z.infer<typeof getMentionsRequest>;
|
||||||
export type GetMentionsResponse = z.infer<typeof getMentionsResponse>;
|
export type GetMentionsResponse = z.infer<typeof getMentionsResponse>;
|
||||||
export type MarkMentionReadRequest = z.infer<typeof markMentionReadRequest>;
|
|
||||||
export type MarkMentionReadResponse = z.infer<typeof markMentionReadResponse>;
|
|
||||||
export type MarkAllMentionsReadResponse = z.infer<typeof markAllMentionsReadResponse>;
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ import {
|
||||||
getCommentsResponse,
|
getCommentsResponse,
|
||||||
getMentionsRequest,
|
getMentionsRequest,
|
||||||
getMentionsResponse,
|
getMentionsResponse,
|
||||||
type MarkMentionReadRequest,
|
|
||||||
markAllMentionsReadResponse,
|
|
||||||
markMentionReadRequest,
|
|
||||||
markMentionReadResponse,
|
|
||||||
type UpdateCommentRequest,
|
type UpdateCommentRequest,
|
||||||
updateCommentRequest,
|
updateCommentRequest,
|
||||||
updateCommentResponse,
|
updateCommentResponse,
|
||||||
|
|
@ -127,39 +123,12 @@ class ChatCommentsApiService {
|
||||||
if (parsed.data.search_space_id !== undefined) {
|
if (parsed.data.search_space_id !== undefined) {
|
||||||
params.set("search_space_id", String(parsed.data.search_space_id));
|
params.set("search_space_id", String(parsed.data.search_space_id));
|
||||||
}
|
}
|
||||||
if (parsed.data.unread_only !== undefined) {
|
|
||||||
params.set("unread_only", String(parsed.data.unread_only));
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = queryString ? `/api/v1/mentions?${queryString}` : "/api/v1/mentions";
|
const url = queryString ? `/api/v1/mentions?${queryString}` : "/api/v1/mentions";
|
||||||
|
|
||||||
return baseApiService.get(url, getMentionsResponse);
|
return baseApiService.get(url, getMentionsResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a mention as read
|
|
||||||
*/
|
|
||||||
markMentionRead = async (request: MarkMentionReadRequest) => {
|
|
||||||
const parsed = markMentionReadRequest.safeParse(request);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", ");
|
|
||||||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseApiService.put(
|
|
||||||
`/api/v1/mentions/${parsed.data.mention_id}/read`,
|
|
||||||
markMentionReadResponse
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark all mentions as read
|
|
||||||
*/
|
|
||||||
markAllMentionsRead = async () => {
|
|
||||||
return baseApiService.put("/api/v1/mentions/read-all", markAllMentionsReadResponse);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chatCommentsApiService = new ChatCommentsApiService();
|
export const chatCommentsApiService = new ChatCommentsApiService();
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,5 @@ export const cacheKeys = {
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
all: (searchSpaceId?: number) => ["mentions", searchSpaceId] as const,
|
all: (searchSpaceId?: number) => ["mentions", searchSpaceId] as const,
|
||||||
unreadOnly: (searchSpaceId?: number) => ["mentions", "unread", searchSpaceId] as const,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue