Add Instagram-style social features and Postman collections

هذا الالتزام موجود في:
boutmoun123
2026-05-24 15:21:03 +03:00
الأصل fdc40192f7
التزام 367fce6557
56 ملفات معدلة مع 20266 إضافات و5965 حذوفات

عرض الملف

@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsMongoId } from 'class-validator';
import { IsEnum, IsIn, IsMongoId, IsOptional } from 'class-validator';
import { ReactionType } from '../../../common/enums/reaction-type.enum';
export class ToggleLikeDto {
@ApiProperty()
@@ -9,4 +10,9 @@ export class ToggleLikeDto {
@ApiProperty({ enum: ['post', 'comment'] })
@IsIn(['post', 'comment'])
targetType!: 'post' | 'comment';
@ApiProperty({ enum: ReactionType, required: false, default: ReactionType.LIKE })
@IsOptional()
@IsEnum(ReactionType)
reactionType?: ReactionType;
}

عرض الملف

@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { ReactionType } from '../../common/enums/reaction-type.enum';
import { Like, LikeDocument } from './schemas/like.schema';
@Injectable()
@@ -17,14 +18,24 @@ export class LikesRepository {
.exec();
}
async create(userId: string, targetId: string, targetType: 'post' | 'comment'): Promise<LikeDocument> {
async create(
userId: string,
targetId: string,
targetType: 'post' | 'comment',
reactionType: ReactionType = ReactionType.LIKE,
): Promise<LikeDocument> {
return this.likeModel.create({
userId: new Types.ObjectId(userId),
targetId: new Types.ObjectId(targetId),
targetType,
reactionType,
});
}
async updateReaction(id: string, reactionType: ReactionType): Promise<LikeDocument | null> {
return this.likeModel.findByIdAndUpdate(id, { reactionType }, { new: true }).exec();
}
async findLikedPostIds(userId: string, postIds: string[]): Promise<string[]> {
if (!postIds.length) {
return [];
@@ -46,4 +57,20 @@ export class LikesRepository {
async deleteById(id: string): Promise<void> {
await this.likeModel.findByIdAndDelete(id).exec();
}
async getReactionSummary(targetId: string, targetType: 'post' | 'comment') {
const rows = await this.likeModel
.aggregate<{ _id: ReactionType; count: number }>([
{
$match: {
targetId: new Types.ObjectId(targetId),
targetType,
},
},
{ $group: { _id: '$reactionType', count: { $sum: 1 } } },
])
.exec();
return Object.fromEntries(rows.map((row) => [row._id ?? ReactionType.LIKE, row.count]));
}
}

عرض الملف

@@ -1,5 +1,6 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Types } from 'mongoose';
import { ReactionType } from '../../common/enums/reaction-type.enum';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import { NotificationsService } from '../notifications/notifications.service';
import { CommentsRepository } from '../comments/comments.repository';
@@ -19,21 +20,31 @@ export class LikesService {
private readonly notificationsService: NotificationsService,
) {}
async toggle(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
async toggle(userId: string, dto: ToggleLikeDto) {
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
return existing ? this.unlike(userId, dto) : this.like(userId, dto);
}
async like(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
async like(userId: string, dto: ToggleLikeDto) {
await this.assertTargetExists(dto);
const notificationContext = await this.resolveNotificationContext(dto);
const reactionType = dto.reactionType ?? ReactionType.LIKE;
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
if (existing) {
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
if ((existing.reactionType ?? ReactionType.LIKE) !== reactionType) {
await this.likesRepository.updateReaction(existing.id, reactionType);
}
return {
liked: true,
reacted: true,
targetId: dto.targetId,
targetType: dto.targetType,
reactionType,
};
}
await this.likesRepository.create(userId, dto.targetId, dto.targetType);
await this.likesRepository.create(userId, dto.targetId, dto.targetType, reactionType);
if (dto.targetType === 'post') {
await this.postsRepository.incrementLikesCount(dto.targetId, 1);
}
@@ -58,15 +69,15 @@ export class LikesService {
}
}
return { liked: true, targetId: dto.targetId, targetType: dto.targetType };
return { liked: true, reacted: true, targetId: dto.targetId, targetType: dto.targetType, reactionType };
}
async unlike(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
async unlike(userId: string, dto: ToggleLikeDto) {
await this.assertTargetExists(dto);
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
if (!existing) {
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
}
await this.likesRepository.deleteById(existing.id);
@@ -75,17 +86,27 @@ export class LikesService {
}
await this.feedVersionService.bumpGlobalVersion();
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
}
async getStatus(userId: string, dto: ToggleLikeDto): Promise<{ liked: boolean; targetId: string; targetType: string }> {
async getStatus(userId: string, dto: ToggleLikeDto) {
const targetExists = await this.targetExists(dto);
if (!targetExists) {
return { liked: false, targetId: dto.targetId, targetType: dto.targetType };
return { liked: false, reacted: false, targetId: dto.targetId, targetType: dto.targetType };
}
const existing = await this.likesRepository.findOne(userId, dto.targetId, dto.targetType);
return { liked: !!existing, targetId: dto.targetId, targetType: dto.targetType };
const [existing, reactionSummary] = await Promise.all([
this.likesRepository.findOne(userId, dto.targetId, dto.targetType),
this.likesRepository.getReactionSummary(dto.targetId, dto.targetType),
]);
return {
liked: !!existing,
reacted: !!existing,
targetId: dto.targetId,
targetType: dto.targetType,
reactionType: existing?.reactionType ?? null,
reactionSummary,
};
}
private async assertTargetExists(dto: ToggleLikeDto): Promise<void> {

عرض الملف

@@ -1,5 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
import { ReactionType } from '../../../common/enums/reaction-type.enum';
export type LikeDocument = HydratedDocument<Like>;
@@ -13,6 +14,9 @@ export class Like {
@Prop({ required: true, enum: ['post', 'comment'] })
targetType!: 'post' | 'comment';
@Prop({ type: String, enum: Object.values(ReactionType), default: ReactionType.LIKE, index: true })
reactionType!: ReactionType;
}
export const LikeSchema = SchemaFactory.createForClass(Like);