newOS for Developers
User Management
Profile CRUD, avatar upload, social graph, and user discovery operations.
packages/newgraph-signals/src/actions/user.ts (712 lines)
Quick Reference
CRUD Operations
readUser— Fetch user by IDreadUserByUsername— Fetch by usernameupdateUser— Update profile (bio, avatar)searchUsers— Search users by query
Related Data
readUserFolders— Get user's folders/moodsreadUserGrants— Get access grantsreadUserHistory— Get activity historyreadUserOutgraph— Get social outgraph
Social Graph
poweredInUsers— Users who powered this userpoweredOutUsers— Users this user poweredlistTopUsers— Top users by wattsleaderBoardUsers— Filtered leaderboard
Cache & WATTS
cacheUsers— Store to IndexedDBuserQuery— Query from cacheclaimWatts— Claim WATTS tokensgrants— Query user's folder grants
API Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
| /user | GET | Get current user (private profile) |
| /user | PUT | Update user profile |
| /user/{id} | GET | Get user by ID (public profile) |
| /user/upload/avatar | POST | Get presigned S3 URL for avatar |
| /user/{id}/moods | GET | Get user's public folders |
| /user/{id}/moods/own | GET | Get own folders (when owner) |
| /user/{id}/grants | GET | Get access grants for user |
| /user/top | GET | List top users by watts |
| /user/search | GET | Search users with filters |
| /user/history | GET | Get current user's activity |
| /user/watts/claim | POST | Claim WATTS tokens |
readUser
ProgressiveHandler Auto-startFetch a user's public profile by ID with caching.
readUser({ id }: QueryById)
: ProgressiveHandlerResponse<UserReadPublicResponse>| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | User ID to fetch |
API Endpoint
api.user.userList({id})Nuances
- Auto-starts on call (no need to call
.exec()) - Returns cached data first via
userQuery(id) - Fetches from API and updates cache via
cacheUsers() - Returns early if no id provided
Usage
import { readUser } from "newgraph-signals/actions/user";
const [user, progress] = readUser({ id: "abc123" });
// Auto-starts, no exec needed
await progress.value.promise;
console.log(user.value); // UserReadPublicResponseupdateUser
ProgressiveHandlerUpdate a user's profile with optional avatar upload to S3.
updateUser(): ProgressiveHandlerResponse<void>
// Execute with:
progress.exec({
userData: UserUpdateRequest;
profilePicture?: {
name: string;
type: string;
originFileObj: File;
};
})| Parameter | Type | Description |
|---|---|---|
| userData | UserUpdateRequest | Profile fields to update |
| userData.id | string | User ID (defaults to current user) |
| userData.displayName | string | Display name |
| userData.bio | string | User bio/description |
| profilePicture | File object | Avatar image to upload |
Avatar Upload Flow
- Resizes image to thumbnail via
resizeImage() - Updates cache with temporary base64
contentUrl - Calls
api.user.userUpdate()to update profile - Gets presigned S3 URL via
api.user.userUploadAvatarCreate() - Uploads file directly to S3 via PUT request
- Updates
_currentsignal if updating own profile
Nuances
- Does NOT auto-start — must call
exec() - Stores
uploadStatein cache during upload for UI feedback - Preserves existing tags if new tags array is empty
- Uses force cache update to ensure immediate UI refresh
readUserFolders
ProgressiveHandler Auto-start PaginatedGet all folders/moods created by a user.
readUserFolders(
{ id }: QueryById,
flags?: string
): ProgressiveHandlerResponse<MoodReadResponse[]>API Method Selection
| Condition | Method |
|---|---|
| Logged in & viewing own profile | moodsOwnList |
| Logged in & viewing other user | moodsList |
| Not logged in | moodsPubList |
Nuances
- Caches folders to IndexedDB
- Stores edges:
user+id+author+folder - Results sorted by created date (newest first) via local sort
- Paginates automatically until
done: true
Social Graph
poweredInUsers
Auto-start PaginatedGet users who have "powered" (staked WATTS on) the specified user.
poweredInUsers({ id: string })
: ProgressiveHandlerResponse<UserReadPublicResponse[]>
// API: api.user.ratedInList({ id, page })poweredOutUsers
Auto-start PaginatedGet users that the specified user has "powered" (staked on).
poweredOutUsers({ id: string })
: ProgressiveHandlerResponse<UserReadPublicResponse[]>
// API: api.user.ratedOutUsersList({ id, page })listTopUsers
Auto-start PaginatedGet the leaderboard of top users sorted by WATTS.
listTopUsers(params?: { isTrusted?: boolean })
: ProgressiveHandlerResponse<UserReadPublicResponse[]>
// API: api.user.listTopList({ page, isTrusted })WATTS Management
claimWatts
AsyncClaim unclaimed WATTS tokens for the current user.
claimWatts(claims: WattsClaimRequest): Promise<WattsClaimResponse>
// API: api.user.claimWattsCreate({ claims })Cache Utilities
IndexedDBcacheUsers
cacheUsers(
users: UserReadPublicResponse | UserReadPublicResponse[],
opts?: { force?: boolean }
): Promise<void>Stores users to IndexedDB with smart merging:
- Skips if incoming has ≤2 properties and cache has user
- Skips if cached
updatedis newer (unless force) - Preserves valid
subwattsJSON if incoming is invalid - Uses
bulkPutfor multiple users
userQuery
userQuery(id: string | string[]): Promise<UserReadPublicResponse>Query user from cache.user table by ID.
grants
grants(
id?: string | string[],
opts?: { sortBy?: string; reverse?: boolean }
): Promise<MoodReadResponse[]>Query folders user has access to from edge table:user+id+access+folder
Demo Widget
Example component showing user profile:
"use client";
import { useEffect, useState } from "react";
import { readUser } from "newgraph-signals/actions/user";
import { Badge } from "@/components/ui/badge";
export function UserProfileWidget({ userId }: { userId: string }) {
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const [userSignal, progress] = readUser({ id: userId });
progress.value.promise.then(() => {
setUser(userSignal.value);
setLoading(false);
});
}, [userId]);
if (loading) return <div className="animate-pulse">Loading...</div>;
if (!user?.id) return <div>User not found</div>;
return (
<div className="rounded-lg border p-4 space-y-3">
<div className="flex items-center gap-3">
{user.contentUrl && (
<img
src={user.contentUrl}
alt={user.username}
className="w-12 h-12 rounded-full object-cover"
/>
)}
<div>
<p className="font-medium">{user.displayName || user.username}</p>
<p className="text-sm text-muted-foreground">@{user.username}</p>
</div>
</div>
<div className="flex gap-2">
<Badge variant="outline">{user.watts || 0} WATTS</Badge>
<Badge variant="secondary">{user.id}</Badge>
</div>
</div>
);
}