diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 158adacca..23b96e9fa 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1324,7 +1324,7 @@ export type HomeItemSource = { __typename?: 'HomeItemSource'; icon?: Maybe; id?: Maybe; - name: Scalars['String']; + name?: Maybe; type: HomeItemSourceType; url?: Maybe; }; @@ -6117,7 +6117,7 @@ export type HomeItemResolvers = { icon?: Resolver, ParentType, ContextType>; id?: Resolver, ParentType, ContextType>; - name?: Resolver; + name?: Resolver, ParentType, ContextType>; type?: Resolver; url?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 18a8fca83..ab28f495b 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1192,7 +1192,7 @@ type HomeItem { type HomeItemSource { icon: String id: ID - name: String! + name: String type: HomeItemSourceType! url: String } diff --git a/packages/api/src/jobs/update_home.ts b/packages/api/src/jobs/update_home.ts index 5b5e46aa8..29d1ca477 100644 --- a/packages/api/src/jobs/update_home.ts +++ b/packages/api/src/jobs/update_home.ts @@ -104,19 +104,55 @@ const publicItemToCandidate = (item: PublicItem): Candidate => ({ score: 0, }) -const selectCandidates = async (user: User): Promise> => { - const userId = user.id - // get last 100 library items saved and not seen by user +const getJustAddedCandidates = async ( + userId: string, + limit = 100 +): Promise> => { const libraryItems = await searchLibraryItems( { - size: 100, + size: limit, includeContent: false, - query: `-is:seen wordsCount:>0`, + query: `in:inbox saved:last24hrs -is:seen`, }, userId ) - logger.info(`Found ${libraryItems.length} library items`) + logger.info(`Found ${libraryItems.length} just added library items`) + + // get subscriptions for the library items + const subscriptionNames = libraryItems + .filter((item) => !!item.subscription) + .map((item) => item.subscription as string) + + const subscriptions = await findSubscriptionsByNames( + userId, + subscriptionNames + ) + + // map library items to candidates + const justAddedCandidates: Array = libraryItems.map((item) => + libraryItemToCandidate(item, subscriptions) + ) + + return justAddedCandidates +} + +const selectCandidates = async ( + user: User, + limit = 100 +): Promise> => { + const userId = user.id + // get last 100 library items saved and not seen by user + const libraryItems = await searchLibraryItems( + { + size: limit, + includeContent: false, + query: `in:inbox -saved:last24hrs -is:seen`, + }, + userId + ) + + logger.info(`Found ${libraryItems.length} not just added library items`) // get subscriptions for the library items const subscriptionNames = libraryItems @@ -290,27 +326,10 @@ const appendSectionsToHome = async ( await pipeline.exec() } -const mixHomeItems = (rankedHomeItems: Array): Array
=> { - // find the median word count - const wordCounts = rankedHomeItems.map((item) => item.wordCount) - wordCounts.sort((a, b) => a - b) - const medianWordCount = wordCounts[Math.floor(wordCounts.length / 2)] - // separate items into two groups based on word count - const shortItems: Array = [] - const longItems: Array = [] - for (const item of rankedHomeItems) { - if (item.wordCount < medianWordCount) { - shortItems.push(item) - } else { - longItems.push(item) - } - } - // initialize empty batches - const batches: Array> = Array.from( - { length: Math.floor(rankedHomeItems.length / 10) }, - () => [] - ) - +const mixHomeItems = ( + justAddedCandidates: Array, + rankedHomeItems: Array +): Array
=> { const checkConstraints = (batch: Array, item: Candidate) => { const titleCount = batch.filter((i) => i.title === item.title).length const authorCount = batch.filter((i) => i.author === item.author).length @@ -328,14 +347,22 @@ const mixHomeItems = (rankedHomeItems: Array): Array
=> { ) } + const candidateToItem = (candidate: Candidate): Item => ({ + id: candidate.id, + type: candidate.type, + score: candidate.score, + }) + const distributeItems = ( items: Array, batches: Array> ) => { + const batchSize = Math.ceil(items.length / batches.length) + for (const item of items) { let added = false for (const batch of batches) { - if (batch.length < 5 && checkConstraints(batch, item)) { + if (batch.length < batchSize && checkConstraints(batch, item)) { batch.push(item) added = true break @@ -344,7 +371,7 @@ const mixHomeItems = (rankedHomeItems: Array): Array
=> { if (!added) { for (const batch of batches) { - if (batch.length < 10) { + if (batch.length < batchSize) { batch.push(item) break } @@ -353,31 +380,51 @@ const mixHomeItems = (rankedHomeItems: Array): Array
=> { } } - // distribute quick link items first - distributeItems(shortItems, batches) - distributeItems(longItems, batches) + // find the median word count + const wordCounts = rankedHomeItems.map((item) => item.wordCount) + wordCounts.sort((a, b) => a - b) + const medianWordCount = wordCounts[Math.floor(wordCounts.length / 2)] + // separate items into two groups based on word count + const shortItems: Array = [] + const longItems: Array = [] + for (const item of rankedHomeItems) { + if (item.wordCount < medianWordCount) { + shortItems.push(item) + } else { + longItems.push(item) + } + } + + // initialize empty batches + const numOfBatches = 10 + const batches = { + short: Array.from({ length: numOfBatches }, () => []) as Array< + Array + >, + long: Array.from({ length: numOfBatches }, () => []) as Array< + Array + >, + } + + distributeItems(shortItems, batches.short) + distributeItems(longItems, batches.long) // convert batches to sections const sections = [] - for (const batch of batches) { - // create a section for all quick links - sections.push({ - items: batch.slice(0, 5).map((item) => ({ - id: item.id, - type: item.type, - score: item.score, - })), - layout: 'quick links', - }) + sections.push({ + items: batches.short.flat().map(candidateToItem), + layout: 'quick links', + }) - // create a section for each long item - sections.push( - ...batch.slice(5).map((item) => ({ - items: [{ id: item.id, type: item.type, score: item.score }], - layout: 'long', - })) - ) - } + sections.push({ + items: batches.long.flat().map(candidateToItem), + layout: 'long', + }) + + sections.push({ + items: justAddedCandidates.map(candidateToItem), + layout: 'just added', + }) return sections } @@ -395,6 +442,13 @@ export const updateHome = async (data: UpdateHomeJobData) => { logger.info(`Updating home for user ${userId}`) + logger.profile('justAdded') + const justAddedCandidates = await getJustAddedCandidates(userId) + logger.profile('justAdded', { + level: 'info', + message: `Found ${justAddedCandidates.length} just added candidates`, + }) + logger.profile('selecting') const candidates = await selectCandidates(user) logger.profile('selecting', { @@ -402,7 +456,7 @@ export const updateHome = async (data: UpdateHomeJobData) => { message: `Found ${candidates.length} candidates`, }) - if (candidates.length === 0) { + if (!justAddedCandidates.length && !candidates.length) { logger.info('No candidates found') return } @@ -419,7 +473,7 @@ export const updateHome = async (data: UpdateHomeJobData) => { // TODO: filter candidates logger.profile('mixing') - const rankedSections = mixHomeItems(rankedCandidates) + const rankedSections = mixHomeItems(justAddedCandidates, rankedCandidates) logger.profile('mixing', { level: 'info', message: `Created ${rankedSections.length} sections`, diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 329961d37..1821ab37a 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -3121,7 +3121,7 @@ const schema = gql` type HomeItemSource { id: ID - name: String! + name: String url: String icon: String type: HomeItemSourceType!