Add reels feed support

This commit is contained in:
boutmoun123
2026-07-06 15:17:53 +03:00
parent 830b0fdf84
commit 1b24ca4294
4 changed files with 304 additions and 18 deletions
+61 -16
View File
@@ -17,7 +17,7 @@ import { PostShareTarget } from '../posts/dto/share-post.dto';
import { SavesRepository } from '../saves/saves.repository';
import { UserDocument } from '../users/schemas/user.schema';
import { UsersRepository } from '../users/users.repository';
import { FeedQueryDto } from './dto/feed-query.dto';
import { FeedPostTypeFilter, FeedQueryDto } from './dto/feed-query.dto';
import { FeedRepository } from './feed.repository';
type FeedPostItem = Record<string, unknown> & {
@@ -119,6 +119,7 @@ export class FeedService {
const page = query.page ?? 1;
const radiusKm = query.radiusKm ?? 30;
const skip = cursorOffset ?? (page - 1) * limit;
const requestedPostType = this.resolveRequestedPostType(query.preferredPostType);
const [followingIds, invisibleUserIds] = await Promise.all([
this.feedRepository.findFollowingIds(currentUserId),
@@ -126,6 +127,9 @@ export class FeedService {
]);
const relationLookupMs = this.markTiming(timing);
let filter = this.buildVisiblePostsFilter(currentUserId, followingIds, followingOnly, invisibleUserIds);
if (requestedPostType) {
filter = { $and: [filter, { postType: requestedPostType }] };
}
let candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
const firstCandidateLookupMs = this.markTiming(timing);
@@ -137,17 +141,18 @@ export class FeedService {
followingOnly
) {
filter = this.buildVisiblePostsFilter(currentUserId, followingIds, false, invisibleUserIds);
if (requestedPostType) {
filter = { $and: [filter, { postType: requestedPostType }] };
}
candidates = await this.feedRepository.findCandidatePosts(filter, Math.max(limit * 12, 300));
}
const fallbackCandidateLookupMs = this.markTiming(timing);
const scored = candidates
.filter((post) => {
if (!query.preferredPostType) {
return true;
}
return post.postType === query.preferredPostType;
})
const filteredCandidates = requestedPostType
? candidates.filter((post) => post.postType === requestedPostType)
: candidates;
const scored = filteredCandidates
.map((post) => ({
post,
score: this.scorePost({
@@ -173,10 +178,14 @@ export class FeedService {
feedScore: Number(entry.score.toFixed(3)),
}));
const decoratedPosts = await this.decoratePostsForViewer(currentUserId, pagedPosts, followingIds);
const normalizedPosts = this.normalizePreferredPostTypeForResponse(
decoratedPosts,
query.preferredPostType,
);
const decorationMs = this.markTiming(timing);
const items = includeSuggestions
? await this.mixHomeFeedItems(currentUserId, decoratedPosts, query.suggestionInterval ?? 4)
: decoratedPosts;
? await this.mixHomeFeedItems(currentUserId, normalizedPosts, query.suggestionInterval ?? 4)
: normalizedPosts;
const cardsMs = this.markTiming(timing);
const nextOffset = skip + pagedPosts.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
@@ -209,9 +218,10 @@ export class FeedService {
followingCount: followingIds.length,
invisibleUserCount: invisibleUserIds.length,
candidateCount: candidates.length,
filteredCandidateCount: filteredCandidates.length,
scoredCount: scored.length,
itemCount: items.length,
postItemCount: decoratedPosts.length,
postItemCount: normalizedPosts.length,
nextCursor,
relationLookupMs,
firstCandidateLookupMs,
@@ -233,6 +243,14 @@ export class FeedService {
});
}
async getReels(currentUserId: string, query: FeedQueryDto) {
return this.getMyFeed(currentUserId, {
...query,
preferredPostType: 'reel',
includeSuggestions: false,
});
}
async getTrending(currentUserId: string, query: FeedQueryDto) {
const timing = this.startTiming('feed.trending');
const cacheEnabled =
@@ -272,8 +290,9 @@ export class FeedService {
if (invisibleUserIds.length) {
trendingFilter.authorId = { $nin: invisibleUserIds.map((id) => new Types.ObjectId(id)) };
}
if (query.preferredPostType) {
trendingFilter.postType = query.preferredPostType;
const requestedPostType = this.resolveRequestedPostType(query.preferredPostType);
if (requestedPostType) {
trendingFilter.postType = requestedPostType;
}
const [rows, total] = await Promise.all([
@@ -286,11 +305,15 @@ export class FeedService {
rows.map((item) => item.toObject() as unknown as Record<string, unknown>),
followingIds,
);
const normalizedPosts = this.normalizePreferredPostTypeForResponse(
decoratedPosts,
query.preferredPostType,
);
const decorationMs = this.markTiming(timing);
const nextOffset = skip + rows.length;
const nextCursor = nextOffset < total ? encodeOffsetCursor(nextOffset) : null;
const result = buildPaginatedResponse(decoratedPosts, {
const result = buildPaginatedResponse(normalizedPosts, {
page,
limit,
total,
@@ -316,7 +339,7 @@ export class FeedService {
preferredPostType: query.preferredPostType ?? '',
followingCount: followingIds.length,
invisibleUserCount: invisibleUserIds.length,
itemCount: decoratedPosts.length,
itemCount: normalizedPosts.length,
total,
nextCursor,
relationLookupMs,
@@ -537,7 +560,7 @@ export class FeedService {
currentUserId: string;
followingIds: string[];
post: Record<string, any>;
preferredPostType?: PostType;
preferredPostType?: FeedPostTypeFilter;
radiusKm: number;
}): number {
const { currentUser, currentUserId, followingIds, post, preferredPostType, radiusKm } = input;
@@ -680,6 +703,28 @@ export class FeedService {
return '';
}
private resolveRequestedPostType(preferredPostType?: FeedPostTypeFilter): PostType | null {
if (!preferredPostType) {
return null;
}
return preferredPostType === 'reel' ? PostType.VIDEO : preferredPostType;
}
private normalizePreferredPostTypeForResponse<T extends FeedPostItem>(
items: T[],
preferredPostType?: FeedPostTypeFilter,
): T[] {
if (preferredPostType !== 'reel') {
return items;
}
return items.map((item) => ({
...item,
postType: 'reel',
}));
}
private startTiming(scope: string): { scope: string; startedAt: number; lastAt: number } {
const now = performance.now();
return { scope, startedAt: now, lastAt: now };