@ -1,7 +1,7 @@
import dayjs from "dayjs" ;
import dayjs from "dayjs" ;
import { countBy } from "lodash-es" ;
import { countBy } from "lodash-es" ;
import { useEffect , useState } from "react" ;
import { useEffect , useState } from "react" ;
import { memoStore } from "@/store" ;
import { memoStore , userStore } from "@/store" ;
import type { StatisticsData } from "@/types/statistics" ;
import type { StatisticsData } from "@/types/statistics" ;
export interface FilteredMemoStats {
export interface FilteredMemoStats {
@ -11,20 +11,51 @@ export interface FilteredMemoStats {
}
}
/ * *
/ * *
* Hook to compute statistics and tags from memos in the store cache .
* Convert user name to user stats key .
* Backend returns UserStats with name "users/{id}/stats" but we pass "users/{id}"
* @param userName - User name in format "users/{id}"
* @returns Stats key in format "users/{id}/stats"
* /
const getUserStatsKey = ( userName : string ) : string = > {
return ` ${ userName } /stats ` ;
} ;
export interface UseFilteredMemoStatsOptions {
/ * *
* User name to fetch stats for ( e . g . , "users/123" )
*
* When provided :
* - Fetches backend user stats via GetUserStats API
* - Returns unfiltered tags and activity ( all NORMAL memos for that user )
* - Tags remain stable even when memo filters are applied
*
* When undefined :
* - Computes stats from cached memos in the store
* - Reflects current filters ( useful for Explore / Archived pages )
*
* IMPORTANT : Backend user stats only include NORMAL ( non - archived ) memos .
* Do NOT use for Archived page context .
* /
userName? : string ;
}
/ * *
* Hook to compute statistics and tags for the sidebar .
*
*
* This provides a unified approach for all pages ( Home , Explore , Archived , Profile ) :
* Data sources by context :
* - Uses memos already loaded in the store by PagedMemoList
* - * * Home / Profile * * : Uses backend UserStats API ( unfiltered , normal memos only )
* - Computes statistics and tags from those cached memos
* - * * Archived / Explore * * : Computes from cached memos ( filtered by page context )
* - Updates automatically when memos are created , updated , or deleted
* - No separate API call needed , reducing network overhead
*
*
* @returns Object with statistics data , tag counts , and loading state
* Benefits of using backend stats :
* - Tag list remains stable when memo filters are applied
* - Activity calendar shows full history , not just filtered results
* - Prevents "disappearing tags" issue when filtering by tag
*
*
* Note : This hook now computes stats from the memo store cache rather than
* @param options - Configuration options
* making a separate API call . It relies on PagedMemoList to populate the store .
* @returns Object with statistics data , tag counts , and loading state
* /
* /
export const useFilteredMemoStats = ( ) : FilteredMemoStats = > {
export const useFilteredMemoStats = ( options : UseFilteredMemoStatsOptions = { } ) : FilteredMemoStats = > {
const { userName } = options ;
const [ data , setData ] = useState < FilteredMemoStats > ( {
const [ data , setData ] = useState < FilteredMemoStats > ( {
statistics : {
statistics : {
activityStats : { } ,
activityStats : { } ,
@ -34,33 +65,65 @@ export const useFilteredMemoStats = (): FilteredMemoStats => {
} ) ;
} ) ;
// React to memo store changes (create, update, delete)
// React to memo store changes (create, update, delete)
const memoStoreStateId = memoStore . state . stateId ;
const memoStoreStateId = memoStore . state . stateId ;
// React to user stats changes (for tag counts)
const userStatsStateId = userStore . state . statsStateId ;
useEffect ( ( ) = > {
useEffect ( ( ) = > {
// Compute statistics and tags from memos already in the store
const computeStats = async ( ) = > {
// This avoids making a separate API call and relies on PagedMemoList to populate the store
let activityStats : Record < string , number > = { } ;
const computeStatsFromCache = ( ) = > {
let tagCount : Record < string , number > = { } ;
const displayTimeList : Date [ ] = [ ] ;
let useBackendStats = false ;
const tagCount : Record < string , number > = { } ;
// Use memos already loaded in the store
// Try to use backend user stats if userName is provided
const memos = memoStore . state . memos ;
if ( userName ) {
// Check if stats are already cached, otherwise fetch them
const statsKey = getUserStatsKey ( userName ) ;
let userStats = userStore . state . userStatsByName [ statsKey ] ;
for ( const memo of memos ) {
if ( ! userStats ) {
// Add display time for calendar
try {
if ( memo . displayTime ) {
await userStore . fetchUserStats ( userName ) ;
displayTimeList . push ( memo . displayTime ) ;
userStats = userStore . state . userStatsByName [ statsKey ] ;
} catch ( error ) {
console . error ( "Failed to fetch user stats:" , error ) ;
// Will fall back to computing from cache below
}
}
}
// Count tags
if ( userStats ) {
if ( memo . tags && memo . tags . length > 0 ) {
// Use activity timestamps from user stats
for ( const tag of memo . tags ) {
if ( userStats . memoDisplayTimestamps && userStats . memoDisplayTimestamps . length > 0 ) {
tagCount [ tag ] = ( tagCount [ tag ] || 0 ) + 1 ;
activityStats = countBy ( userStats . memoDisplayTimestamps . map ( ( date ) = > dayjs ( date ) . format ( "YYYY-MM-DD" ) ) ) ;
}
// Use tag counts from user stats
if ( userStats . tagCount ) {
tagCount = userStats . tagCount ;
}
}
useBackendStats = true ;
}
}
}
}
// Compute activity calendar data
// Fallback: compute from cached memos if backend stats not available
const activityStats = countBy ( displayTimeList . map ( ( date ) = > dayjs ( date ) . format ( "YYYY-MM-DD" ) ) ) ;
// Also used for Explore and Archived contexts
if ( ! useBackendStats ) {
const displayTimeList : Date [ ] = [ ] ;
const memos = memoStore . state . memos ;
for ( const memo of memos ) {
// Collect display timestamps for activity calendar
if ( memo . displayTime ) {
displayTimeList . push ( memo . displayTime ) ;
}
// Count tags
if ( memo . tags && memo . tags . length > 0 ) {
for ( const tag of memo . tags ) {
tagCount [ tag ] = ( tagCount [ tag ] || 0 ) + 1 ;
}
}
}
activityStats = countBy ( displayTimeList . map ( ( date ) = > dayjs ( date ) . format ( "YYYY-MM-DD" ) ) ) ;
}
setData ( {
setData ( {
statistics : { activityStats } ,
statistics : { activityStats } ,
@ -69,8 +132,8 @@ export const useFilteredMemoStats = (): FilteredMemoStats => {
} ) ;
} ) ;
} ;
} ;
computeStats FromCache ( ) ;
computeStats ( ) ;
} , [ memoStoreStateId ]) ;
} , [ memoStoreStateId , userStatsStateId , userName ]) ;
return data ;
return data ;
} ;
} ;