Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/backend/src/graphql/resolvers/social/session-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@ export const sessionFeedQueries = {

if (tickRows.length === 0) return null;

// Batch-fetch tick vote counts
const tickUuids = tickRows.map((r) => r.tick.uuid);
const tickVoteCounts = tickUuids.length > 0
? await db
.select({
entityId: dbSchema.voteCounts.entityId,
upvotes: sql<number>`COALESCE(${dbSchema.voteCounts.upvotes}, 0)`,
})
.from(dbSchema.voteCounts)
.where(
and(
eq(dbSchema.voteCounts.entityType, 'tick'),
inArray(dbSchema.voteCounts.entityId, tickUuids),
),
)
: [];
const tickVoteMap = new Map(tickVoteCounts.map((v) => [v.entityId, Number(v.upvotes)]));

// Build ticks
const ticks: SessionDetailTick[] = tickRows.map((row) => ({
uuid: row.tick.uuid,
Expand All @@ -272,6 +290,7 @@ export const sessionFeedQueries = {
frames: row.frames || null,
setterUsername: row.setterUsername || null,
climbedAt: row.tick.climbedAt,
upvotes: tickVoteMap.get(row.tick.uuid) ?? 0,
}));

// Compute aggregates
Expand Down
1 change: 1 addition & 0 deletions packages/shared-schema/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2643,6 +2643,7 @@ export const typeDefs = /* GraphQL */ `
frames: String
setterUsername: String
climbedAt: String!
upvotes: Int!
}

"""
Expand Down
1 change: 1 addition & 0 deletions packages/shared-schema/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ export type SessionDetailTick = {
frames?: string | null;
setterUsername?: string | null;
climbedAt: string;
upvotes: number;
};

export type SessionDetail = {
Expand Down
4 changes: 3 additions & 1 deletion packages/web/app/components/social/feed-comment-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ interface FeedCommentButtonProps {
entityType: SocialEntityType;
entityId: string;
commentCount?: number;
defaultExpanded?: boolean;
}

export default function FeedCommentButton({
entityType,
entityId,
commentCount = 0,
defaultExpanded = false,
}: FeedCommentButtonProps) {
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(defaultExpanded);

const handleToggle = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
Expand Down
4 changes: 4 additions & 0 deletions packages/web/app/lib/graphql/operations/activity-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const GET_SESSION_DETAIL = gql`
frames
setterUsername
climbedAt
upvotes
}
}
}
Expand Down Expand Up @@ -107,6 +108,7 @@ export const UPDATE_INFERRED_SESSION = gql`
frames
setterUsername
climbedAt
upvotes
}
}
}
Expand Down Expand Up @@ -135,6 +137,7 @@ export const ADD_USER_TO_SESSION = gql`
frames
setterUsername
climbedAt
upvotes
}
}
}
Expand Down Expand Up @@ -163,6 +166,7 @@ export const REMOVE_USER_FROM_SESSION = gql`
frames
setterUsername
climbedAt
upvotes
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions packages/web/app/session/[sessionId]/session-detail-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { useSession } from 'next-auth/react';
import type { SessionDetail, SessionDetailTick, SessionFeedParticipant } from '@boardsesh/shared-schema';
import GradeDistributionBar from '@/app/components/charts/grade-distribution-bar';
import VoteButton from '@/app/components/social/vote-button';
import CommentSection from '@/app/components/social/comment-section';
import FeedCommentButton from '@/app/components/social/feed-comment-button';
import ClimbsList from '@/app/components/board-page/climbs-list';
import { FavoritesProvider } from '@/app/components/climb-actions/favorites-batch-context';
import { PlaylistsProvider } from '@/app/components/climb-actions/playlists-batch-context';
Expand Down Expand Up @@ -201,6 +201,7 @@ export default function SessionDetailContent({ session: initialSession }: Sessio
ticks,
upvotes,
downvotes,
commentCount,
} = session;

const currentUserId = authSession?.user?.id;
Expand Down Expand Up @@ -367,9 +368,8 @@ export default function SessionDetailContent({ session: initialSession }: Sessio
<VoteButton
entityType="tick"
entityId={tick.uuid}
initialUpvotes={0}
initialUpvotes={tick.upvotes}
initialDownvotes={0}
initialUserVote={0}
likeOnly
/>
</Box>
Expand Down Expand Up @@ -676,10 +676,14 @@ export default function SessionDetailContent({ session: initialSession }: Sessio
initialDownvotes={downvotes}
likeOnly
/>
<FeedCommentButton
entityType="session"
entityId={sessionId}
commentCount={commentCount}
defaultExpanded
/>
</Box>

<CommentSection entityType="session" entityId={sessionId} />

<Divider />

{/* Climbs list */}
Expand Down
31 changes: 17 additions & 14 deletions packages/web/app/session/__tests__/session-detail-content.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ vi.mock('@/app/components/charts/grade-distribution-bar', () => ({
}));

vi.mock('@/app/components/social/vote-button', () => ({
default: ({ entityType, entityId }: { entityType: string; entityId: string }) => (
<div data-testid="vote-button" data-entity-type={entityType} data-entity-id={entityId} />
default: ({ entityType, entityId, initialUpvotes }: { entityType: string; entityId: string; initialUpvotes?: number }) => (
<div data-testid="vote-button" data-entity-type={entityType} data-entity-id={entityId} data-initial-upvotes={initialUpvotes ?? 0} />
),
}));

Expand All @@ -66,9 +66,9 @@ vi.mock('@/app/components/social/vote-summary-context', () => ({
useVoteSummaryContext: () => null,
}));

vi.mock('@/app/components/social/comment-section', () => ({
default: ({ entityType, entityId }: { entityType: string; entityId: string }) => (
<div data-testid="comment-section" data-entity-type={entityType} data-entity-id={entityId} />
vi.mock('@/app/components/social/feed-comment-button', () => ({
default: ({ entityType, entityId, commentCount }: { entityType: string; entityId: string; commentCount?: number }) => (
<div data-testid="feed-comment-button" data-entity-type={entityType} data-entity-id={entityId} data-comment-count={commentCount ?? 0} />
),
}));

Expand Down Expand Up @@ -169,6 +169,7 @@ function makeSession(overrides: Partial<SessionDetail> = {}): SessionDetail {
frames: 'abc',
setterUsername: 'setter1',
climbedAt: '2024-01-15T10:30:00.000Z',
upvotes: 3,
},
],
upvotes: 5,
Expand Down Expand Up @@ -210,11 +211,12 @@ describe('SessionDetailContent', () => {
expect(sessionVote!.getAttribute('data-entity-id')).toBe('session-1');
});

it('renders CommentSection with session entity type', () => {
it('renders FeedCommentButton with session entity type and comment count', () => {
render(<SessionDetailContent session={makeSession()} />);
const commentSection = screen.getByTestId('comment-section');
expect(commentSection.getAttribute('data-entity-type')).toBe('session');
expect(commentSection.getAttribute('data-entity-id')).toBe('session-1');
const commentButton = screen.getByTestId('feed-comment-button');
expect(commentButton.getAttribute('data-entity-type')).toBe('session');
expect(commentButton.getAttribute('data-entity-id')).toBe('session-1');
expect(commentButton.getAttribute('data-comment-count')).toBe('2');
});

it('shows session name when available', () => {
Expand Down Expand Up @@ -267,14 +269,15 @@ describe('SessionDetailContent', () => {
expect(screen.getByText('Hardest: V8')).toBeTruthy();
});

it('renders per-tick VoteButton with tick entity type for single-user sessions', () => {
it('renders per-tick VoteButton with tick entity type and SSR upvotes', () => {
render(<SessionDetailContent session={makeSession()} />);
const voteButtons = screen.getAllByTestId('vote-button');
const tickVote = voteButtons.find(
(el) => el.getAttribute('data-entity-type') === 'tick',
);
expect(tickVote).toBeTruthy();
expect(tickVote!.getAttribute('data-entity-id')).toBe('tick-1');
expect(tickVote!.getAttribute('data-initial-upvotes')).toBe('3');
});

it('renders tick status details for single-user sessions', () => {
Expand All @@ -290,13 +293,13 @@ describe('SessionDetailContent', () => {
uuid: 'tick-1', userId: 'user-1', climbUuid: 'climb-1', climbName: 'Duplicated Climb',
boardType: 'kilter', layoutId: 1, angle: 40, status: 'send', attemptCount: 1,
difficulty: 20, difficultyName: 'V5', quality: 3, isMirror: false, isBenchmark: false,
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:30:00.000Z',
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:30:00.000Z', upvotes: 0,
},
{
uuid: 'tick-2', userId: 'user-1', climbUuid: 'climb-1', climbName: 'Duplicated Climb',
boardType: 'kilter', layoutId: 1, angle: 40, status: 'flash', attemptCount: 1,
difficulty: 20, difficultyName: 'V5', quality: 3, isMirror: false, isBenchmark: false,
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T11:00:00.000Z',
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T11:00:00.000Z', upvotes: 0,
},
],
});
Expand All @@ -316,13 +319,13 @@ describe('SessionDetailContent', () => {
uuid: 'tick-1', userId: 'user-1', climbUuid: 'climb-1', climbName: 'Shared Climb',
boardType: 'kilter', layoutId: 1, angle: 40, status: 'send', attemptCount: 2,
difficulty: 20, difficultyName: 'V5', quality: 3, isMirror: false, isBenchmark: false,
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:30:00.000Z',
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:30:00.000Z', upvotes: 0,
},
{
uuid: 'tick-2', userId: 'user-2', climbUuid: 'climb-1', climbName: 'Shared Climb',
boardType: 'kilter', layoutId: 1, angle: 40, status: 'flash', attemptCount: 1,
difficulty: 20, difficultyName: 'V5', quality: 3, isMirror: false, isBenchmark: false,
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:35:00.000Z',
comment: null, frames: 'abc', setterUsername: 'setter1', climbedAt: '2024-01-15T10:35:00.000Z', upvotes: 0,
},
],
});
Expand Down
Loading