Normalize audio waveform peaks for Flutter
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled

هذا الالتزام موجود في:
boutmoun123
2026-06-08 01:43:13 +03:00
الأصل 8a3414db72
التزام 87cd42b706
9 ملفات معدلة مع 2247 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,121 @@
# Artist Dashboard API
This endpoint powers the Flutter artist profile dashboard for the currently authenticated user.
## Endpoint
`GET /api/v1/users/me/dashboard`
## Headers
```http
Authorization: Bearer <accessToken>
```
## Notes
- This endpoint is private to the authenticated user.
- It does not accept a `userId` parameter.
- It does not expose another user's private analytics.
- It keeps the existing `GET /api/v1/users/:id/profile-overview` endpoint unchanged.
- Earnings, audience non-followers, and chart data are not faked. They return safe empty/default values until dedicated analytics storage exists.
## Success Response Example
```json
{
"profile": {
"_id": "665f00000000000000000001",
"coverImage": "",
"avatar": "",
"name": "Artist Name",
"stageName": "Stage Name",
"bio": "Short artist bio",
"isVerified": false
},
"stats": {
"followersCount": 120,
"followingCount": 45,
"postsCount": 12,
"collaborationsCount": 2,
"viewCount": 3000,
"playCount": 980,
"listenCount": 980,
"likesCount": 220,
"commentsCount": 40,
"savesCount": 18,
"sharesCount": 11,
"engagementRate": 7.26,
"scorePercentage": 0,
"earnings": 0
},
"audience": {
"followers": 120,
"nonFollowers": 0,
"newFollowersThisWeek": 4,
"newFollowersThisMonth": 19
},
"engagementChart": [],
"topContent": [
{
"_id": "665f00000000000000000002",
"postType": "audio",
"thumbnailUrl": "",
"displayUrl": "",
"content": "New track",
"viewCount": 1000,
"playCount": 600,
"likesCount": 90,
"commentsCount": 10,
"savesCount": 8,
"engagementScore": 113,
"createdAt": "2026-06-08T00:00:00.000Z"
}
],
"recentActivity": [],
"meta": {
"generatedAt": "2026-06-08T00:00:00.000Z",
"chartAvailable": false,
"earningsAvailable": false,
"audienceAnalyticsAvailable": false
}
}
```
## Flutter Model Suggestion
- `ArtistDashboard`
- `ArtistDashboardProfile`
- `ArtistDashboardStats`
- `ArtistDashboardAudience`
- `ArtistDashboardTopContent`
- `ArtistDashboardRecentActivity`
- `ArtistDashboardMeta`
## UI Sections
1. Profile header: use `profile.coverImage`, `profile.avatar`, `profile.name`, `profile.stageName`, `profile.bio`, `profile.isVerified`.
2. Stats cards: use `stats.followersCount`, `stats.followingCount`, `stats.viewCount`, `stats.listenCount`, `stats.engagementRate`.
3. Engagement chart: render only when `meta.chartAvailable === true`; otherwise show the empty state.
4. Top content list: use `topContent`.
5. Audience summary: use `audience.followers`, `audience.nonFollowers`, `audience.newFollowersThisWeek`, `audience.newFollowersThisMonth`.
6. Recent activity: use `recentActivity`.
7. Empty states: show clear UI for no posts, no analytics, chart unavailable, and earnings unavailable.
## Empty States
- No posts yet: `stats.postsCount === 0`.
- No analytics yet: `stats.viewCount === 0 && stats.playCount === 0`.
- Chart not available: `meta.chartAvailable === false`.
- Earnings not available: `meta.earningsAvailable === false`.
## Future Analytics Upgrade
Daily chart data requires event-level or snapshot storage. Recommended future collections:
- `post_view_events`
- `post_play_events`
- `post_engagement_events`
- `daily_artist_analytics`
Audience split requires viewer identity tracking per view/play event. Earnings requires a monetization/revenue module. Until those exist, the backend intentionally returns safe defaults instead of fake values.

عرض الملف

@@ -5599,6 +5599,382 @@
} }
] ]
}, },
{
"name": "Realtime Socket.IO",
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
"item": [
{
"name": "Connection - Chat Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Connection - Notifications Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Emit join_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit leave_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit send_message",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit typing",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_delivered",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_seen",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Listen joined_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen left_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen new_message",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
}
},
{
"name": "Listen message_delivered",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen message_seen",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen presence",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
}
},
{
"name": "Listen socket_error",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
}
},
{
"name": "Listen notification:new",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen notification:unread_counts",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
}
},
{
"name": "Legacy Notification Events",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
}
}
]
},
{ {
"name": "Feed", "name": "Feed",
"item": [ "item": [
@@ -9782,6 +10158,22 @@
{ {
"key": "notificationCategory", "key": "notificationCategory",
"value": "interactions" "value": "interactions"
},
{
"key": "socketBaseUrl",
"value": "http://localhost:4000"
},
{
"key": "chatSocketUrl",
"value": "{{socketBaseUrl}}/chat"
},
{
"key": "notificationsSocketUrl",
"value": "{{socketBaseUrl}}/notifications"
},
{
"key": "socketAccessToken",
"value": "{{accessToken}}"
} }
] ]
} }

عرض الملف

@@ -4684,6 +4684,382 @@
} }
] ]
}, },
{
"name": "Realtime Socket.IO",
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
"item": [
{
"name": "Connection - Chat Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Connection - Notifications Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Emit join_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit leave_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit send_message",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit typing",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_delivered",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_seen",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Listen joined_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen left_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen new_message",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
}
},
{
"name": "Listen message_delivered",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen message_seen",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen presence",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
}
},
{
"name": "Listen socket_error",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
}
},
{
"name": "Listen notification:new",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen notification:unread_counts",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
}
},
{
"name": "Legacy Notification Events",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
}
}
]
},
{ {
"name": "Support Tickets", "name": "Support Tickets",
"item": [ "item": [
@@ -5003,6 +5379,382 @@
] ]
} }
] ]
},
{
"name": "Realtime Socket.IO",
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
"item": [
{
"name": "Connection - Chat Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Connection - Notifications Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Emit join_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit leave_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit send_message",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit typing",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_delivered",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_seen",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Listen joined_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen left_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen new_message",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
}
},
{
"name": "Listen message_delivered",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen message_seen",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen presence",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
}
},
{
"name": "Listen socket_error",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
}
},
{
"name": "Listen notification:new",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen notification:unread_counts",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
}
},
{
"name": "Legacy Notification Events",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
}
}
]
} }
], ],
"variable": [ "variable": [
@@ -5576,6 +6328,22 @@
{ {
"key": "notificationCategory", "key": "notificationCategory",
"value": "interactions" "value": "interactions"
},
{
"key": "socketBaseUrl",
"value": "http://localhost:4000"
},
{
"key": "chatSocketUrl",
"value": "{{socketBaseUrl}}/chat"
},
{
"key": "notificationsSocketUrl",
"value": "{{socketBaseUrl}}/notifications"
},
{
"key": "socketAccessToken",
"value": "{{accessToken}}"
} }
] ]
} }

عرض الملف

@@ -5025,6 +5025,382 @@
} }
] ]
}, },
{
"name": "Realtime Socket.IO",
"description": "Socket.IO realtime contract for Flutter. These examples document emits/listeners; they are not REST endpoints.",
"item": [
{
"name": "Connection - Chat Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /chat\nLocal URL: {{socketBaseUrl}}/chat\nProduction URL: https://YOUR_DOMAIN/chat\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Connection - Notifications Namespace",
"request": {
"method": "GET",
"header": [
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket type: Socket.IO\nNamespace: /notifications\nLocal URL: {{socketBaseUrl}}/notifications\nProduction URL: https://YOUR_DOMAIN/notifications\nFlutter auth: auth: { token: accessToken }\nOn connect, backend joins user room user:{userId}."
}
},
{
"name": "Emit join_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: join_conversation\nDirection: Client -> Backend\nServer confirms with joined_conversation.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit leave_conversation",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: leave_conversation\nDirection: Client -> Backend\nServer confirms with left_conversation. Socket remains connected.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit send_message",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: send_message\nDirection: Client -> Backend\nServer emits new_message to conversation:{conversationId}. REST send also emits new_message once.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit typing",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: typing\nDirection: Client -> Backend\nBackend validates membership and broadcasts typing to the conversation room excluding sender.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"isTyping\": true\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_delivered",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_delivered\nDirection: Client -> Backend\nBackend stores deliveredBy once per user and emits message_delivered.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Emit mark_seen",
"request": {
"method": "POST",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: mark_seen\nDirection: Client -> Backend\nBackend stores seenBy, clears unread count, and emits message_seen.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}\n```",
"body": {
"mode": "raw",
"raw": "{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "Listen joined_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: joined_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen left_conversation",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: left_conversation\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen new_message",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: new_message\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{messageId}}\",\n \"conversationId\": \"{{conversationId}}\",\n \"senderId\": \"{{userId}}\",\n \"content\": \"Hello\",\n \"messageType\": \"text\"\n}\n```"
}
},
{
"name": "Listen message_delivered",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_delivered\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"deliveredAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen message_seen",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: message_seen\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"conversationId\": \"{{conversationId}}\",\n \"messageId\": \"{{messageId}}\",\n \"userId\": \"{{targetUserId}}\",\n \"seenAt\": \"2026-06-07T10:00:00.000Z\"\n}\n```"
}
},
{
"name": "Listen presence",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: presence\nDirection: Backend -> Client\nonline is kept for backward compatibility; prefer isOnline.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"userId\": \"{{targetUserId}}\",\n \"isOnline\": true,\n \"online\": true,\n \"lastSeenAt\": null\n}\n```"
}
},
{
"name": "Listen socket_error",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{chatSocketUrl}}",
"description": "Socket.IO event: socket_error\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"event\": \"join_conversation\",\n \"message\": \"Not allowed\",\n \"code\": \"FORBIDDEN\"\n}\n```"
}
},
{
"name": "Listen notification:new",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:new\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"_id\": \"{{notificationId}}\",\n \"type\": \"message\",\n \"resourceType\": \"conversation\",\n \"deepLink\": \"/chat/conversations/{{conversationId}}\"\n}\n```"
}
},
{
"name": "Listen notification:unread_counts",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification:unread_counts\nDirection: Backend -> Client\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"total\": 10,\n \"interactions\": 4,\n \"messages\": 3,\n \"follows\": 1,\n \"followRequests\": 2,\n \"collaboration\": 1,\n \"system\": 0\n}\n```"
}
},
{
"name": "Legacy Notification Events",
"request": {
"method": "GET",
"header": [
{
"key": "Socket.IO",
"value": "Use socket_io_client / Socket.IO, not HTTP REST"
},
{
"key": "Auth",
"value": "auth: { token: '{{socketAccessToken}}' }"
}
],
"url": "{{notificationsSocketUrl}}",
"description": "Socket.IO event: notification_created / notifications_unread_count\nDirection: Backend -> Client\nKept for backward compatibility. New Flutter code should prefer notification:new and notification:unread_counts.\nConnection auth:\nauth: { token: '{{socketAccessToken}}' }\nPayload example:\n```json\n{\n \"notification_created\": {\n \"_id\": \"{{notificationId}}\"\n },\n \"notifications_unread_count\": {\n \"unreadCount\": 10\n }\n}\n```"
}
}
]
},
{ {
"name": "Feed", "name": "Feed",
"item": [ "item": [
@@ -6998,6 +7374,22 @@
{ {
"key": "notificationCategory", "key": "notificationCategory",
"value": "interactions" "value": "interactions"
},
{
"key": "socketBaseUrl",
"value": "http://localhost:4000"
},
{
"key": "chatSocketUrl",
"value": "{{socketBaseUrl}}/chat"
},
{
"key": "notificationsSocketUrl",
"value": "{{socketBaseUrl}}/notifications"
},
{
"key": "socketAccessToken",
"value": "{{accessToken}}"
} }
] ]
} }

عرض الملف

@@ -0,0 +1,76 @@
export type ArtistDashboardProfile = {
_id: string;
coverImage: string;
avatar: string;
name: string;
stageName: string;
bio: string;
isVerified: boolean;
};
export type ArtistDashboardStats = {
followersCount: number;
followingCount: number;
postsCount: number;
collaborationsCount: number;
viewCount: number;
playCount: number;
listenCount: number;
likesCount: number;
commentsCount: number;
savesCount: number;
sharesCount: number;
engagementRate: number;
scorePercentage: number;
earnings: number;
};
export type ArtistDashboardAudience = {
followers: number;
nonFollowers: number;
newFollowersThisWeek: number;
newFollowersThisMonth: number;
};
export type ArtistDashboardTopContentItem = {
_id: string;
postType: string;
thumbnailUrl: string;
displayUrl: string;
content: string;
viewCount: number;
playCount: number;
likesCount: number;
commentsCount: number;
savesCount: number;
engagementScore: number;
createdAt: string | Date;
};
export type ArtistDashboardRecentActivityItem = {
_id: string;
type: string;
title: string;
previewText: string;
resourceType: string;
referenceId: string | null;
deepLink: string;
createdAt: string | Date;
};
export type ArtistDashboardMeta = {
generatedAt: string;
chartAvailable: boolean;
earningsAvailable: boolean;
audienceAnalyticsAvailable: boolean;
};
export type ArtistDashboardResponse = {
profile: ArtistDashboardProfile;
stats: ArtistDashboardStats;
audience: ArtistDashboardAudience;
engagementChart: unknown[];
topContent: ArtistDashboardTopContentItem[];
recentActivity: ArtistDashboardRecentActivityItem[];
meta: ArtistDashboardMeta;
};

عرض الملف

@@ -0,0 +1,15 @@
import { UsersController } from './users.controller';
describe('UsersController artist dashboard', () => {
it('uses the authenticated user id for the private dashboard endpoint', async () => {
const usersService = {
getMyDashboard: jest.fn().mockResolvedValue({ profile: { _id: 'user-1' } }),
};
const controller = new UsersController(usersService as any);
const result = await controller.getMyDashboard({ sub: 'user-1' } as any);
expect(usersService.getMyDashboard).toHaveBeenCalledWith('user-1');
expect(result).toEqual({ profile: { _id: 'user-1' } });
});
});

عرض الملف

@@ -281,6 +281,13 @@ export class UsersController {
return this.usersService.discoverTalents(query); return this.usersService.discoverTalents(query);
} }
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get('me/dashboard')
async getMyDashboard(@CurrentUser() user: JwtPayload) {
return this.usersService.getMyDashboard(user.sub);
}
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Get(':id/profile-overview') @Get(':id/profile-overview')

عرض الملف

@@ -0,0 +1,185 @@
import { BadRequestException } from '@nestjs/common';
import { Types } from 'mongoose';
import { UsersService } from './users.service';
const chain = <T>(value: T) => ({
sort: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
project: jest.fn().mockReturnThis(),
toArray: jest.fn().mockResolvedValue(value),
});
const createService = (options: {
user?: Record<string, any> | null;
postStats?: Record<string, number>;
topContent?: Record<string, any>[];
collaborationsCount?: number;
newFollowersThisWeek?: number;
newFollowersThisMonth?: number;
recentActivity?: Record<string, any>[];
} = {}) => {
const userId = new Types.ObjectId().toString();
const user = options.user ?? {
_id: new Types.ObjectId(userId),
name: 'Artist',
stageName: 'Stage',
bio: 'Bio',
avatar: '',
coverImage: '',
followersCount: 10,
followingCount: 4,
isVerified: true,
isDisabled: false,
toObject() {
return {
_id: userId,
name: this.name,
stageName: this.stageName,
bio: this.bio,
avatar: this.avatar,
coverImage: this.coverImage,
followersCount: this.followersCount,
followingCount: this.followingCount,
isVerified: this.isVerified,
};
},
};
const postsAggregate = jest
.fn()
.mockReturnValueOnce({
toArray: jest.fn().mockResolvedValue(options.postStats ? [options.postStats] : []),
})
.mockReturnValueOnce({
toArray: jest.fn().mockResolvedValue(options.topContent ?? []),
});
const postsCollection = {
aggregate: postsAggregate,
countDocuments: jest.fn().mockResolvedValue(options.collaborationsCount ?? 0),
};
const followsCollection = {
countDocuments: jest
.fn()
.mockResolvedValueOnce(options.newFollowersThisWeek ?? 0)
.mockResolvedValueOnce(options.newFollowersThisMonth ?? 0),
};
const notificationsCollection = {
find: jest.fn().mockReturnValue(chain(options.recentActivity ?? [])),
};
const connection = {
collection: jest.fn((name: string) => {
if (name === 'posts') {
return postsCollection;
}
if (name === 'follows') {
return followsCollection;
}
if (name === 'notifications') {
return notificationsCollection;
}
throw new Error(`Unexpected collection ${name}`);
}),
};
const usersRepository = {
findById: jest.fn().mockResolvedValue(user),
};
const service = new UsersService(
connection as any,
usersRepository as any,
{ logSuperAdminAction: jest.fn() } as any,
{ get: jest.fn() } as any,
{ saveFile: jest.fn(), deleteFile: jest.fn() } as any,
);
return { service, userId, usersRepository, postsCollection, followsCollection };
};
describe('UsersService artist dashboard', () => {
it('returns zeros and safe empty arrays for an authenticated user with no posts', async () => {
const { service, userId } = createService();
const result = await service.getMyDashboard(userId);
expect(result.profile._id).toBe(userId);
expect(result.stats.postsCount).toBe(0);
expect(result.stats.viewCount).toBe(0);
expect(result.stats.playCount).toBe(0);
expect(result.stats.listenCount).toBe(0);
expect(result.stats.engagementRate).toBe(0);
expect(result.stats.earnings).toBe(0);
expect(result.stats.scorePercentage).toBe(0);
expect(result.audience.followers).toBe(10);
expect(result.audience.nonFollowers).toBe(0);
expect(result.engagementChart).toEqual([]);
expect(result.topContent).toEqual([]);
expect(result.meta.chartAvailable).toBe(false);
expect(result.meta.earningsAvailable).toBe(false);
});
it('aggregates owned posts and calculates engagement rate safely', async () => {
const postId = new Types.ObjectId();
const { service, userId } = createService({
postStats: {
postsCount: 2,
viewCount: 100,
playCount: 50,
likesCount: 12,
commentsCount: 3,
savesCount: 2,
sharesCount: 1,
},
topContent: [
{
_id: postId,
postType: 'audio',
content: 'Track',
thumbnailUrl: '',
audioUrl: '/uploads/posts/audios/audio.mp3',
viewCount: 100,
playCount: 50,
likesCount: 12,
commentsCount: 3,
savesCount: 2,
engagementScore: 18,
createdAt: new Date('2026-06-08T00:00:00.000Z'),
},
],
collaborationsCount: 2,
newFollowersThisWeek: 1,
newFollowersThisMonth: 3,
});
const result = await service.getMyDashboard(userId);
expect(result.stats.postsCount).toBe(2);
expect(result.stats.viewCount).toBe(100);
expect(result.stats.playCount).toBe(50);
expect(result.stats.listenCount).toBe(50);
expect(result.stats.likesCount).toBe(12);
expect(result.stats.commentsCount).toBe(3);
expect(result.stats.savesCount).toBe(2);
expect(result.stats.sharesCount).toBe(1);
expect(result.stats.collaborationsCount).toBe(2);
expect(result.stats.engagementRate).toBe(12);
expect(result.audience.newFollowersThisWeek).toBe(1);
expect(result.audience.newFollowersThisMonth).toBe(3);
expect(result.topContent).toHaveLength(1);
expect(result.topContent[0]).toEqual(
expect.objectContaining({
_id: postId.toString(),
postType: 'audio',
content: 'Track',
engagementScore: 18,
}),
);
});
it('rejects invalid current user ids', async () => {
const { service } = createService();
await expect(service.getMyDashboard('not-object-id')).rejects.toBeInstanceOf(
BadRequestException,
);
});
});

عرض الملف

@@ -13,12 +13,18 @@ import { ExperienceLevel } from '../../common/enums/experience-level.enum';
import { MusicRole } from '../../common/enums/music-role.enum'; import { MusicRole } from '../../common/enums/music-role.enum';
import { hashValue } from '../../common/utils/hash.util'; import { hashValue } from '../../common/utils/hash.util';
import { buildPaginatedResponse } from '../../common/utils/pagination.util'; import { buildPaginatedResponse } from '../../common/utils/pagination.util';
import { resolveManagedFileUrl } from '../../common/utils/public-url.util';
import { resolveMongoSortDirection } from '../../common/utils/sort.util'; import { resolveMongoSortDirection } from '../../common/utils/sort.util';
import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service'; import { ManagedStorageService } from '../../infrastructure/storage/managed-storage.service';
import { AuditService } from '../audit/audit.service'; import { AuditService } from '../audit/audit.service';
import { UserRole } from '../../common/enums/user-role.enum'; import { UserRole } from '../../common/enums/user-role.enum';
import { CreateUserDto } from './dto/create-user.dto'; import { CreateUserDto } from './dto/create-user.dto';
import { AdminDisableUserDto } from './dto/admin-disable-user.dto'; import { AdminDisableUserDto } from './dto/admin-disable-user.dto';
import {
ArtistDashboardRecentActivityItem,
ArtistDashboardResponse,
ArtistDashboardTopContentItem,
} from './dto/artist-dashboard-response.dto';
import { CreateAdminDto } from './dto/create-admin.dto'; import { CreateAdminDto } from './dto/create-admin.dto';
import { MusicSetupDto } from './dto/music-setup.dto'; import { MusicSetupDto } from './dto/music-setup.dto';
import { ProfileSetupDto } from './dto/profile-setup.dto'; import { ProfileSetupDto } from './dto/profile-setup.dto';
@@ -36,6 +42,16 @@ type UploadedImageFile = {
originalname?: string; originalname?: string;
}; };
type UserPostStats = {
postsCount: number;
viewCount: number;
playCount: number;
likesCount: number;
commentsCount: number;
savesCount: number;
sharesCount: number;
};
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
@@ -790,6 +806,94 @@ export class UsersService {
}; };
} }
async getMyDashboard(currentUserId: string): Promise<ArtistDashboardResponse> {
if (!Types.ObjectId.isValid(currentUserId)) {
throw new BadRequestException('Invalid user id');
}
const user = await this.findByIdOrFail(currentUserId);
if (user.isDisabled) {
throw new NotFoundException('User not found');
}
const objectUserId = new Types.ObjectId(currentUserId);
const now = new Date();
const weekAgo = new Date(now);
weekAgo.setDate(weekAgo.getDate() - 7);
const monthAgo = new Date(now);
monthAgo.setDate(monthAgo.getDate() - 30);
const [postStats, collaborationsCount, topContent, recentActivity, newFollowersThisWeek, newFollowersThisMonth] =
await Promise.all([
this.aggregateUserPostStats(objectUserId),
this.countUserCollaborations(objectUserId),
this.findTopDashboardContent(objectUserId, 5),
this.findArtistRecentActivity(objectUserId, 10),
this.connection.collection('follows').countDocuments({
followingId: objectUserId,
createdAt: { $gte: weekAgo },
}),
this.connection.collection('follows').countDocuments({
followingId: objectUserId,
createdAt: { $gte: monthAgo },
}),
]);
const followersCount = this.toSafeNumber(user.followersCount);
const followingCount = this.toSafeNumber(user.followingCount);
const totalEngagement =
postStats.likesCount + postStats.commentsCount + postStats.savesCount + postStats.sharesCount;
const totalReach = postStats.viewCount + postStats.playCount;
const engagementRate = this.roundPercent((totalEngagement / Math.max(totalReach, 1)) * 100);
const userObject = typeof user.toObject === 'function' ? (user.toObject() as Record<string, any>) : (user as any);
return {
profile: {
_id: currentUserId,
coverImage: userObject.coverImage ?? '',
avatar: userObject.avatar ?? '',
name: userObject.name ?? '',
stageName: userObject.stageName ?? '',
bio: userObject.bio ?? '',
isVerified: userObject.isVerified ?? false,
},
stats: {
followersCount,
followingCount,
postsCount: postStats.postsCount,
collaborationsCount,
viewCount: postStats.viewCount,
playCount: postStats.playCount,
listenCount: postStats.playCount,
likesCount: postStats.likesCount,
commentsCount: postStats.commentsCount,
savesCount: postStats.savesCount,
sharesCount: postStats.sharesCount,
engagementRate,
// TODO: Replace with real artist score logic when product scoring rules exist.
scorePercentage: 0,
// TODO: Replace with real monetization calculation when earnings module exists.
earnings: 0,
},
audience: {
followers: followersCount,
// TODO: Requires viewer-level analytics/events to calculate non-followers.
nonFollowers: 0,
newFollowersThisWeek,
newFollowersThisMonth,
},
engagementChart: [],
topContent,
recentActivity,
meta: {
generatedAt: now.toISOString(),
chartAvailable: false,
earningsAvailable: false,
audienceAnalyticsAvailable: false,
},
};
}
async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{ async searchUsersForSuperAdmin(query: UserQueryDto): Promise<{
items: UserDocument[]; items: UserDocument[];
page: number; page: number;
@@ -819,6 +923,193 @@ export class UsersService {
}); });
} }
private async aggregateUserPostStats(authorId: Types.ObjectId): Promise<UserPostStats> {
const [row] = await this.connection
.collection('posts')
.aggregate<UserPostStats>([
{
$match: {
authorId,
isDeleted: { $ne: true },
isArchived: { $ne: true },
moderationStatus: { $ne: ModerationStatus.HIDDEN },
},
},
{
$group: {
_id: null,
postsCount: { $sum: 1 },
viewCount: { $sum: { $ifNull: ['$viewCount', 0] } },
playCount: { $sum: { $ifNull: ['$playCount', 0] } },
likesCount: { $sum: { $ifNull: ['$likesCount', 0] } },
commentsCount: { $sum: { $ifNull: ['$commentsCount', 0] } },
savesCount: { $sum: { $ifNull: ['$savesCount', 0] } },
sharesCount: { $sum: { $ifNull: ['$shareCount', 0] } },
},
},
{ $project: { _id: 0 } },
])
.toArray();
return {
postsCount: this.toSafeNumber(row?.postsCount),
viewCount: this.toSafeNumber(row?.viewCount),
playCount: this.toSafeNumber(row?.playCount),
likesCount: this.toSafeNumber(row?.likesCount),
commentsCount: this.toSafeNumber(row?.commentsCount),
savesCount: this.toSafeNumber(row?.savesCount),
sharesCount: this.toSafeNumber(row?.sharesCount),
};
}
private async countUserCollaborations(userId: Types.ObjectId): Promise<number> {
return this.connection.collection('posts').countDocuments({
isDeleted: { $ne: true },
isArchived: { $ne: true },
moderationStatus: { $ne: ModerationStatus.HIDDEN },
$or: [
{ authorId: userId, 'taggedUserIds.0': { $exists: true } },
{ authorId: { $ne: userId }, taggedUserIds: userId },
{ collaboratorIds: userId },
],
});
}
private async findTopDashboardContent(
authorId: Types.ObjectId,
limit: number,
): Promise<ArtistDashboardTopContentItem[]> {
const rows = await this.connection
.collection('posts')
.aggregate<Record<string, any>>([
{
$match: {
authorId,
isDeleted: { $ne: true },
isArchived: { $ne: true },
moderationStatus: { $ne: ModerationStatus.HIDDEN },
},
},
{
$addFields: {
engagementScore: {
$add: [
{ $ifNull: ['$likesCount', 0] },
{ $ifNull: ['$commentsCount', 0] },
{ $ifNull: ['$savesCount', 0] },
{ $ifNull: ['$shareCount', 0] },
],
},
},
},
{ $sort: { engagementScore: -1, viewCount: -1, playCount: -1, createdAt: -1 } },
{ $limit: Math.max(1, limit) },
{
$project: {
postType: 1,
thumbnailUrl: 1,
imageUrls: 1,
imageItems: 1,
videoUrl: 1,
audioUrl: 1,
content: 1,
viewCount: 1,
playCount: 1,
likesCount: 1,
commentsCount: 1,
savesCount: 1,
engagementScore: 1,
createdAt: 1,
},
},
])
.toArray();
return rows.map((post) => {
const thumbnailUrl = this.resolveManagedStringUrl(post.thumbnailUrl);
const fallbackDisplayUrl = this.resolveTopContentDisplayUrl(post);
return {
_id: post._id?.toString?.() ?? '',
postType: post.postType ?? '',
thumbnailUrl,
displayUrl: thumbnailUrl || fallbackDisplayUrl,
content: post.content ?? '',
viewCount: this.toSafeNumber(post.viewCount),
playCount: this.toSafeNumber(post.playCount),
likesCount: this.toSafeNumber(post.likesCount),
commentsCount: this.toSafeNumber(post.commentsCount),
savesCount: this.toSafeNumber(post.savesCount),
engagementScore: this.toSafeNumber(post.engagementScore),
createdAt: post.createdAt ?? '',
};
});
}
private async findArtistRecentActivity(
userId: Types.ObjectId,
limit: number,
): Promise<ArtistDashboardRecentActivityItem[]> {
const rows = await this.connection
.collection('notifications')
.find({
$or: [{ recipientId: userId }, { actorId: userId }],
})
.sort({ createdAt: -1 })
.limit(Math.max(1, limit))
.project({
type: 1,
title: 1,
previewText: 1,
resourceType: 1,
referenceId: 1,
deepLink: 1,
createdAt: 1,
})
.toArray();
return rows.map((activity) => ({
_id: activity._id?.toString?.() ?? '',
type: activity.type ?? '',
title: activity.title ?? '',
previewText: activity.previewText ?? '',
resourceType: activity.resourceType ?? '',
referenceId: activity.referenceId?.toString?.() ?? null,
deepLink: activity.deepLink ?? '',
createdAt: activity.createdAt ?? '',
}));
}
private resolveTopContentDisplayUrl(post: Record<string, any>): string {
const imageItems = Array.isArray(post.imageItems) ? post.imageItems : [];
const imageUrls = Array.isArray(post.imageUrls) ? post.imageUrls : [];
const candidate =
imageItems.find((item) => typeof item?.url === 'string' && item.url)?.url ??
imageUrls.find((url) => typeof url === 'string' && url) ??
post.videoUrl ??
post.audioUrl ??
'';
return this.resolveManagedStringUrl(candidate);
}
private resolveManagedStringUrl(value: unknown): string {
const resolved = resolveManagedFileUrl(typeof value === 'string' ? value : '');
return typeof resolved === 'string' ? resolved : '';
}
private toSafeNumber(value: unknown): number {
const numeric = Number(value ?? 0);
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
}
private roundPercent(value: number): number {
if (!Number.isFinite(value)) {
return 0;
}
return Math.round(value * 100) / 100;
}
private async assertTargetIsAdmin(userId: string): Promise<UserDocument> { private async assertTargetIsAdmin(userId: string): Promise<UserDocument> {
const user = await this.findByIdOrFail(userId); const user = await this.findByIdOrFail(userId);
if (user.role !== UserRole.ADMIN) { if (user.role !== UserRole.ADMIN) {