feat: add ViewCounter enhancements and file upload progress tracking

- Improved `ViewCounter` with visibility-based view increment using `IntersectionObserver` and 50% video progress tracking.
- Added real-time file upload progress updates via Socket.io, including status and percentage feedback.
- Integrated `ViewCounter` dynamically into `ContentCard` and removed redundant instances from static pages.
- Updated backend upload logic to emit progress updates at different stages via the `EventsGateway`.
This commit is contained in:
Mathis HERRIOT
2026-01-29 14:57:44 +01:00
parent 9db3067721
commit 29b1db4aed
7 changed files with 159 additions and 22 deletions

View File

@@ -1,13 +1,14 @@
import { Module } from "@nestjs/common";
import { AuthModule } from "../auth/auth.module";
import { MediaModule } from "../media/media.module";
import { RealtimeModule } from "../realtime/realtime.module";
import { S3Module } from "../s3/s3.module";
import { ContentsController } from "./contents.controller";
import { ContentsService } from "./contents.service";
import { ContentsRepository } from "./repositories/contents.repository";
@Module({
imports: [S3Module, AuthModule, MediaModule],
imports: [S3Module, AuthModule, MediaModule, RealtimeModule],
controllers: [ContentsController],
providers: [ContentsService, ContentsRepository],
exports: [ContentsRepository],

View File

@@ -14,6 +14,7 @@ import type {
} from "../common/interfaces/media.interface";
import type { IStorageService } from "../common/interfaces/storage.interface";
import { MediaService } from "../media/media.service";
import { EventsGateway } from "../realtime/events.gateway";
import { S3Service } from "../s3/s3.service";
import { CreateContentDto } from "./dto/create-content.dto";
import { UploadContentDto } from "./dto/upload-content.dto";
@@ -29,6 +30,7 @@ export class ContentsService {
@Inject(MediaService) private readonly mediaService: IMediaService,
private readonly configService: ConfigService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly eventsGateway: EventsGateway,
) {}
private async clearContentsCache() {
@@ -48,6 +50,11 @@ export class ContentsService {
data: UploadContentDto,
) {
this.logger.log(`Uploading and processing file for user ${userId}`);
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "starting",
progress: 0,
});
// 0. Validation du format et de la taille
const allowedMimeTypes = [
"image/png",
@@ -60,13 +67,25 @@ export class ContentsService {
];
if (!allowedMimeTypes.includes(file.mimetype)) {
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "error",
message: "Format de fichier non supporté",
});
throw new BadRequestException(
"Format de fichier non supporté. Formats acceptés: png, jpeg, jpg, webp, webm, mp4, mov, gif.",
);
}
const isGif = file.mimetype === "image/gif";
const isVideo = file.mimetype.startsWith("video/");
// Autodétermination du type si non fourni ou pour valider
let contentType: "meme" | "gif" | "video" = "meme";
if (file.mimetype === "image/gif") {
contentType = "gif";
} else if (file.mimetype.startsWith("video/")) {
contentType = "video";
}
const isGif = contentType === "gif";
const isVideo = contentType === "video";
let maxSizeKb: number;
if (isGif) {
@@ -78,23 +97,39 @@ export class ContentsService {
}
if (file.size > maxSizeKb * 1024) {
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "error",
message: "Fichier trop volumineux",
});
throw new BadRequestException(
`Fichier trop volumineux. Limite pour ${isGif ? "GIF" : isVideo ? "vidéo" : "image"}: ${maxSizeKb} Ko.`,
);
}
// 1. Scan Antivirus
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "scanning",
progress: 20,
});
const scanResult = await this.mediaService.scanFile(
file.buffer,
file.originalname,
);
if (scanResult.isInfected) {
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "error",
message: "Fichier infecté",
});
throw new BadRequestException(
`Le fichier est infecté par ${scanResult.virusName}`,
);
}
// 2. Transcodage
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "processing",
progress: 40,
});
let processed: MediaProcessingResult;
if (file.mimetype.startsWith("image/") && file.mimetype !== "image/gif") {
// Image -> WebP (format moderne, bien supporté)
@@ -110,17 +145,34 @@ export class ContentsService {
}
// 3. Upload vers S3
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "uploading_s3",
progress: 70,
});
const key = `contents/${userId}/${Date.now()}-${uuidv4()}.${processed.extension}`;
await this.s3Service.uploadFile(key, processed.buffer, processed.mimeType);
this.logger.log(`File uploaded successfully to S3: ${key}`);
// 4. Création en base de données
return await this.create(userId, {
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "saving",
progress: 90,
});
const content = await this.create(userId, {
...data,
type: contentType, // Utiliser le type autodéterminé
storageKey: key,
mimeType: processed.mimeType,
fileSize: processed.size,
});
this.eventsGateway.sendToUser(userId, "upload_progress", {
status: "completed",
progress: 100,
contentId: content.id,
});
return content;
}
async findAll(options: {