Skip to content

Commit 6bd5c27

Browse files
fix(notifications): personalize embedded users in related response (#14366)
## Summary - Regenerate `@audius/sdk` to pick up the new `user_id` query parameter and renamed path placeholder on `GET /notifications/{id}` and `GET /notifications/{id}/playlist_updates` (api PRs [AudiusProject/api#837](AudiusProject/api#837) + [AudiusProject/api#838](AudiusProject/api#838), both merged). - Pass the current user id through every call site so the backend personalizes embedded `related.users` in the response (`does_current_user_follow`, etc.). ## Why The notifications endpoint embeds user objects in `related.users`. The backend personalizes those via `MyID: app.getMyId(c)` in [`api/v1_notifications.go:336`](https://github.com/AudiusProject/api/blob/main/api/v1_notifications.go#L336), and [`getMyId`](https://github.com/AudiusProject/api/blob/main/api/resolve_middleware.go#L30) reads from the **query string** `?user_id=`, not the URL path. The SDK only put the id in the path, so on the backend `MyID = 0` and the SQL [short-circuited](https://github.com/AudiusProject/api/blob/main/api/dbv1/get_users.sql.go#L129-L136) `does_current_user_follow` to `false` for every embedded user. On a cold app boot, `useNotifications` fires early and `primeRelatedData` writes those un-personalized users into the tan-query cache. `primeUserData` only writes when the slot is empty (no `forceReplace`), so the bad entries stick until the user navigates to that profile and a personalized `getUser` fetch overwrites the cache. That's why visiting `@audius` directly and refreshing makes the Following state appear correct. A backend-only fix would collapse two semantically distinct ids (notifications owner vs. requester) — incorrect when a manager reads a managed user's notifications. The fix is client-side: send the requester id as `?user_id=`, keep the path id for whose notifications. ## Changes | File | Change | | --- | --- | | `packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts` | Regenerated. Path field renames `userId` → `id`; new `userId?: string` query field carries the requester id. | | `packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts` | Incidental drift: `getUserForYouFeed` was removed upstream and the generated copy was stale. | | `packages/common/src/api/tan-query/notifications/useNotifications.ts` | Pass `{ id, userId }` to the SDK. | | `packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts` | Update local cast type and call site to use `id` as the path field. (No requester id passed here — the unread count doesn't hydrate `related.users`.) | | `packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts` | Pass `{ id, userId }` to the SDK; widen the local cast type. | ## Breaking SDK change (external consumers) The path field on `GetNotificationsRequest` and `GetPlaylistUpdatesRequest` renames from `userId` to `id`. External `@audius/sdk` consumers of these two methods will need to rename the field. Wire format and server behaviour are unchanged. ## Test plan - [ ] CI green (typecheck/lint/tests). - [ ] On a fresh app load with a signed-in account that follows `@audius`: confirm Following state renders correctly on lists hydrated from notification responses (e.g. notification panel actor avatars, recommended profiles surfaced from related users) without first having to navigate to `@audius`'s profile. - [ ] Network inspector on the notifications request shows `?user_id=<hashed-current-user-id>` and `does_current_user_follow: true` for users actually followed in `related.users`. - [ ] Manager flow: read notifications while acting as a managed user. `?user_id=` carries whatever the app treats as "current user" (the managed user in normal manager-switched state); personalization in the response reflects that perspective. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a80d544 commit 6bd5c27

6 files changed

Lines changed: 51 additions & 75 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@audius/sdk': major
3+
---
4+
5+
Rename the path field on `getNotifications` and `getPlaylistUpdates` from `userId` to `id`, matching the convention used by every `/users/{id}/…` method. Add an optional `userId` query field that carries the requester id for personalization of embedded `related.users` (e.g. `does_current_user_follow`).
6+
7+
**Migration:**
8+
9+
```ts
10+
// Before
11+
sdk.notifications.getNotifications({ userId: 'aE9MA' })
12+
sdk.notifications.getPlaylistUpdates({ userId: 'aE9MA' })
13+
14+
// After
15+
sdk.notifications.getNotifications({
16+
id: 'aE9MA', // notifications owner (was `userId`)
17+
userId: 'aE9MA' // requester id, for personalization of related.users
18+
})
19+
sdk.notifications.getPlaylistUpdates({ id: 'aE9MA', userId: 'aE9MA' })
20+
```
21+
22+
The two ids differ only when a manager reads a managed user's notifications; in the normal flow they're the same value. The wire format and server URL are unchanged — only the request type shape was renamed to remove a collision between the path and the new query parameter.

packages/common/src/api/tan-query/notifications/useNotificationUnreadCount.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ export const useNotificationUnreadCount = () => {
3535
const response = await (
3636
sdk.notifications as {
3737
getNotifications: (params: {
38-
userId: string
38+
id: string
3939
limit?: number
4040
}) => Promise<{
4141
data?: { unreadCount?: number }
4242
}>
4343
}
4444
).getNotifications({
45-
userId: Id.parse(currentUserId),
45+
id: Id.parse(currentUserId),
4646
limit: 0
4747
})
4848
return response?.data?.unreadCount ?? 0

packages/common/src/api/tan-query/notifications/useNotifications.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export const useNotifications = (options?: QueryOptions) => {
7676
queryFn: async ({ pageParam = null }) => {
7777
const sdk = await audiusSdk()
7878
const response = await sdk.notifications.getNotifications({
79+
id: Id.parse(currentUserId),
80+
// Requester id sent as `?user_id=` so the backend personalizes
81+
// embedded related.users (e.g. does_current_user_follow). The path
82+
// id alone identifies the notifications owner, not the requester.
7983
userId: Id.parse(currentUserId),
8084
limit: DEFAULT_LIMIT,
8185
timestamp: pageParam?.timestamp,

packages/common/src/api/tan-query/playlist-updates/usePlaylistUpdates.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ export const usePlaylistUpdates = <TResult = PlaylistUpdate[]>(
3838
const sdk = await audiusSdk()
3939
// sdk.notifications.getPlaylistUpdates is not currently typed in the
4040
// public SDK surface; cast to the expected shape used in the legacy saga.
41+
// userId carries the requester id as `?user_id=` so the backend can
42+
// personalize related.users in the response.
4143
const response = (await (
4244
sdk.notifications as {
4345
getPlaylistUpdates: (params: {
44-
userId: string
46+
id: string
47+
userId?: string
4548
}) => Promise<PlaylistUpdatesResponse>
4649
}
4750
).getPlaylistUpdates({
51+
id: Id.parse(currentUserId),
4852
userId: Id.parse(currentUserId)
4953
})) as PlaylistUpdatesResponse | undefined
5054

packages/sdk/src/sdk/api/generated/default/apis/NotificationsApi.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ import {
2626
} from '../models';
2727

2828
export interface GetNotificationsRequest {
29-
userId: string;
29+
id: string;
30+
userId?: string;
3031
timestamp?: number;
3132
groupId?: string;
3233
limit?: number;
3334
types?: Array<GetNotificationsTypesEnum>;
3435
}
3536

3637
export interface GetPlaylistUpdatesRequest {
37-
userId: string;
38+
id: string;
39+
userId?: string;
3840
}
3941

4042
/**
@@ -47,12 +49,16 @@ export class NotificationsApi extends runtime.BaseAPI {
4749
* Get notifications for user ID
4850
*/
4951
async getNotificationsRaw(params: GetNotificationsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<NotificationsResponse>> {
50-
if (params.userId === null || params.userId === undefined) {
51-
throw new runtime.RequiredError('userId','Required parameter params.userId was null or undefined when calling getNotifications.');
52+
if (params.id === null || params.id === undefined) {
53+
throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getNotifications.');
5254
}
5355

5456
const queryParameters: any = {};
5557

58+
if (params.userId !== undefined) {
59+
queryParameters['user_id'] = params.userId;
60+
}
61+
5662
if (params.timestamp !== undefined) {
5763
queryParameters['timestamp'] = params.timestamp;
5864
}
@@ -72,7 +78,7 @@ export class NotificationsApi extends runtime.BaseAPI {
7278
const headerParameters: runtime.HTTPHeaders = {};
7379

7480
const response = await this.request({
75-
path: `/notifications/{user_id}`.replace(`{${"user_id"}}`, encodeURIComponent(String(params.userId))),
81+
path: `/notifications/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))),
7682
method: 'GET',
7783
headers: headerParameters,
7884
query: queryParameters,
@@ -94,16 +100,20 @@ export class NotificationsApi extends runtime.BaseAPI {
94100
* Get playlists the user has saved that have been updated for user ID
95101
*/
96102
async getPlaylistUpdatesRaw(params: GetPlaylistUpdatesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<PlaylistUpdatesResponse>> {
97-
if (params.userId === null || params.userId === undefined) {
98-
throw new runtime.RequiredError('userId','Required parameter params.userId was null or undefined when calling getPlaylistUpdates.');
103+
if (params.id === null || params.id === undefined) {
104+
throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getPlaylistUpdates.');
99105
}
100106

101107
const queryParameters: any = {};
102108

109+
if (params.userId !== undefined) {
110+
queryParameters['user_id'] = params.userId;
111+
}
112+
103113
const headerParameters: runtime.HTTPHeaders = {};
104114

105115
const response = await this.request({
106-
path: `/notifications/{user_id}/playlist_updates`.replace(`{${"user_id"}}`, encodeURIComponent(String(params.userId))),
116+
path: `/notifications/{id}/playlist_updates`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))),
107117
method: 'GET',
108118
headers: headerParameters,
109119
query: queryParameters,

packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -686,14 +686,6 @@ export interface GetUserFeedRequest {
686686
encodedDataSignature?: string;
687687
}
688688

689-
export interface GetUserForYouFeedRequest {
690-
id: string;
691-
limit?: number;
692-
offset?: number;
693-
maxPerArtist?: number;
694-
userId?: string;
695-
}
696-
697689
export interface GetUserIDsByAddressesRequest {
698690
address: Array<string>;
699691
}
@@ -4175,62 +4167,6 @@ export class UsersApi extends runtime.BaseAPI {
41754167
return await response.value();
41764168
}
41774169

4178-
/**
4179-
* @hidden
4180-
* Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground, similar-artist) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead).
4181-
* Get For You feed for user
4182-
*/
4183-
async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Tracks>> {
4184-
if (params.id === null || params.id === undefined) {
4185-
throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getUserForYouFeed.');
4186-
}
4187-
4188-
const queryParameters: any = {};
4189-
4190-
if (params.limit !== undefined) {
4191-
queryParameters['limit'] = params.limit;
4192-
}
4193-
4194-
if (params.offset !== undefined) {
4195-
queryParameters['offset'] = params.offset;
4196-
}
4197-
4198-
if (params.maxPerArtist !== undefined) {
4199-
queryParameters['max_per_artist'] = params.maxPerArtist;
4200-
}
4201-
4202-
if (params.userId !== undefined) {
4203-
queryParameters['user_id'] = params.userId;
4204-
}
4205-
4206-
const headerParameters: runtime.HTTPHeaders = {};
4207-
4208-
if (!headerParameters["Authorization"] && this.configuration && this.configuration.accessToken) {
4209-
const token = await this.configuration.accessToken("OAuth2", ["read"]);
4210-
if (token) {
4211-
headerParameters["Authorization"] = token;
4212-
}
4213-
}
4214-
4215-
const response = await this.request({
4216-
path: `/users/{id}/feed/for-you`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))),
4217-
method: 'GET',
4218-
headers: headerParameters,
4219-
query: queryParameters,
4220-
}, initOverrides);
4221-
4222-
return new runtime.JSONApiResponse(response, (jsonValue) => TracksFromJSON(jsonValue));
4223-
}
4224-
4225-
/**
4226-
* Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground, similar-artist) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead).
4227-
* Get For You feed for user
4228-
*/
4229-
async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Tracks> {
4230-
const response = await this.getUserForYouFeedRaw(params, initOverrides);
4231-
return await response.value();
4232-
}
4233-
42344170
/**
42354171
* @hidden
42364172
* Gets User IDs from any Ethereum wallet address or Solana account address associated with their Audius account.

0 commit comments

Comments
 (0)