Add chat hiding mentions follow counts waveform and post fixes
هذا الالتزام موجود في:
@@ -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> {
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم