Add Instagram-style social features and Postman collections
هذا الالتزام موجود في:
@@ -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);
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم