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
20 changes: 19 additions & 1 deletion src/post/dto/create-post.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
import { IsArray, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, MaxLength } from 'class-validator';
import { PostType, PostVisibility } from '@prisma/client';
import { Transform } from 'class-transformer';
import { IsParentIdAllowed } from '../decorators/is-parent-id-allowed.decorator';
Expand Down Expand Up @@ -62,5 +62,23 @@ export class CreatePostDto {
})
media?: Express.Multer.File[];

@ApiPropertyOptional({
type: [Number],
description: 'Optional array of user IDs to mention',
example: [1, 2, 3],
})
@IsOptional()
@IsArray()
@IsNumber({}, { each: true })
@Transform(({ value }) => {
if (!value) return undefined;
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed.map((v) => Number(v));
}
return value;
})
mentionsIds?: number[];

userId: number;
}
1 change: 1 addition & 0 deletions src/post/interfaces/post.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface RawPost {
media: Media[];
likes: { user_id: number; }[];
repostedBy: { user_id: number; }[];
mentions: { user_id: number; }[];
}

export interface TransformedPost {
Expand Down
30 changes: 24 additions & 6 deletions src/post/services/like.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { Services } from 'src/utils/constants';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { NotificationType } from 'src/notifications/enums/notification.enum';
import { PostService } from './post.service';

@Injectable()
export class LikeService {
constructor(
@Inject(Services.PRISMA)
private readonly prismaService: PrismaService,
@Inject(Services.POST)
private readonly postService: PostService,
private readonly eventEmitter: EventEmitter2,
) {}
) { }

async togglePostLike(postId: number, userId: number) {
const existingLike = await this.prismaService.like.findUnique({
Expand Down Expand Up @@ -85,15 +88,30 @@ export class LikeService {
async getLikedPostsByUser(userId: number, page: number, limit: number) {
const likes = await this.prismaService.like.findMany({
where: { user_id: userId },
include: { post: true },
select: { post_id: true, created_at: true },
orderBy: { created_at: 'desc' },
skip: (page - 1) * limit,
take: limit,
});

const likedPostsIds = likes.map(like => like.post_id);

return likes.map((like) => ({
...like.post,
liked_at: like.created_at,
}));
const likedPosts = await this.postService.findPosts({
where: {
is_deleted: false,
id: { in: likedPostsIds }
},
userId,
limit,
page
})
const orderMap = new Map(
likes.map((m, index) => [m.post_id, index])
);

likedPosts.sort(
(a, b) => orderMap.get(a.postId)! - orderMap.get(b.postId)!
);
return likedPosts;
}
}
24 changes: 18 additions & 6 deletions src/post/services/mention.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { Services } from 'src/utils/constants';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { NotificationType } from 'src/notifications/enums/notification.enum';
import { PostService } from './post.service';

@Injectable()
export class MentionService {
constructor(
@Inject(Services.PRISMA)
private readonly prismaService: PrismaService,
@Inject(Services.POST)
private readonly postService: PostService,
private readonly eventEmitter: EventEmitter2,
) {}
) { }

private async checkUserExists(userId: number) {
const user = await this.prismaService.user.findUnique({
Expand Down Expand Up @@ -74,17 +77,26 @@ export class MentionService {
async getMentionedPosts(userId: number, page: number, limit: number) {
const mentions = await this.prismaService.mention.findMany({
where: { user_id: userId },
include: { post: true },
select: { post_id: true, created_at: true },
distinct: ['post_id'],
orderBy: { created_at: 'desc' },
skip: (page - 1) * limit,
take: limit,
});

return mentions.map((mention) => ({
...mention.post,
mentionedAt: mention.created_at,
}));
const postsIds = mentions.map(mention => mention.post_id);

const mentionPosts = await this.postService.findPosts({
where: {
is_deleted: false,
id: { in: postsIds }
},
userId,
limit,
page
})

return mentionPosts
}

async getMentionsForPost(postId: number, page: number = 1, limit: number = 10) {
Expand Down
154 changes: 95 additions & 59 deletions src/post/services/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export class PostService {
@InjectQueue(RedisQueues.postQueue.name)
private readonly postQueue: Queue,
private readonly eventEmitter: EventEmitter2,
) {}
) { }

private extractHashtags(content: string): string[] {
if (!content) return [];
Expand Down Expand Up @@ -282,7 +282,7 @@ export class PostService {
return stats;
}

private async findPosts(options: {
async findPosts(options: {
where: PrismalSql.PostWhereInput;
userId: number;
page?: number;
Expand Down Expand Up @@ -330,6 +330,7 @@ export class PostService {
where: { user_id: userId },
select: { user_id: true },
},
mentions: { where: { user_id: userId }, select: { user_id: true } },
},
skip: (page - 1) * limit,
take: limit,
Expand Down Expand Up @@ -389,6 +390,13 @@ export class PostService {
})),
});

await tx.mention.createMany({
data: postData.mentionsIds?.map((id) => ({
post_id: post.id,
user_id: id,
})) ?? [],
});

// Handle notifications after transaction
if (postData.parentId) {
// Fetch parent post to get author
Expand Down Expand Up @@ -424,11 +432,30 @@ export class PostService {
});
}

private async checkUsersExistence(usersIds: number[]) {
if (usersIds.length === 0) {
return;
}
const uniqueIds = Array.from(new Set(usersIds));

const existingUsers = await this.prismaService.user.findMany({
where: { id: { in: uniqueIds } },
select: { id: true },
});


if (existingUsers.length !== uniqueIds.length) {
throw new UnprocessableEntityException("Some user IDs are invalid")
}
}

async createPost(createPostDto: CreatePostDto) {
let urls: string[] = [];
try {
const { content, media, userId } = createPostDto;
urls = await this.storageService.uploadFiles(media);
console.log(createPostDto.mentionsIds)
await this.checkUsersExistence(createPostDto.mentionsIds ?? [])

const hashtags = this.extractHashtags(content);

Expand Down Expand Up @@ -484,16 +511,16 @@ export class PostService {

const where = hasFilters
? {
...(userId && { user_id: userId }),
...(hashtag && { hashtags: { some: { tag: hashtag } } }),
...(type && { type }),
is_deleted: false,
}
...(userId && { user_id: userId }),
...(hashtag && { hashtags: { some: { tag: hashtag } } }),
...(type && { type }),
is_deleted: false,
}
: {
// TODO: improve this fallback
visibility: PostVisibility.EVERY_ONE, // fallback: only public posts
is_deleted: false,
};
// TODO: improve this fallback
visibility: PostVisibility.EVERY_ONE, // fallback: only public posts
is_deleted: false,
};

const posts = await this.prismaService.post.findMany({
where,
Expand Down Expand Up @@ -684,12 +711,12 @@ export class PostService {
isSimpleRepost && post.repostedBy
? post.repostedBy
: {
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
};
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
};

// Build originalPostData
let originalPostData: any = null;
Expand Down Expand Up @@ -1010,12 +1037,21 @@ export class PostService {
url: m.media_url,
type: m.type,
})),
mentions: post.mentions,
isRepost: false,
isQuote: PostType.QUOTE === post.type,
createdAt: post.created_at,
}));
}

// async getUserMedia(userId: number, page:number, limit: number){
// return await this.prismaService.media.findMany({
// where:{
// user
// }
// })
// }

async getUserReplies(userId: number, page: number, limit: number) {
return await this.findPosts({
where: {
Expand Down Expand Up @@ -1667,12 +1703,12 @@ export class PostService {
isSimpleRepost && post.repostedBy
? post.repostedBy
: {
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
};
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
};

return {
// User Information (reposter for simple reposts, author otherwise)
Expand Down Expand Up @@ -1706,42 +1742,42 @@ export class PostService {
originalPostData:
isSimpleRepost || isQuote
? {
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
postId: post.id,
date: post.created_at,
likesCount: post.likeCount,
retweetsCount: post.repostCount,
commentsCount: post.replyCount,
isLikedByMe: post.isLikedByMe,
isFollowedByMe: post.isFollowedByMe,
isRepostedByMe: post.isRepostedByMe || false,
text: post.content || '',
media: Array.isArray(post.mediaUrls) ? post.mediaUrls : [],
...(isQuote && post.originalPost
? {
// Override with quoted post data for quotes
userId: post.originalPost.author.userId,
username: post.originalPost.author.username,
verified: post.originalPost.author.isVerified,
name: post.originalPost.author.name,
avatar: post.originalPost.author.avatar,
postId: post.originalPost.postId,
date: post.originalPost.createdAt,
likesCount: post.originalPost.likeCount,
retweetsCount: post.originalPost.repostCount,
commentsCount: post.originalPost.replyCount,
isLikedByMe: post.originalPost.isLikedByMe,
isFollowedByMe: post.originalPost.isFollowedByMe,
isRepostedByMe: post.originalPost.isRepostedByMe,
text: post.originalPost.content || '',
media: post.originalPost.media || [],
}
: {}),
}
userId: post.user_id,
username: post.username,
verified: post.isVerified,
name: post.authorName || post.username,
avatar: post.authorProfileImage,
postId: post.id,
date: post.created_at,
likesCount: post.likeCount,
retweetsCount: post.repostCount,
commentsCount: post.replyCount,
isLikedByMe: post.isLikedByMe,
isFollowedByMe: post.isFollowedByMe,
isRepostedByMe: post.isRepostedByMe || false,
text: post.content || '',
media: Array.isArray(post.mediaUrls) ? post.mediaUrls : [],
...(isQuote && post.originalPost
? {
// Override with quoted post data for quotes
userId: post.originalPost.author.userId,
username: post.originalPost.author.username,
verified: post.originalPost.author.isVerified,
name: post.originalPost.author.name,
avatar: post.originalPost.author.avatar,
postId: post.originalPost.postId,
date: post.originalPost.createdAt,
likesCount: post.originalPost.likeCount,
retweetsCount: post.originalPost.repostCount,
commentsCount: post.originalPost.replyCount,
isLikedByMe: post.originalPost.isLikedByMe,
isFollowedByMe: post.originalPost.isFollowedByMe,
isRepostedByMe: post.originalPost.isRepostedByMe,
text: post.originalPost.content || '',
media: post.originalPost.media || [],
}
: {}),
}
: undefined,

// Scores data
Expand Down
Loading