mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
add just added section to home
This commit is contained in:
parent
93f1076a1c
commit
4bb33da27f
4 changed files with 110 additions and 56 deletions
|
|
@ -1324,7 +1324,7 @@ export type HomeItemSource = {
|
|||
__typename?: 'HomeItemSource';
|
||||
icon?: Maybe<Scalars['String']>;
|
||||
id?: Maybe<Scalars['ID']>;
|
||||
name: Scalars['String'];
|
||||
name?: Maybe<Scalars['String']>;
|
||||
type: HomeItemSourceType;
|
||||
url?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
|
@ -6117,7 +6117,7 @@ export type HomeItemResolvers<ContextType = ResolverContext, ParentType extends
|
|||
export type HomeItemSourceResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['HomeItemSource'] = ResolversParentTypes['HomeItemSource']> = {
|
||||
icon?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
type?: Resolver<ResolversTypes['HomeItemSourceType'], ParentType, ContextType>;
|
||||
url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
|
|||
|
|
@ -1192,7 +1192,7 @@ type HomeItem {
|
|||
type HomeItemSource {
|
||||
icon: String
|
||||
id: ID
|
||||
name: String!
|
||||
name: String
|
||||
type: HomeItemSourceType!
|
||||
url: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,19 +104,55 @@ const publicItemToCandidate = (item: PublicItem): Candidate => ({
|
|||
score: 0,
|
||||
})
|
||||
|
||||
const selectCandidates = async (user: User): Promise<Array<Candidate>> => {
|
||||
const userId = user.id
|
||||
// get last 100 library items saved and not seen by user
|
||||
const getJustAddedCandidates = async (
|
||||
userId: string,
|
||||
limit = 100
|
||||
): Promise<Array<Candidate>> => {
|
||||
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<Candidate> = libraryItems.map((item) =>
|
||||
libraryItemToCandidate(item, subscriptions)
|
||||
)
|
||||
|
||||
return justAddedCandidates
|
||||
}
|
||||
|
||||
const selectCandidates = async (
|
||||
user: User,
|
||||
limit = 100
|
||||
): Promise<Array<Candidate>> => {
|
||||
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<Candidate>): Array<Section> => {
|
||||
// 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<Candidate> = []
|
||||
const longItems: Array<Candidate> = []
|
||||
for (const item of rankedHomeItems) {
|
||||
if (item.wordCount < medianWordCount) {
|
||||
shortItems.push(item)
|
||||
} else {
|
||||
longItems.push(item)
|
||||
}
|
||||
}
|
||||
// initialize empty batches
|
||||
const batches: Array<Array<Candidate>> = Array.from(
|
||||
{ length: Math.floor(rankedHomeItems.length / 10) },
|
||||
() => []
|
||||
)
|
||||
|
||||
const mixHomeItems = (
|
||||
justAddedCandidates: Array<Candidate>,
|
||||
rankedHomeItems: Array<Candidate>
|
||||
): Array<Section> => {
|
||||
const checkConstraints = (batch: Array<Candidate>, 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<Candidate>): Array<Section> => {
|
|||
)
|
||||
}
|
||||
|
||||
const candidateToItem = (candidate: Candidate): Item => ({
|
||||
id: candidate.id,
|
||||
type: candidate.type,
|
||||
score: candidate.score,
|
||||
})
|
||||
|
||||
const distributeItems = (
|
||||
items: Array<Candidate>,
|
||||
batches: Array<Array<Candidate>>
|
||||
) => {
|
||||
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<Candidate>): Array<Section> => {
|
|||
|
||||
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<Candidate>): Array<Section> => {
|
|||
}
|
||||
}
|
||||
|
||||
// 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<Candidate> = []
|
||||
const longItems: Array<Candidate> = []
|
||||
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<Candidate>
|
||||
>,
|
||||
long: Array.from({ length: numOfBatches }, () => []) as Array<
|
||||
Array<Candidate>
|
||||
>,
|
||||
}
|
||||
|
||||
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`,
|
||||
|
|
|
|||
|
|
@ -3121,7 +3121,7 @@ const schema = gql`
|
|||
|
||||
type HomeItemSource {
|
||||
id: ID
|
||||
name: String!
|
||||
name: String
|
||||
url: String
|
||||
icon: String
|
||||
type: HomeItemSourceType!
|
||||
|
|
|
|||
Loading…
Reference in a new issue