Add chat hiding mentions follow counts waveform and post fixes

هذا الالتزام موجود في:
boutmoun123
2026-06-15 15:05:30 +03:00
الأصل 20fe06b5ed
التزام 3d2a41b22a
28 ملفات معدلة مع 1352 إضافات و75 حذوفات

عرض الملف

@@ -15,9 +15,10 @@ import { ProcessingStatus } from '../../common/enums/processing-status.enum';
import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import {
generateWaveformPeaksFromBuffer,
generateWaveformPeaksFromSeed,
normalizeWaveformPeaks,
buildWaveformPeakSet,
generateWaveformPeakSetFromBuffer,
generateWaveformPeakSetFromSeed,
WaveformPeakSet,
} from '../../common/utils/waveform.util';
import { FeedVersionService } from '../../infrastructure/cache/feed-version.service';
import {
@@ -56,6 +57,17 @@ type NormalizedPostMediaMetadata = {
maqam: string;
rhythmSignature: string;
waveformPeaks: number[];
waveformPeaksPreview: number[];
waveformPeaksDetailed: number[];
};
type MentionTarget = {
id: string;
username: string;
name?: string;
stageName?: string;
avatar?: string;
isVerified?: boolean;
};
type SavedVideoUpload = {
@@ -162,6 +174,7 @@ export class PostsService {
const imageItems = this.buildImageItems(finalImageUrls, dto.imageCaptions, dto.imageAltTexts);
const mentionResolution = await this.resolveMentionTargets(
dto.mentionUsernames,
dto.mentionedUserIds,
finalContent,
userId,
);
@@ -193,6 +206,7 @@ export class PostsService {
taggedUserIds,
collaboratorIds,
mentionUsernames: mentionResolution.mentionUsernames,
mentionedUserIds: mentionResolution.mentionedUserIds,
location,
latitude,
longitude,
@@ -234,7 +248,7 @@ export class PostsService {
mentionResolution.mentionedUsers,
finalContent,
);
return post;
return (await this.postsRepository.findById(post.id)) ?? post;
}
async update(
@@ -371,13 +385,19 @@ export class PostsService {
hasImageUpdate ? [] : (post.imageItems ?? []),
);
const previousMentionUsernames = this.normalizeMentionUsernames(post.mentionUsernames ?? []);
const previousMentionedUserIds = (post.mentionedUserIds ?? []).map(
(id: Types.ObjectId | string | { _id?: unknown; id?: unknown }) => this.extractEntityId(id),
).filter(Boolean);
const shouldRecomputeMentions =
typeof dto.content === 'string' || typeof dto.mentionUsernames !== 'undefined';
typeof dto.content === 'string' ||
typeof dto.mentionUsernames !== 'undefined' ||
typeof dto.mentionedUserIds !== 'undefined';
const mentionResolution = shouldRecomputeMentions
? await this.resolveMentionTargets(dto.mentionUsernames, nextContent, userId)
? await this.resolveMentionTargets(dto.mentionUsernames, dto.mentionedUserIds, nextContent, userId)
: {
mentionUsernames: previousMentionUsernames,
mentionedUsers: [] as Array<{ id: string; username: string }>,
mentionedUserIds: previousMentionedUserIds.map((id) => new Types.ObjectId(id)),
mentionedUsers: [] as MentionTarget[],
};
const {
location: nextLocation,
@@ -401,6 +421,8 @@ export class PostsService {
maqam: post.maqam ?? '',
rhythmSignature: post.rhythmSignature ?? '',
waveformPeaks: post.waveformPeaks ?? [],
waveformPeaksPreview: post.waveformPeaksPreview ?? [],
waveformPeaksDetailed: post.waveformPeaksDetailed ?? [],
},
{
audioSourceBuffer: audioFile?.buffer,
@@ -421,6 +443,7 @@ export class PostsService {
taggedUserIds: nextTaggedUserIds,
collaboratorIds: nextCollaboratorIds,
mentionUsernames: mentionResolution.mentionUsernames,
mentionedUserIds: mentionResolution.mentionedUserIds,
location: nextLocation,
latitude: nextLatitude,
longitude: nextLongitude,
@@ -596,6 +619,18 @@ export class PostsService {
authorId: new Types.ObjectId(userId),
isArchived: { $ne: true },
};
const archivedOnly = query.visibility === 'archived';
if (archivedOnly) {
if (!viewerUserId || viewerUserId !== userId) {
return buildPaginatedResponse([], {
page,
limit,
total: 0,
offset: skip,
});
}
filter.isArchived = true;
}
if (viewerUserId && viewerUserId !== userId) {
const isBlocked = await this.hasBlockBetween(viewerUserId, userId);
if (isBlocked) {
@@ -611,7 +646,7 @@ export class PostsService {
? { $in: [PostVisibility.PUBLIC, PostVisibility.FOLLOWERS] }
: PostVisibility.PUBLIC;
}
if (query.visibility) {
if (query.visibility && !archivedOnly) {
filter.visibility = query.visibility;
}
if (query.postType) {
@@ -1000,6 +1035,8 @@ export class PostsService {
maqam: '',
rhythmSignature: '',
waveformPeaks: [],
waveformPeaksPreview: [],
waveformPeaksDetailed: [],
},
options: {
audioSourceBuffer?: Buffer;
@@ -1024,6 +1061,8 @@ export class PostsService {
maqam: '',
rhythmSignature: '',
waveformPeaks: [],
waveformPeaksPreview: [],
waveformPeaksDetailed: [],
};
}
@@ -1031,13 +1070,29 @@ export class PostsService {
throw new BadRequestException('waveformPeaks is allowed only for audio posts');
}
const durationSeconds =
typeof options.extractedDurationSeconds === 'number'
? options.extractedDurationSeconds
: typeof dto.durationSeconds === 'number'
? dto.durationSeconds
: fallback.durationSeconds;
const waveformSet = supportsWaveform
? this.resolveAudioWaveformPeaks(
Array.isArray(dto.waveformPeaks) ? dto.waveformPeaks : undefined,
options.audioSourceBuffer,
options.waveformSeed,
{
waveformPeaks: fallback.waveformPeaks,
waveformPeaksPreview: fallback.waveformPeaksPreview,
waveformPeaksDetailed: fallback.waveformPeaksDetailed,
},
durationSeconds,
)
: { waveformPeaks: [], waveformPeaksPreview: [], waveformPeaksDetailed: [] };
return {
durationSeconds:
typeof options.extractedDurationSeconds === 'number'
? options.extractedDurationSeconds
: typeof dto.durationSeconds === 'number'
? dto.durationSeconds
: fallback.durationSeconds,
durationSeconds,
thumbnailUrl:
typeof dto.thumbnailUrl === 'string'
? dto.thumbnailUrl.trim()
@@ -1048,14 +1103,7 @@ export class PostsService {
typeof dto.rhythmSignature === 'string'
? dto.rhythmSignature.trim()
: fallback.rhythmSignature,
waveformPeaks: supportsWaveform
? this.resolveAudioWaveformPeaks(
Array.isArray(dto.waveformPeaks) ? dto.waveformPeaks : undefined,
options.audioSourceBuffer,
options.waveformSeed,
fallback.waveformPeaks,
)
: [],
...waveformSet,
};
}
@@ -1090,11 +1138,13 @@ export class PostsService {
private async resolveMentionTargets(
explicitMentionUsernames: string[] | undefined,
explicitMentionedUserIds: string[] | undefined,
content: string,
authorId: string,
): Promise<{
mentionUsernames: string[];
mentionedUsers: Array<{ id: string; username: string }>;
mentionedUserIds: Types.ObjectId[];
mentionedUsers: MentionTarget[];
}> {
const mergedMentionUsernames = Array.from(
new Set([
@@ -1107,29 +1157,78 @@ export class PostsService {
throw new BadRequestException('You can mention up to 30 users only');
}
if (!mergedMentionUsernames.length) {
return { mentionUsernames: [], mentionedUsers: [] };
const mentionedIds = this.normalizeMentionedUserIds(explicitMentionedUserIds, authorId);
if (mergedMentionUsernames.length + mentionedIds.length > 30) {
throw new BadRequestException('You can mention up to 30 users only');
}
const users = await this.usersRepository.findByUsernames(mergedMentionUsernames);
if (!mergedMentionUsernames.length && !mentionedIds.length) {
return { mentionUsernames: [], mentionedUserIds: [], mentionedUsers: [] };
}
const [usersByUsername, usersById] = await Promise.all([
mergedMentionUsernames.length
? this.usersRepository.findByUsernames(mergedMentionUsernames)
: Promise.resolve([]),
mentionedIds.length
? this.usersRepository.findMany(
{ _id: { $in: mentionedIds }, isDisabled: false },
0,
mentionedIds.length,
)
: Promise.resolve([]),
]);
const userByUsername = new Map(
users.map((user) => [
user.username.toLowerCase(),
{ id: user.id, username: user.username.toLowerCase() },
]),
usersByUsername
.filter((user) => !user.isDisabled)
.map((user) => [
user.username.toLowerCase(),
this.toMentionTarget(user),
]),
);
const mentionedUsers = mergedMentionUsernames
.map((username) => userByUsername.get(username))
.filter((user): user is { id: string; username: string } => !!user)
.filter((user) => user.id !== authorId);
const mentionedUsersById = new Map<string, MentionTarget>();
for (const user of [
...mergedMentionUsernames
.map((username) => userByUsername.get(username))
.filter((user): user is MentionTarget => !!user),
...usersById.map((user) => this.toMentionTarget(user)),
]) {
if (user.id !== authorId && user.username) {
mentionedUsersById.set(user.id, user);
}
}
const mentionedUsers = Array.from(mentionedUsersById.values()).slice(0, 30);
return {
mentionUsernames: mentionedUsers.map((user) => user.username),
mentionUsernames: mentionedUsers.map((user) => user.username).filter(Boolean),
mentionedUserIds: mentionedUsers.map((user) => new Types.ObjectId(user.id)),
mentionedUsers,
};
}
private normalizeMentionedUserIds(input: string[] | undefined, authorId: string): string[] {
return Array.from(
new Set(
(input ?? [])
.map((id) => id?.trim())
.filter((id): id is string => !!id && id !== authorId && Types.ObjectId.isValid(id)),
),
);
}
private toMentionTarget(user: any): MentionTarget {
return {
id: user.id ?? user._id?.toString?.() ?? '',
username: user.username?.toLowerCase?.() ?? '',
name: user.name ?? '',
stageName: user.stageName ?? '',
avatar: user.avatar ?? '',
isVerified: user.isVerified ?? false,
};
}
private extractHashtags(content: string): string[] {
const matches = content.match(/#[\p{L}\p{N}_]+/gu) ?? [];
const normalized = matches
@@ -1363,7 +1462,7 @@ export class PostsService {
}
const content = dto.content?.trim() ?? '';
const mentionResolution = await this.resolveMentionTargets(undefined, content, userId);
const mentionResolution = await this.resolveMentionTargets(undefined, undefined, content, userId);
const hashtags = this.extractHashtags(content);
const post = await this.postsRepository.create(userId, {
content,
@@ -1373,6 +1472,7 @@ export class PostsService {
repostOfPostId: content ? null : new Types.ObjectId(sourcePostId),
quoteOfPostId: content ? new Types.ObjectId(sourcePostId) : null,
mentionUsernames: mentionResolution.mentionUsernames,
mentionedUserIds: mentionResolution.mentionedUserIds,
hashtags,
});
@@ -1664,21 +1764,30 @@ export class PostsService {
providedPeaks: number[] | undefined,
sourceBuffer: Buffer | undefined,
waveformSeed: string | undefined,
fallbackPeaks: number[] = [],
): number[] {
fallbackPeaks: Partial<WaveformPeakSet> = {},
durationSeconds?: number | null,
): WaveformPeakSet {
if (Array.isArray(providedPeaks) && providedPeaks.length) {
return normalizeWaveformPeaks(providedPeaks);
return buildWaveformPeakSet(providedPeaks, durationSeconds);
}
if (sourceBuffer?.length) {
return generateWaveformPeaksFromBuffer(sourceBuffer);
return generateWaveformPeakSetFromBuffer(sourceBuffer, durationSeconds);
}
if (fallbackPeaks.length) {
return normalizeWaveformPeaks(fallbackPeaks);
if (fallbackPeaks.waveformPeaksDetailed?.length) {
return buildWaveformPeakSet(fallbackPeaks.waveformPeaksDetailed, durationSeconds);
}
return generateWaveformPeaksFromSeed(waveformSeed ?? 'audio-post');
if (fallbackPeaks.waveformPeaksPreview?.length) {
return buildWaveformPeakSet(fallbackPeaks.waveformPeaksPreview, durationSeconds);
}
if (fallbackPeaks.waveformPeaks?.length) {
return buildWaveformPeakSet(fallbackPeaks.waveformPeaks, durationSeconds);
}
return generateWaveformPeakSetFromSeed(waveformSeed ?? 'audio-post', durationSeconds);
}
private async assertPostOwner(userId: string, postId: string): Promise<PostDocument> {