From 835b61064f78d314ca9650faee25ee19d76e790b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Sat, 7 Oct 2023 21:30:23 +0800 Subject: [PATCH 01/15] allow filter by wordsCount --- packages/api/src/resolvers/article/index.ts | 1 + .../api/src/services/integrations/readwise.ts | 20 +++++--- packages/api/src/services/library_item.ts | 29 ++++++++++- packages/api/src/utils/search.ts | 49 +++++++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index f2ae8f800..aa57c1543 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -649,6 +649,7 @@ export const searchResolver = authorized< size: first + 1, // fetch one more item to get next cursor sort: searchQuery.sort, includePending: true, + includeContent: params.includeContent || false, ...searchQuery, }, uid diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index 28c444cfc..f111599a8 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -6,7 +6,7 @@ import { LibraryItem } from '../../entity/library_item' import { env } from '../../env' import { wait } from '../../utils/helpers' import { logger } from '../../utils/logger' -import { getHighlightUrl } from '../highlights' +import { findHighlightsByLibraryItemId, getHighlightUrl } from '../highlights' import { IntegrationService } from './integration' interface ReadwiseHighlight { @@ -64,10 +64,12 @@ export class ReadwiseIntegration extends IntegrationService { ): Promise => { let result = true - const highlights = items.flatMap(this.libraryItemToReadwiseHighlight) + const highlights = await Promise.all( + items.map((item) => this.libraryItemToReadwiseHighlight(item)) + ) // If there are no highlights, we will skip the sync if (highlights.length > 0) { - result = await this.syncWithReadwise(integration.token, highlights) + result = await this.syncWithReadwise(integration.token, highlights.flat()) } // update integration syncedAt if successful @@ -84,10 +86,16 @@ export class ReadwiseIntegration extends IntegrationService { return result } - libraryItemToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => { - if (!item.highlights) return [] + libraryItemToReadwiseHighlight = async ( + item: LibraryItem + ): Promise => { + let highlights = item.highlights + if (!highlights) { + highlights = await findHighlightsByLibraryItemId(item.id, item.user.id) + } + const category = item.siteName === 'Twitter' ? 'tweets' : 'articles' - return item.highlights + return highlights .map((highlight) => { // filter out highlights that are not of type highlight or have no quote if ( diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 1dcdb146d..1d869fbfc 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -6,7 +6,7 @@ import { Label } from '../entity/label' import { LibraryItem, LibraryItemState } from '../entity/library_item' import { BulkActionType } from '../generated/graphql' import { createPubSubClient, EntityType } from '../pubsub' -import { authTrx } from '../repository' +import { authTrx, getColumns } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { wordsCount } from '../utils/helpers' import { @@ -17,6 +17,7 @@ import { LabelFilter, LabelFilterType, NoFilter, + RangeFilter, ReadFilter, Sort, SortBy, @@ -42,6 +43,7 @@ export interface SearchArgs { recommendedBy?: string includeContent?: boolean noFilters?: NoFilter[] + rangeFilters?: RangeFilter[] } export interface SearchResultItem { @@ -258,6 +260,22 @@ const buildWhereClause = ( ) } } + + if (args.includeContent) { + queryBuilder.addSelect('library_item.readableContent') + } + + if (args.rangeFilters && args.rangeFilters.length > 0) { + args.rangeFilters.forEach((filter, i) => { + const param = `range_${filter.field}_${i}` + queryBuilder.andWhere( + `library_item.${filter.field} ${filter.operator} ${param}`, + { + [param]: filter.value, + } + ) + }) + } } export const searchLibraryItems = async ( @@ -271,11 +289,20 @@ export const searchLibraryItems = async ( // default sort by saved_at const sortField = sort?.by || SortBy.SAVED + const selectColumns = getColumns(libraryItemRepository) + .map((column) => `library_item.${column}`) + .filter( + (column) => + column !== 'library_item.readableContent' && + column !== 'library_item.originalContent' + ) + // add pagination and sorting return authTrx( async (tx) => { const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') + .select(selectColumns) .where('library_item.user_id = :userId', { userId }) // build the where clause diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index 5bbe09cde..f5d9e9b6c 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -40,6 +40,7 @@ export interface SearchFilter { ids: string[] recommendedBy?: string noFilters: NoFilter[] + rangeFilters: RangeFilter[] } export enum LabelFilterType { @@ -63,6 +64,12 @@ export interface DateFilter { endDate?: Date } +export interface RangeFilter { + field: string + operator: string + value: number +} + export enum SortBy { SAVED = 'savedAt', UPDATED = 'updatedAt', @@ -255,6 +262,40 @@ const parseDateFilter = ( } } +const parseRangeFilter = ( + field: string, + str?: string +): RangeFilter | undefined => { + if (str === undefined) { + return undefined + } + + switch (field.toUpperCase()) { + case 'WORDSCOUNT': + field = 'wordCount' + break + default: + return undefined + } + + const operatorRegex = /([<>]=?)/ + const operator = str.match(operatorRegex)?.[0] + if (!operator) { + return undefined + } + + const value = str.replace(operatorRegex, '') + if (!value) { + return undefined + } + + return { + field, + operator, + value: Number(value), + } +} + const parseFieldFilter = ( field: string, str?: string @@ -323,6 +364,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { matchFilters: [], ids: [], noFilters: [], + rangeFilters: [], } if (!searchQuery) { @@ -337,6 +379,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { matchFilters: [], ids: [], noFilters: [], + rangeFilters: [], } } @@ -364,6 +407,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { 'site', 'note', 'rss', + 'wordCount', ], tokenize: true, }) @@ -460,6 +504,11 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { case 'mode': // mode is ignored and used only by the frontend break + case 'wordCount': { + const rangeFilter = parseRangeFilter(keyword.keyword, keyword.value) + rangeFilter && result.rangeFilters.push(rangeFilter) + break + } } } } From 3e91f3836b40f2a0f3769b951571fc22a2fdd94b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 9 Oct 2023 12:25:37 +0800 Subject: [PATCH 02/15] fix tests --- packages/api/src/services/integrations/readwise.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/api/src/services/integrations/readwise.ts b/packages/api/src/services/integrations/readwise.ts index f111599a8..0c6676ebd 100644 --- a/packages/api/src/services/integrations/readwise.ts +++ b/packages/api/src/services/integrations/readwise.ts @@ -65,7 +65,9 @@ export class ReadwiseIntegration extends IntegrationService { let result = true const highlights = await Promise.all( - items.map((item) => this.libraryItemToReadwiseHighlight(item)) + items.map((item) => + this.libraryItemToReadwiseHighlight(item, integration.user.id) + ) ) // If there are no highlights, we will skip the sync if (highlights.length > 0) { @@ -87,11 +89,12 @@ export class ReadwiseIntegration extends IntegrationService { } libraryItemToReadwiseHighlight = async ( - item: LibraryItem + item: LibraryItem, + userId: string ): Promise => { let highlights = item.highlights if (!highlights) { - highlights = await findHighlightsByLibraryItemId(item.id, item.user.id) + highlights = await findHighlightsByLibraryItemId(item.id, userId) } const category = item.siteName === 'Twitter' ? 'tweets' : 'articles' From 93c410a56414b73cf9a3fcc0c29c56fd4e263a34 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 10 Oct 2023 12:21:32 +0800 Subject: [PATCH 03/15] fix is:read filter and add is:reading filter --- .../api/src/events/user/profile_created.ts | 34 ----------- packages/api/src/services/create_user.ts | 6 +- packages/api/src/services/library_item.ts | 60 +++++++++++++------ packages/api/src/utils/search.ts | 3 + packages/api/test/resolvers/article.test.ts | 44 +++++++++++--- 5 files changed, 85 insertions(+), 62 deletions(-) delete mode 100644 packages/api/src/events/user/profile_created.ts diff --git a/packages/api/src/events/user/profile_created.ts b/packages/api/src/events/user/profile_created.ts deleted file mode 100644 index 0af9326e0..000000000 --- a/packages/api/src/events/user/profile_created.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - EntitySubscriberInterface, - EventSubscriber, - InsertEvent, -} from 'typeorm' -import { Profile } from '../../entity/profile' -import { createDefaultFiltersForUser } from '../../services/create_user' -import { addPopularReadsForNewUser } from '../../services/popular_reads' - -@EventSubscriber() -export class AddPopularReadsToNewUser - implements EntitySubscriberInterface -{ - listenTo() { - return Profile - } - - async afterInsert(event: InsertEvent): Promise { - await addPopularReadsForNewUser(event.entity.user.id, event.manager) - } -} - -@EventSubscriber() -export class AddDefaultFiltersToNewUser - implements EntitySubscriberInterface -{ - listenTo() { - return Profile - } - - async afterInsert(event: InsertEvent): Promise { - await createDefaultFiltersForUser(event.manager)(event.entity.user.id) - } -} diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 4648658c8..6ded934b4 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -15,6 +15,7 @@ import { analytics } from '../utils/analytics' import { IntercomClient } from '../utils/intercom' import { logger } from '../utils/logger' import { validateUsername } from '../utils/usernamePolicy' +import { addPopularReadsForNewUser } from './popular_reads' import { sendConfirmationEmail } from './send_emails' export const MAX_RECORDS_LIMIT = 1000 @@ -103,6 +104,9 @@ export const createUser = async (input: { }) } + await addPopularReadsForNewUser(user.id, t) + await createDefaultFiltersForUser(t)(user.id) + return [user, profile] } ) @@ -146,7 +150,7 @@ export const createUser = async (input: { return [user, profile] } -export const createDefaultFiltersForUser = +const createDefaultFiltersForUser = (t: EntityManager) => async (userId: string): Promise => { const defaultFilters = [ diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 1d869fbfc..f9f8d5cb7 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -1,4 +1,13 @@ -import { DeepPartial, SelectQueryBuilder } from 'typeorm' +import { + Between, + DeepPartial, + In, + IsNull, + LessThan, + MoreThan, + Not, + SelectQueryBuilder, +} from 'typeorm' import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity' import { EntityLabel } from '../entity/entity_label' import { Highlight } from '../entity/highlight' @@ -106,10 +115,14 @@ const buildWhereClause = ( if (args.inFilter !== InFilter.ALL) { switch (args.inFilter) { case InFilter.INBOX: - queryBuilder.andWhere('library_item.archived_at IS NULL') + queryBuilder.andWhere({ + archivedAt: IsNull(), + }) break case InFilter.ARCHIVE: - queryBuilder.andWhere('library_item.archived_at IS NOT NULL') + queryBuilder.andWhere({ + archivedAt: Not(IsNull()), + }) break case InFilter.TRASH: // return only deleted pages within 14 days @@ -119,16 +132,20 @@ const buildWhereClause = ( break case InFilter.SUBSCRIPTION: queryBuilder - .andWhere('library_item.subscription IS NOT NULL') .andWhere("NOT ('library' ILIKE ANY (library_item.label_names))") - .andWhere('library_item.archived_at IS NULL') + .andWhere({ + subscription: Not(IsNull()), + archivedAt: IsNull(), + }) break case InFilter.LIBRARY: queryBuilder .andWhere( "(library_item.subscription IS NULL OR 'library' ILIKE ANY (library_item.label_names))" ) - .andWhere('library_item.archived_at IS NULL') + .andWhere({ + archivedAt: IsNull(), + }) break } } @@ -136,10 +153,17 @@ const buildWhereClause = ( if (args.readFilter !== ReadFilter.ALL) { switch (args.readFilter) { case ReadFilter.READ: - queryBuilder.andWhere('library_item.reading_progress_top_percent >= 98') + queryBuilder.andWhere({ + readingProgressBottomPercent: MoreThan(98), + }) + break + case ReadFilter.READING: + queryBuilder.andWhere({ readingProgressBottomPercent: Between(2, 98) }) break case ReadFilter.UNREAD: - queryBuilder.andWhere('library_item.reading_progress_top_percent < 98') + queryBuilder.andWhere({ + readingProgressBottomPercent: LessThan(2), + }) break } } @@ -148,12 +172,10 @@ const buildWhereClause = ( args.hasFilters.forEach((filter) => { switch (filter) { case HasFilter.HIGHLIGHTS: - queryBuilder.andWhere( - 'array_length(library_item.highlight_annotations, 1) > 0' - ) + queryBuilder.andWhere("library_item.highlight_annotations <> '{}'") break case HasFilter.LABELS: - queryBuilder.andWhere('array_length(library_item.label_names, 1) > 0') + queryBuilder.andWhere("library_item.label_names <> '{}'") break } }) @@ -225,18 +247,20 @@ const buildWhereClause = ( } if (args.ids && args.ids.length > 0) { - queryBuilder.andWhere('library_item.id IN (:...ids)', { ids: args.ids }) + queryBuilder.andWhere({ + id: In(args.ids), + }) } if (!args.includePending) { - queryBuilder.andWhere('library_item.state != :state', { - state: LibraryItemState.Processing, + queryBuilder.andWhere({ + state: Not(LibraryItemState.Processing), }) } if (!args.includeDeleted && args.inFilter !== InFilter.TRASH) { - queryBuilder.andWhere('library_item.state != :state', { - state: LibraryItemState.Deleted, + queryBuilder.andWhere({ + state: Not(LibraryItemState.Deleted), }) } @@ -303,7 +327,7 @@ export const searchLibraryItems = async ( const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') .select(selectColumns) - .where('library_item.user_id = :userId', { userId }) + .where({ user: { id: userId } }) // build the where clause buildWhereClause(queryBuilder, args) diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index f5d9e9b6c..c3af64881 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -14,6 +14,7 @@ import { InputMaybe, PageType, SortParams } from '../generated/graphql' export enum ReadFilter { ALL, READ, + READING, UNREAD, } @@ -110,6 +111,8 @@ const parseIsFilter = (str: string | undefined): ReadFilter => { switch (str?.toUpperCase()) { case 'READ': return ReadFilter.READ + case 'READING': + return ReadFilter.READING case 'UNREAD': return ReadFilter.UNREAD } diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 2959a2072..85b788f90 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -818,6 +818,7 @@ describe('Article API', () => { let keyword = '' before(async () => { + const readingProgressArray = [0, 2, 97, 98, 100] // Create some test items for (let i = 0; i < 5; i++) { const itemToSave: DeepPartial = { @@ -827,6 +828,7 @@ describe('Article API', () => { slug: 'test slug', originalUrl: `${url}/${i}`, siteName: 'Example', + readingProgressBottomPercent: readingProgressArray[i], } const item = await createLibraryItem(itemToSave, user.id) items.push(item) @@ -887,15 +889,39 @@ describe('Article API', () => { keyword = `'${searchedKeyword}' is:unread` }) - it('should return unread articles in descending order', async () => { + it('returns unread articles in descending order', async () => { const res = await graphqlRequest(query, authToken).expect(200) - expect(res.body.data.search.edges.length).to.eq(5) + expect(res.body.data.search.edges.length).to.eq(1) + expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) + }) + }) + + context('when is:reading is in the query', () => { + before(() => { + keyword = `'${searchedKeyword}' is:reading` + }) + + it('returns reading articles in descending order', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.edges.length).to.eq(3) + expect(res.body.data.search.edges[0].node.id).to.eq(items[3].id) + expect(res.body.data.search.edges[1].node.id).to.eq(items[2].id) + expect(res.body.data.search.edges[2].node.id).to.eq(items[1].id) + }) + }) + + context('when is:read is in the query', () => { + before(() => { + keyword = `'${searchedKeyword}' is:read` + }) + + it('returns fully read articles in descending order', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.edges.length).to.eq(1) expect(res.body.data.search.edges[0].node.id).to.eq(items[4].id) - expect(res.body.data.search.edges[1].node.id).to.eq(items[3].id) - expect(res.body.data.search.edges[2].node.id).to.eq(items[2].id) - expect(res.body.data.search.edges[3].node.id).to.eq(items[1].id) - expect(res.body.data.search.edges[4].node.id).to.eq(items[0].id) }) }) @@ -1122,7 +1148,7 @@ describe('Article API', () => { slug: 'test slug 2', originalUrl: `${url}/test2`, archivedAt: new Date(), - readingProgressTopPercent: 100, + readingProgressBottomPercent: 100, }, { user, @@ -1140,7 +1166,7 @@ describe('Article API', () => { await deleteLibraryItems(items, user.id) }) - it('returns unfinished archived items', async () => { + it('returns unread archived items', async () => { const res = await graphqlRequest(query, authToken).expect(200) expect(res.body.data.search.pageInfo.totalCount).to.eq(1) @@ -1221,7 +1247,7 @@ describe('Article API', () => { slug: 'test slug 2', originalUrl: `${url}/test2`, deletedAt: new Date(), - readingProgressTopPercent: 100, + readingProgressBottomPercent: 100, }, { user, From 62ffaa924bd8b3873d01fbc7731fb7d906f91eb9 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 10 Oct 2023 12:52:49 +0800 Subject: [PATCH 04/15] add wordsCount filter --- packages/api/src/services/library_item.ts | 2 +- packages/api/src/utils/search.ts | 11 ++- packages/api/test/resolvers/article.test.ts | 98 +++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index f9f8d5cb7..0fcf6940e 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -293,7 +293,7 @@ const buildWhereClause = ( args.rangeFilters.forEach((filter, i) => { const param = `range_${filter.field}_${i}` queryBuilder.andWhere( - `library_item.${filter.field} ${filter.operator} ${param}`, + `library_item.${filter.field} ${filter.operator} :${param}`, { [param]: filter.value, } diff --git a/packages/api/src/utils/search.ts b/packages/api/src/utils/search.ts index c3af64881..357875449 100644 --- a/packages/api/src/utils/search.ts +++ b/packages/api/src/utils/search.ts @@ -275,7 +275,10 @@ const parseRangeFilter = ( switch (field.toUpperCase()) { case 'WORDSCOUNT': - field = 'wordCount' + field = 'word_count' + break + case 'READPOSITION': + field = 'reading_progress_bottom_percent' break default: return undefined @@ -410,7 +413,8 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { 'site', 'note', 'rss', - 'wordCount', + 'wordsCount', + 'readPosition', ], tokenize: true, }) @@ -507,7 +511,8 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => { case 'mode': // mode is ignored and used only by the frontend break - case 'wordCount': { + case 'readPosition': + case 'wordsCount': { const rangeFilter = parseRangeFilter(keyword.keyword, keyword.value) rangeFilter && result.rangeFilters.push(rangeFilter) break diff --git a/packages/api/test/resolvers/article.test.ts b/packages/api/test/resolvers/article.test.ts index 85b788f90..d8c2545d9 100644 --- a/packages/api/test/resolvers/article.test.ts +++ b/packages/api/test/resolvers/article.test.ts @@ -1272,6 +1272,104 @@ describe('Article API', () => { expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) }) }) + + context('when readPosition:>20 readPosition:<50 is in the query', () => { + let items: LibraryItem[] = [] + + before(async () => { + keyword = 'readPosition:>20 readPosition:<50' + // Create some test items + items = await createLibraryItems( + [ + { + user, + title: 'test title 1', + readableContent: '

test 1

', + slug: 'test slug 1', + originalUrl: `${url}/test1`, + readingProgressBottomPercent: 40, + }, + { + user, + title: 'test title 2', + readableContent: '

test 2

', + slug: 'test slug 2', + originalUrl: `${url}/test2`, + readingProgressBottomPercent: 10, + }, + { + user, + title: 'test title 3', + readableContent: '

test 3

', + slug: 'test slug 3', + originalUrl: `${url}/test3`, + readingProgressBottomPercent: 100, + }, + ], + user.id + ) + }) + + after(async () => { + await deleteLibraryItems(items, user.id) + }) + + it('returns items with reading progress between 20% and 50% exclusively', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.pageInfo.totalCount).to.eq(1) + expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) + }) + }) + + context('when wordsCount:>=10000 wordsCount:<=20000 is in the query', () => { + let items: LibraryItem[] = [] + + before(async () => { + keyword = 'wordsCount:>=10000 wordsCount:<=20000' + // Create some test items + items = await createLibraryItems( + [ + { + user, + title: 'test title 1', + readableContent: '

test 1

', + slug: 'test slug 1', + originalUrl: `${url}/test1`, + wordCount: 10000, + }, + { + user, + title: 'test title 2', + readableContent: '

test 2

', + slug: 'test slug 2', + originalUrl: `${url}/test2`, + wordCount: 8000, + }, + { + user, + title: 'test title 3', + readableContent: '

test 3

', + slug: 'test slug 3', + originalUrl: `${url}/test3`, + wordCount: 100000, + }, + ], + user.id + ) + }) + + after(async () => { + await deleteLibraryItems(items, user.id) + }) + + it('returns items with words count between 10000 and 20000 inclusively', async () => { + const res = await graphqlRequest(query, authToken).expect(200) + + expect(res.body.data.search.pageInfo.totalCount).to.eq(1) + expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id) + }) + }) }) describe('TypeaheadSearch API', () => { From 768845e101ce957438deaccf579f1dbe26a4f939 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 10 Oct 2023 15:33:59 +0800 Subject: [PATCH 05/15] update the filter value to use is:reading of the Continue Reading filter --- packages/api/src/services/create_user.ts | 5 ++++- .../0130.do.update_continue_reading_filter.sql | 11 +++++++++++ .../0130.undo.update_continue_reading_filter.sql | 11 +++++++++++ .../components/templates/homeFeed/EmptyLibrary.tsx | 2 +- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100755 packages/db/migrations/0130.do.update_continue_reading_filter.sql create mode 100755 packages/db/migrations/0130.undo.update_continue_reading_filter.sql diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 6ded934b4..6fd8e2033 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -155,7 +155,10 @@ const createDefaultFiltersForUser = async (userId: string): Promise => { const defaultFilters = [ { name: 'Inbox', filter: 'in:inbox' }, - { name: 'Continue Reading', filter: 'in:inbox sort:read-desc is:unread' }, + { + name: 'Continue Reading', + filter: 'in:inbox sort:read-desc is:reading', + }, { name: 'Non-Feed Items', filter: 'in:library' }, { name: 'Highlights', filter: 'has:highlights mode:highlights' }, { name: 'Unlabeled', filter: 'no:label' }, diff --git a/packages/db/migrations/0130.do.update_continue_reading_filter.sql b/packages/db/migrations/0130.do.update_continue_reading_filter.sql new file mode 100755 index 000000000..c3e4b21cd --- /dev/null +++ b/packages/db/migrations/0130.do.update_continue_reading_filter.sql @@ -0,0 +1,11 @@ +-- Type: DO +-- Name: update_continue_reading_filter +-- Description: Update the filter value in the Continue Reading filter + +BEGIN; + +UPDATE omnivore.filters + SET filter = 'in:inbox sort:read-desc is:reading' + WHERE name = 'Continue Reading' AND default_filter = true; + +COMMIT; diff --git a/packages/db/migrations/0130.undo.update_continue_reading_filter.sql b/packages/db/migrations/0130.undo.update_continue_reading_filter.sql new file mode 100755 index 000000000..288485f0e --- /dev/null +++ b/packages/db/migrations/0130.undo.update_continue_reading_filter.sql @@ -0,0 +1,11 @@ +-- Type: UNDO +-- Name: update_continue_reading_filter +-- Description: Update the filter value in the Continue Reading filter + +BEGIN; + +UPDATE omnivore.filters + SET filter = 'in:inbox sort:read-desc is:unread' + WHERE name = 'Continue Reading' AND default_filter = true; + +COMMIT; diff --git a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx index 04b6df2e0..72da7891b 100644 --- a/packages/web/components/templates/homeFeed/EmptyLibrary.tsx +++ b/packages/web/components/templates/homeFeed/EmptyLibrary.tsx @@ -167,7 +167,7 @@ export const EmptyLibrary = (props: EmptyLibraryProps) => { switch (props.searchTerm) { case 'in:inbox': return 'inbox' - case 'in:inbox sort:read-desc is:unread': + case 'in:inbox sort:read-desc is:reading': return 'continue' case 'in:library': return 'non-feed' From 453000286c5664516570a82d7e5b3498b1c4b4d1 Mon Sep 17 00:00:00 2001 From: Remy Chantenay Date: Tue, 10 Oct 2023 09:26:37 +0200 Subject: [PATCH 06/15] iOS: Add length indicator to new label textfield --- .../Sources/App/Views/Labels/LabelsView.swift | 10 +++++++++- .../Sources/App/Views/Labels/LabelsViewModel.swift | 2 ++ apple/OmnivoreKit/Sources/Views/LocalText.swift | 1 + .../Views/Resources/en.lproj/Localizable.strings | 1 + .../Views/Resources/es.lproj/Localizable.strings | 1 + .../Views/Resources/zh-Hans.lproj/Localizable.strings | 1 + 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index 7473d8b25..96c859699 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -116,8 +116,16 @@ struct CreateLabelView: View { } .padding(.bottom, 8) - TextField("Label Name", text: $newLabelName) + TextField(LocalText.labelNamePlaceholder, text: $newLabelName) .textFieldStyle(StandardTextFieldStyle()) + .onChange(of: newLabelName) { inputLabelName in + newLabelName = String(inputLabelName.prefix(viewModel.labelNameMaxLength)) + } + + Text("\(newLabelName.count)/\(viewModel.labelNameMaxLength)") + .font(.caption) + .frame(maxWidth: .infinity, alignment: .trailing) + .foregroundColor(newLabelName.count < viewModel.labelNameMaxLength ? .gray : .red) ScrollView(.horizontal, showsIndicators: false) { LazyHGrid(rows: rows, alignment: .top, spacing: 20) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 10525fa35..50b11ca6a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -5,6 +5,8 @@ import SwiftUI import Views @MainActor final class LabelsViewModel: ObservableObject { + let labelNameMaxLength = 64 + @Published var isLoading = false @Published var selectedLabels = Set() @Published var unselectedLabels = Set() diff --git a/apple/OmnivoreKit/Sources/Views/LocalText.swift b/apple/OmnivoreKit/Sources/Views/LocalText.swift index 13a5ba82c..2fc984a17 100644 --- a/apple/OmnivoreKit/Sources/Views/LocalText.swift +++ b/apple/OmnivoreKit/Sources/Views/LocalText.swift @@ -26,6 +26,7 @@ public enum LocalText { public static let labelsViewAssignNameColor = localText(key: "labelsViewAssignNameColor") public static let createLabelMessage = localText(key: "createLabelMessage") public static let labelsPurposeDescription = localText(key: "labelsPurposeDescription") + public static let labelNamePlaceholder = localText(key: "labelNamePlaceholder") // Manage Account View public static let manageAccountDelete = localText(key: "manageAccountDelete") diff --git a/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings b/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings index 127dd0811..d69e034ce 100644 --- a/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings +++ b/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings @@ -19,6 +19,7 @@ "labelsViewAssignNameColor" = "Assign a name and color."; "createLabelMessage" = "Create a new Label"; "labelsPurposeDescription" = "Use labels to create curated collections of links."; +"labelNamePlaceholder" = "Label Name"; // Manage Account View "manageAccountDelete" = "Delete Account"; diff --git a/apple/OmnivoreKit/Sources/Views/Resources/es.lproj/Localizable.strings b/apple/OmnivoreKit/Sources/Views/Resources/es.lproj/Localizable.strings index 848d343f5..dff64a42a 100644 --- a/apple/OmnivoreKit/Sources/Views/Resources/es.lproj/Localizable.strings +++ b/apple/OmnivoreKit/Sources/Views/Resources/es.lproj/Localizable.strings @@ -19,6 +19,7 @@ "labelsViewAssignNameColor" = "Asigna un nombre y un color."; "createLabelMessage" = "Crea una nueva Etiqueta"; "labelsPurposeDescription" = "Usa Etiquetas para coleccione de enlaces organizadas."; +"labelNamePlaceholder" = "Nombre de la etiqueta"; // Manage Account View "manageAccountDelete" = "Borrar Cuenta"; diff --git a/apple/OmnivoreKit/Sources/Views/Resources/zh-Hans.lproj/Localizable.strings b/apple/OmnivoreKit/Sources/Views/Resources/zh-Hans.lproj/Localizable.strings index f8a5ca95c..7cee43c8d 100644 --- a/apple/OmnivoreKit/Sources/Views/Resources/zh-Hans.lproj/Localizable.strings +++ b/apple/OmnivoreKit/Sources/Views/Resources/zh-Hans.lproj/Localizable.strings @@ -20,6 +20,7 @@ "labelsViewAssignNameColor" = "指定名称和颜色。"; "createLabelMessage" = "创建新标签"; "labelsPurposeDescription" = "使用标签创建精选的链接集合。"; +"labelNamePlaceholder" = "标签名称"; // Manage Account View "manageAccountDelete" = "删除帐户"; From dc14743fb86194bfed62e68220d4bb34a961dd92 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Tue, 10 Oct 2023 22:26:02 +0800 Subject: [PATCH 07/15] remove data assertion from migration script to speed up --- packages/db/elastic_migrations/migrate_from_elastic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/db/elastic_migrations/migrate_from_elastic.py b/packages/db/elastic_migrations/migrate_from_elastic.py index 98bc1a6aa..a27179ddb 100755 --- a/packages/db/elastic_migrations/migrate_from_elastic.py +++ b/packages/db/elastic_migrations/migrate_from_elastic.py @@ -313,7 +313,7 @@ async def main(): ES_USERNAME, ES_PASSWORD), retry_on_timeout=True) try: - updated_user_ids = [] + # updated_user_ids = [] print(await es_client.info()) @@ -454,8 +454,8 @@ async def main(): library_items.append(library_item) library_items_original_ids.append(doc_id) - if user_id not in updated_user_ids: - updated_user_ids.append(user_id) + # if user_id not in updated_user_ids: + # updated_user_ids.append(user_id) # convert labels to postgres format if 'labels' in source: @@ -551,7 +551,7 @@ async def main(): print('Migration complete', END_TIME) - await assert_data(db_conn, es_client, updated_user_ids, uploaded_files) + # await assert_data(db_conn, es_client, updated_user_ids, uploaded_files) except Exception as err: print('Migration error', err) finally: From caa596994015574a01ed1634f300d527ee699c4c Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 11 Oct 2023 14:44:39 +0800 Subject: [PATCH 08/15] fix updated subscription always show failed in the rss handler logs --- packages/rss-handler/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rss-handler/src/index.ts b/packages/rss-handler/src/index.ts index 2440ef520..9c37e22f9 100644 --- a/packages/rss-handler/src/index.ts +++ b/packages/rss-handler/src/index.ts @@ -70,7 +70,7 @@ const sendUpdateSubscriptionMutation = async ( ) /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - return !!response.data.data.savePage + return !!response.data.data.updateSubscription.subscription } catch (error) { if (axios.isAxiosError(error)) { console.error('update subscription mutation error', error.message) From a4aeb4027609fb6809b0fe7c5aac98676ade282b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 11 Oct 2023 14:48:44 +0800 Subject: [PATCH 09/15] fix: after successfully deleted highlights, still return error codes --- packages/api/src/services/highlights.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/api/src/services/highlights.ts b/packages/api/src/services/highlights.ts index 07cf73eba..4f281d3df 100644 --- a/packages/api/src/services/highlights.ts +++ b/packages/api/src/services/highlights.ts @@ -136,8 +136,11 @@ export const updateHighlight = async ( export const deleteHighlightById = async (highlightId: string) => { return authTrx(async (tx) => { const highlightRepo = tx.withRepository(highlightRepository) - const highlight = await highlightRepo.findOneByOrFail({ - id: highlightId, + const highlight = await highlightRepo.findOneOrFail({ + where: { id: highlightId }, + relations: { + user: true, + }, }) await highlightRepo.delete(highlightId) From 30ba22cbbc2a78fc180df9e3b8038dcaa4ea6d16 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 11 Oct 2023 16:05:35 +0800 Subject: [PATCH 10/15] add user_id to the sql query to reduce query latency --- packages/api/src/resolvers/article/index.ts | 4 ++-- packages/api/src/routers/svc/content.ts | 3 ++- packages/api/src/services/library_item.ts | 22 +++++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/api/src/resolvers/article/index.ts b/packages/api/src/resolvers/article/index.ts index aa57c1543..1c191d0ec 100644 --- a/packages/api/src/resolvers/article/index.ts +++ b/packages/api/src/resolvers/article/index.ts @@ -707,9 +707,9 @@ export const typeaheadSearchResolver = authorized< TypeaheadSearchSuccess, TypeaheadSearchError, QueryTypeaheadSearchArgs ->(async (_obj, { query, first }, { log }) => { +>(async (_obj, { query, first }, { log, uid }) => { try { - const items = await findLibraryItemsByPrefix(query, first || undefined) + const items = await findLibraryItemsByPrefix(query, uid, first || undefined) return { items: items.map((item) => ({ diff --git a/packages/api/src/routers/svc/content.ts b/packages/api/src/routers/svc/content.ts index aed246711..ce184ea5b 100644 --- a/packages/api/src/routers/svc/content.ts +++ b/packages/api/src/routers/svc/content.ts @@ -71,7 +71,8 @@ export function contentServiceRouter() { .withRepository(libraryItemRepository) .createQueryBuilder('item') .innerJoinAndSelect('item.uploadFile', 'file') - .where('file.id = :fileId', { fileId }) + .where('item.user = :userId', { userId: uploadFile.user.id }) + .andWhere('file.id = :fileId', { fileId }) .getOne(), undefined, uploadFile.user.id diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 0fcf6940e..49a363acf 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -379,7 +379,8 @@ export const findLibraryItemByUrl = async ( .leftJoinAndSelect('recommendations.recommender', 'recommender') .leftJoinAndSelect('recommender.profile', 'profile') .leftJoinAndSelect('recommendations.group', 'group') - .where('library_item.original_url = :url', { url }) + .where('library_item.user_id = :userId', { userId }) + .andWhere('library_item.original_url = :url', { url }) .getOne(), undefined, userId @@ -485,6 +486,7 @@ export const createLibraryItem = async ( export const findLibraryItemsByPrefix = async ( prefix: string, + userId: string, limit = 5 ): Promise => { const prefixWildcard = `${prefix}%` @@ -492,10 +494,11 @@ export const findLibraryItemsByPrefix = async ( return authTrx(async (tx) => tx .createQueryBuilder(LibraryItem, 'library_item') - .where('library_item.title ILIKE :prefix', { prefix: prefixWildcard }) - .orWhere('library_item.site_name ILIKE :prefix', { - prefix: prefixWildcard, - }) + .where('library_item.user_id = :userId', { userId }) + .andWhere( + '(library_item.title ILIKE :prefix OR library_item.site_name ILIKE :prefix)', + { prefix: prefixWildcard } + ) .orderBy('library_item.savedAt', 'DESC') .limit(limit) .getMany() @@ -511,7 +514,8 @@ export const countByCreatedAt = async ( async (tx) => tx .createQueryBuilder(LibraryItem, 'library_item') - .where('library_item.created_at between :startDate and :endDate', { + .where('library_item.user_id = :userId', { userId }) + .andWhere('library_item.created_at between :startDate and :endDate', { startDate, endDate, }) @@ -608,10 +612,12 @@ export const deleteLibraryItems = async ( ) } -export const deleteLibraryItemByUrl = async (url: string, userId?: string) => { +export const deleteLibraryItemByUrl = async (url: string, userId: string) => { return authTrx( async (tx) => - tx.withRepository(libraryItemRepository).delete({ originalUrl: url }), + tx + .withRepository(libraryItemRepository) + .delete({ originalUrl: url, user: { id: userId } }), undefined, userId ) From 0c612c27e7dd733f18687a266beb1718b5d3dbb9 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 11 Oct 2023 17:05:19 +0800 Subject: [PATCH 11/15] add default value = now() to updatedAt field in labels, user_profile and highlight tables --- ...131.do.add_default_value_to_updated_at.sql | 22 +++++++++++++++++++ ...1.undo.add_default_value_to_updated_at.sql | 13 +++++++++++ 2 files changed, 35 insertions(+) create mode 100755 packages/db/migrations/0131.do.add_default_value_to_updated_at.sql create mode 100755 packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql diff --git a/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql new file mode 100755 index 000000000..2d856811d --- /dev/null +++ b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql @@ -0,0 +1,22 @@ +-- Type: DO +-- Name: add_default_value_to_updated_at +-- Description: Add default = now() to updated_at field in profile, labels and highlight table + +BEGIN; + +UPDATE omnivore.user_profile SET updated_at = created_at WHERE updated_at IS NULL; +ALTER TABLE omnivore.user_profile + ALTER COLUMN updated_at SET DEFAULT current_timestamp, + ALTER COLUMN updated_at SET NOT NULL; + +UPDATE omnivore.labels SET updated_at = created_at WHERE updated_at IS NULL; +ALTER TABLE omnivore.labels + ALTER COLUMN updated_at SET DEFAULT current_timestamp, + ALTER COLUMN updated_at SET NOT NULL; + +UPDATE omnivore.highlight SET updated_at = created_at WHERE updated_at IS NULL; +ALTER TABLE omnivore.highlight + ALTER COLUMN updated_at SET DEFAULT current_timestamp, + ALTER COLUMN updated_at SET NOT NULL; + +COMMIT; diff --git a/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql b/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql new file mode 100755 index 000000000..6c520c03e --- /dev/null +++ b/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql @@ -0,0 +1,13 @@ +-- Type: UNDO +-- Name: add_default_value_to_updated_at +-- Description: Add default = now() to updated_at field in profile, labels and highlight table + +BEGIN; + +ALTER TABLE omnivore.user_profile ALTER COLUMN updated_at DROP NOT NULL; + +ALTER TABLE omnivore.labels ALTER COLUMN updated_at DROP NOT NULL; + +ALTER TABLE omnivore.highlight ALTER COLUMN updated_at DROP NOT NULL; + +COMMIT; From 3201f96d43d4c127a0044f297bd36208e0a96b7b Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 11 Oct 2023 18:41:10 +0800 Subject: [PATCH 12/15] add default value = now() to updatedAt field in highlight tables only --- .../0131.do.add_default_value_to_updated_at.sql | 10 ---------- .../0131.undo.add_default_value_to_updated_at.sql | 4 ---- 2 files changed, 14 deletions(-) diff --git a/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql index 2d856811d..a5a4ef915 100755 --- a/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql +++ b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql @@ -4,16 +4,6 @@ BEGIN; -UPDATE omnivore.user_profile SET updated_at = created_at WHERE updated_at IS NULL; -ALTER TABLE omnivore.user_profile - ALTER COLUMN updated_at SET DEFAULT current_timestamp, - ALTER COLUMN updated_at SET NOT NULL; - -UPDATE omnivore.labels SET updated_at = created_at WHERE updated_at IS NULL; -ALTER TABLE omnivore.labels - ALTER COLUMN updated_at SET DEFAULT current_timestamp, - ALTER COLUMN updated_at SET NOT NULL; - UPDATE omnivore.highlight SET updated_at = created_at WHERE updated_at IS NULL; ALTER TABLE omnivore.highlight ALTER COLUMN updated_at SET DEFAULT current_timestamp, diff --git a/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql b/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql index 6c520c03e..96cf9e21f 100755 --- a/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql +++ b/packages/db/migrations/0131.undo.add_default_value_to_updated_at.sql @@ -4,10 +4,6 @@ BEGIN; -ALTER TABLE omnivore.user_profile ALTER COLUMN updated_at DROP NOT NULL; - -ALTER TABLE omnivore.labels ALTER COLUMN updated_at DROP NOT NULL; - ALTER TABLE omnivore.highlight ALTER COLUMN updated_at DROP NOT NULL; COMMIT; From 66d93418cd58c5101c984b5bc575672bafcebf14 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Oct 2023 19:21:54 +0800 Subject: [PATCH 13/15] Bump the iOS release --- apple/Omnivore.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index a61592edc..32ee2a7a5 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1400,7 +1400,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.33.0; + MARKETING_VERSION = 1.34.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1435,7 +1435,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.33.0; + MARKETING_VERSION = 1.34.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1490,7 +1490,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.33.0; + MARKETING_VERSION = 1.34.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1831,7 +1831,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.33.0; + MARKETING_VERSION = 1.34.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; From 76b382966d99ee564a713f0adf149c2be76b8435 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Oct 2023 19:27:03 +0800 Subject: [PATCH 14/15] Make highlight modification two steps --- .../0131.do.add_default_value_to_updated_at.sql | 3 +-- .../db/migrations/0132.do.add_highlight_not_null.sql | 11 +++++++++++ .../migrations/0132.undo.add_highlight_not_null.sql | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100755 packages/db/migrations/0132.do.add_highlight_not_null.sql create mode 100755 packages/db/migrations/0132.undo.add_highlight_not_null.sql diff --git a/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql index a5a4ef915..32fdd4a13 100755 --- a/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql +++ b/packages/db/migrations/0131.do.add_default_value_to_updated_at.sql @@ -6,7 +6,6 @@ BEGIN; UPDATE omnivore.highlight SET updated_at = created_at WHERE updated_at IS NULL; ALTER TABLE omnivore.highlight - ALTER COLUMN updated_at SET DEFAULT current_timestamp, - ALTER COLUMN updated_at SET NOT NULL; + ALTER COLUMN updated_at SET DEFAULT current_timestamp; COMMIT; diff --git a/packages/db/migrations/0132.do.add_highlight_not_null.sql b/packages/db/migrations/0132.do.add_highlight_not_null.sql new file mode 100755 index 000000000..47ef70a80 --- /dev/null +++ b/packages/db/migrations/0132.do.add_highlight_not_null.sql @@ -0,0 +1,11 @@ +-- Type: DO +-- Name: add_highlight_not_null +-- Description: Add a not null clause to the highlight updated_at column + +BEGIN; + +ALTER TABLE omnivore.highlight + ALTER COLUMN updated_at SET NOT NULL ; + + +COMMIT; diff --git a/packages/db/migrations/0132.undo.add_highlight_not_null.sql b/packages/db/migrations/0132.undo.add_highlight_not_null.sql new file mode 100755 index 000000000..72c6447f0 --- /dev/null +++ b/packages/db/migrations/0132.undo.add_highlight_not_null.sql @@ -0,0 +1,10 @@ +-- Type: UNDO +-- Name: add_highlight_not_null +-- Description: Add a not null clause to the highlight updated_at column + +BEGIN; + +ALTER TABLE omnivore.highlight + ALTER COLUMN updated_at DROP NOT NULL; + +COMMIT; From 3a50f14e527f149845db531678d70f670c01db3e Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 11 Oct 2023 23:57:48 +0800 Subject: [PATCH 15/15] Use sorted subscriptions in left menu --- .../web/components/templates/homeFeed/LibraryFilterMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx index ff113ea3d..11adec862 100644 --- a/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx +++ b/packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx @@ -223,7 +223,7 @@ function Subscriptions( }, } }), - [props.subscriptions] + [sortedSubscriptions] ) return ( @@ -241,7 +241,7 @@ function Subscriptions( text="Newsletters" {...props} /> - {(props.subscriptions ?? []).map((item) => { + {(sortedSubscriptions ?? []).map((item) => { switch (item.type) { case SubscriptionType.NEWSLETTER: return (