feat(social): show friend presence and require online listening
ci / api / api (push) Successful in 1m11s
ci / db / db (push) Failing after 23s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m29s
ci / ts / ts (packages + web) (push) Successful in 3m15s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m27s

This commit is contained in:
mrleo1nid committed 2026-10-23 12:52:00 +03:00
1 parent 582c0dc3d4
commit cd84ceecb2
26 files changed
+574 -400

No files matched your search

+5 -1
View File
@@ -90,6 +90,7 @@ export function fakeApi(initial: FakeUser[]) {
const listeningMembers = new Map<string, string>();
const listeningRequests = new Map<string, components['schemas']['ListeningRequest'] & { guest_id: string }>();
const typing = new Map<string, number>();
const presence = new Map<string, number>();
const defaultListening = { requests: 'nobody', auto_accept: 'nobody', autoplay: true } as const;
const pair = (a: string, b: string) => [a, b].sort().join(':');
const isBlocked = (a: string, b: string) => blocks.has(`${a}:${b}`) || blocks.has(`${b}:${a}`);
@@ -100,6 +101,7 @@ export function fakeApi(initial: FakeUser[]) {
return { id: p.id, username: p.username, display_name: p.display_name ?? p.username, ...(visible && activity.has(p.id) && !isBlocked(me,p.id) ? { activity: activity.get(p.id) } : {}),
...(last ? { last_message: { id: last.id, sender_id: last.sender_id, text: last.deleted ? '' : last.text.slice(0, 160), attachment_kind: last.attachment_kind, created_at: last.created_at, deleted: last.deleted ?? false } } : {}),
relationship: blocks.has(`${me}:${p.id}`) ? 'blocked' : f?.accepted ? 'friend' : f ? f.sender === me ? 'outgoing' : 'incoming' : 'none',
online: !!f?.accepted && !isBlocked(me, p.id) && (presence.get(p.id) ?? 0) > Date.now() - 45_000,
unread: messages.filter(m => m.recipient === me && m.sender_id === p.id && !m.read).length };
};
const notify = (recipient: string, actor: FakeUser, kind: components['schemas']['SocialNotification']['kind']) => notifications.unshift({
@@ -244,6 +246,7 @@ export function fakeApi(initial: FakeUser[]) {
}
if (path === '/api/v1/me/social/listening/preferences' && body) { listeningPreferences.set(user.id,body as components['schemas']['ListeningPreferences']);return new Response(null,{status:204}); }
if (path === '/api/v1/me/social/listening') {
presence.set(user.id, Date.now());
const room=listeningRooms.get(listeningMembers.get(user.id) ?? '');
if(room)room.participants=[...listeningMembers].filter(([,id])=>id===room.id).map(([id])=>({id,name:users.get(id)?.display_name??users.get(id)?.username??id}));
return json({preferences:listeningPreferences.get(user.id)??defaultListening,room:room??null,requests:[...listeningRequests.values()].filter(r=>r.recipient_id===user.id||r.sender_id===user.id),server_time:new Date().toISOString()});
@@ -258,6 +261,7 @@ export function fakeApi(initial: FakeUser[]) {
const host=op==='join'?target:user.id; const guest=op==='join'?user.id:target; const prefs=listeningPreferences.get(target)??defaultListening;
const friend=!!friendships.get(pair(user.id,target))?.accepted;
if(isBlocked(user.id,target)||!(prefs.requests==='everyone'||prefs.requests==='friends'&&friend))return problem(403,'social.unavailable');
if ((presence.get(target) ?? 0) <= Date.now() - 45_000) return problem(409, 'social.person_offline');
let room=[...listeningRooms.values()].find(r=>r.owner_id===host);
const source=activity.get(host);if(!room){room={id:crypto.randomUUID(),owner_id:host,owner_name:users.get(host)?.username??host,device_id:String(op==='join'?source?.device_id:body.device_id),track_id:String(op==='join'?source?.track_id:body.track_id),position_ms:Number(op==='join'?source?.position_ms:body.position_ms??0),playing:true,updated_at:new Date().toISOString(),participants:[]};listeningRooms.set(room.id,room);listeningMembers.set(host,room.id);}
if(prefs.auto_accept==='everyone'||prefs.auto_accept==='friends'&&friend)listeningMembers.set(guest,room.id);
@@ -510,5 +514,5 @@ export function fakeApi(initial: FakeUser[]) {
return problem(404, 'common.not_found');
};
return { fetch: fetchImpl as typeof fetch, calls, users, playlists, friendships, notifications, messages, activityVisibility, activity, friendRecommendations, listeningPreferences, listeningRooms, listeningMembers, listeningRequests, signOut: () => (current = null) };
return { presence, fetch: fetchImpl as typeof fetch, calls, users, playlists, friendships, notifications, messages, activityVisibility, activity, friendRecommendations, listeningPreferences, listeningRooms, listeningMembers, listeningRequests, signOut: () => (current = null) };
}