feat(social): add friends, private chats, inbox and music sharing
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m8s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m23s
ci / db / db (push) Successful in 2m29s
ci / ts / ts (packages + web) (push) Successful in 2m55s

This commit is contained in:
mrleo1nid committed 2026-10-23 03:19:29 +03:00
1 parent f1528de948
commit 0a05bfe2cd
43 files changed
+2828 -389

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ export {
} from './player';
export { normalizeGain, TARGET_LUFS } from './gain';
export { playlistShareUrl, trackShareUrl } from './share';
export { socialApi, attachmentPath, type SocialPerson, type SocialInbox, type SocialAction, type DirectMessage, type SendDirectMessage, type MusicAttachment } from './social';
export { checkServer, normalizeServerUrl, type ServerCheck } from './server';
export { initialWaveState, mix, rgb, rgba, step, WAVE_COLORS, wavePalette, waveScale, type Rgb, type WaveState } from './wave-anim';
+11
View File
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import { attachmentPath } from './social';
describe('music attachments', () => {
it('creates only internal catalog links and escapes identifiers', () => {
expect(attachmentPath({ attachment_kind: 'track', attachment_id: 'a/b?sig=secret' })).toBe('/track/a%2Fb%3Fsig%3Dsecret');
expect(attachmentPath({ attachment_kind: 'playlist', attachment_id: 'a/b' })).toBe('/shared/playlist/a%2Fb');
expect(attachmentPath({ attachment_kind: 'none' })).toBeNull();
expect(attachmentPath({ attachment_kind: 'track' })).toBeNull();
});
});
+27
View File
@@ -0,0 +1,27 @@
import type { components } from '@pipu/api-client';
import { type AuthSession, unwrap } from './auth';
export type SocialPerson = components['schemas']['SocialPerson'];
export type SocialInbox = components['schemas']['SocialInbox'];
export type SocialAction = components['schemas']['SocialAction'];
export type DirectMessage = components['schemas']['DirectMessage'];
export type SendDirectMessage = components['schemas']['SendDirectMessage'];
export type MusicAttachment = { kind: 'track' | 'playlist'; id: string; title: string };
/** All clients use the authenticated transport, including refresh and account isolation. */
export function socialApi(session: AuthSession) {
return {
inbox: async () => unwrap(await session.client.GET('/api/v1/me/social')),
search: async (q: string) => unwrap(await session.client.GET('/api/v1/me/social/people', { params: { query: { q } } })),
action: async (body: SocialAction) => unwrap(await session.client.POST('/api/v1/me/social/actions', { body })),
messages: async (peerId: string, before?: number) => unwrap(await session.client.GET('/api/v1/me/social/chats/{peerId}', { params: { path: { peerId }, query: { before } } })),
send: async (peerId: string, body: SendDirectMessage) => unwrap(await session.client.POST('/api/v1/me/social/chats/{peerId}', { params: { path: { peerId } }, body })),
};
}
export function attachmentPath(message: Pick<DirectMessage, 'attachment_kind' | 'attachment_id'>): string | null {
if (!message.attachment_id) return null;
if (message.attachment_kind === 'track') return `/track/${encodeURIComponent(message.attachment_id)}`;
if (message.attachment_kind === 'playlist') return `/shared/playlist/${encodeURIComponent(message.attachment_id)}`;
return null;
}