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 ID
  • readUserByUsername — Fetch by username
  • updateUser — Update profile (bio, avatar)
  • searchUsers — Search users by query

Related Data

  • readUserFolders — Get user's folders/moods
  • readUserGrants — Get access grants
  • readUserHistory — Get activity history
  • readUserOutgraph — Get social outgraph

Social Graph

  • poweredInUsers — Users who powered this user
  • poweredOutUsers — Users this user powered
  • listTopUsers — Top users by watts
  • leaderBoardUsers — Filtered leaderboard

Cache & WATTS

  • cacheUsers — Store to IndexedDB
  • userQuery — Query from cache
  • claimWatts — Claim WATTS tokens
  • grants — Query user's folder grants

API Endpoints

EndpointMethodPurpose
/userGETGet current user (private profile)
/userPUTUpdate user profile
/user/{id}GETGet user by ID (public profile)
/user/upload/avatarPOSTGet presigned S3 URL for avatar
/user/{id}/moodsGETGet user's public folders
/user/{id}/moods/ownGETGet own folders (when owner)
/user/{id}/grantsGETGet access grants for user
/user/topGETList top users by watts
/user/searchGETSearch users with filters
/user/historyGETGet current user's activity
/user/watts/claimPOSTClaim WATTS tokens

readUser

ProgressiveHandler Auto-start

Fetch a user's public profile by ID with caching.

readUser({ id }: QueryById)
  : ProgressiveHandlerResponse<UserReadPublicResponse>
ParameterTypeRequiredDescription
idstringYesUser 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); // UserReadPublicResponse

updateUser

ProgressiveHandler

Update 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;
  };
})
ParameterTypeDescription
userDataUserUpdateRequestProfile fields to update
userData.idstringUser ID (defaults to current user)
userData.displayNamestringDisplay name
userData.biostringUser bio/description
profilePictureFile objectAvatar image to upload

Avatar Upload Flow

  1. Resizes image to thumbnail via resizeImage()
  2. Updates cache with temporary base64 contentUrl
  3. Calls api.user.userUpdate() to update profile
  4. Gets presigned S3 URL via api.user.userUploadAvatarCreate()
  5. Uploads file directly to S3 via PUT request
  6. Updates _current signal if updating own profile

Nuances

  • Does NOT auto-start — must call exec()
  • Stores uploadState in 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 Paginated

Get all folders/moods created by a user.

readUserFolders(
  { id }: QueryById,
  flags?: string
): ProgressiveHandlerResponse<MoodReadResponse[]>

API Method Selection

ConditionMethod
Logged in & viewing own profilemoodsOwnList
Logged in & viewing other usermoodsList
Not logged inmoodsPubList

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 Paginated

Get users who have "powered" (staked WATTS on) the specified user.

poweredInUsers({ id: string })
  : ProgressiveHandlerResponse<UserReadPublicResponse[]>

// API: api.user.ratedInList({ id, page })

poweredOutUsers

Auto-start Paginated

Get users that the specified user has "powered" (staked on).

poweredOutUsers({ id: string })
  : ProgressiveHandlerResponse<UserReadPublicResponse[]>

// API: api.user.ratedOutUsersList({ id, page })

listTopUsers

Auto-start Paginated

Get the leaderboard of top users sorted by WATTS.

listTopUsers(params?: { isTrusted?: boolean })
  : ProgressiveHandlerResponse<UserReadPublicResponse[]>

// API: api.user.listTopList({ page, isTrusted })

WATTS Management

claimWatts

Async

Claim unclaimed WATTS tokens for the current user.

claimWatts(claims: WattsClaimRequest): Promise<WattsClaimResponse>

// API: api.user.claimWattsCreate({ claims })

Cache Utilities

IndexedDB

cacheUsers

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 updated is newer (unless force)
  • Preserves valid subwatts JSON if incoming is invalid
  • Uses bulkPut for 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>
  );
}

Related