Merge pull request #3062 from omnivore-app/main

Web production deployment
This commit is contained in:
Jackson Harper 2023-11-02 11:53:13 +08:00 committed by GitHub
commit 5a0014e4a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 395 additions and 199 deletions

View file

@ -0,0 +1,161 @@
# Omnivore'a Başlarken
Omnivore, çevrimiçi okuduğunuz her şeyi kaydetmenizi ve düzenlemenizi sağlayan bir **daha sonra oku** uygulamasıdır.
Bu kılavuz, size Omnivore'u nasıl kullanacağınızı temel işlevleri ve gelişmiş özellikleri dört parçaya bölerek anlatır:
- Kaydetme
- Okuma
- Organizasyon
- Entegrasyonlar
**Kütüphane**niz kaydettiğiniz tüm bağlantılara hızlı bir şekilde erişebildiğiniz Omnivore deneyiminizin merkezidir. Kaydedilen bağlantılar, siz silmediğiniz sürece sonsuza kadar kitaplığınızda saklanır.
## Kaydetme
Daha sonra okumak istediğiniz sayfaların veya makalelerin bağlantılarını kaydetmenin beş ana yolu vardır:
- Omnivore Kitaplığınızdan Kaydetme
- Tarayıcıdan Kaydetme
- Telefon veya Tabletten Kaydetme (iOS veya Android)
- E-posta ile Bülten Abonelikleri
- Mac bir cihazdan PDF kaydetme
### Omnivore Kütüphanenizden Kaydetme
1. Kitaplığınızın sağ üst köşesindeki **Bağlantı Ekle** düğmesine dokunun.
2. Kaydetmek istediğiniz URL'i girin ve **Bağlantı Ekle** düğmesine dokunun.
3. Bağlantı, bir sonraki yenilemenizde kitaplığınızda görünecektir.
### Tarayıcıdan Kaydetme
1. Tarayıcınız için Omnivore uzantısını indirin ve yükleyin:
- [Chrome](https://omnivore.app/install/chrome)
- [Edge](https://omnivore.app/install/edge)
- [Firefox](https://omnivore.app/install/firefox)
- [Safari](https://omnivore.app/install/safari)
2. Kaydetmek istediğiniz sayfaya gidin ve tarayıcınızın araç çubuğundaki veya Uzantılar menüsündeki Omnivore düğmesine dokunun.
3. Alternatif olarak, herhangi bir linke sağ tıklayabilir (Mac'te command+tıklama) ve menüden **Save to Omnivore** seçeneğine tıklayabilirsiniz.
4. Bağlantı, bir sonraki yenilemenizde kitaplığınızda görünecektir.
### Telefon veya Tabletten Kaydetme
Mobil cihazınızdan bağlantıları kaydetmenin en iyi yolu Omnivore uygulamasıdır. Uygulamayı buradan indirebilirsiniz:
- [iOS (iPhone veya iPad)](https://omnivore.app/install/ios)
- Android
Mobil uygulama yüklendikten sonra:
1. Tarayıcınızda, kaydetmek istediğiniz sayfaya gidin ve **Paylaş** düğmesine dokunun.
2. Paylaş menüsündeki **Omnivore** simgesine dokunun.
3. Bağlantı, bir sonraki yenilemenizde kitaplığınızda görünecektir.
### E-posta ile Bülten Abonelikleri
1. Omnivore web sitesinde veya uygulamasında, profil menüsüne erişmek için sağ üst köşedeki fotoğrafınıza, baş harfinize veya avatarınıza dokunun. Menüden **E-postalar** öğesini seçin.
2. Listeye yeni bir e-posta adresi (örn. username-123_abc@inbox.omnivore.app) eklemek için **Yeni E-posta Adresi Oluştur** öğesine dokunun.
3. E-posta adresinin yanındaki kopyala simgesine tıklayın.
4. Abone olmak istediğiniz haber bülteninin kayıt sayfasına gidin.
5. Omnivore e-posta adresini kayıt formuna yapıştırın.
6. Yeni bültenler otomatik olarak Omnivore gelen kutunuza gönderilecektir.
### Mac bir cihazdan PDF kaydetme
1. [Mac uygulaması](https://omnivore.app/install/mac)nı kurun.
2. Mac'inizde kaydetmek istediğiniz dosyanın ismine sağ tıklayın veya ctrl+tıklayın.
3. Menüden **Paylaş**'a tıklayın ve **Omnivore**'u seçin.
4. Bağlantı, bir sonraki yenilemenizde kitaplığınızda görünecektir.
## Okuma
Kütüphanenizde kaydedilmiş herhangi bir bağlantıya tıklayarak Okuyucu görünümüne girebilirsiniz.
Omnivore, dikkatinizi dağıtmadan okumayı kolaylaştırmak için sayfaları kolay okunur ve vurgulanabilir bir biçime dönüştürür, reklamları ve gereksiz içerikleri kaldırır. Metin odaklı görünüm, aynı zamanda makaleleri daha küçük hale getirir ve daha hızlı yüklenmelerini sağlar.
Okurken şunları yapabilirsiniz:
- <span style="text-decoration:underline;">Biçimi Değiştir</span>
- <span style="text-decoration:underline;">Metni Vurgula</span>
- <span style="text-decoration:underline;">Not Ekle</span>
- <span style="text-decoration:underline;">Tüm Kaydedilmiş Vurguları ve Notları Görüntüle</span>
- <span style="text-decoration:underline;">Okuma İlerlemenizi Takip Et</span>
### Biçimi Değiştir
1. **_Tema:_** Profil menüsüne erişmek için sağ üst köşede fotoğrafınıza, baş harfinize veya avatarınıza dokunun. Aydınlık veya karanlık temayı seçmek için beyaz veya siyah simgeyi seçin.
2. **_Metin Biçimi:_** Metin boyutunu, yazı tipini, kenar boşluklarını ve satır aralığını ayarlamak için Aa simgesine dokunun.
### Metni Vurgula
1. Vurgulamak istediğiniz metni seçin.
2. **Vurgula** düğmesine dokunun.
3. Metin, makaleyi bir sonraki görüntülediğinizde vurgulanmış olarak görünecektir.
### Not Ekle
1. Bir not eklemek istediğiniz metin bölümünü vurgulayın.
2. **Not** düğmesine dokunun, notunuzu yazın ve **Kaydet** düğmesine dokunun.
3. Not simgesi, bu makaleyi bir sonraki görüntülemenizde görünecektir.
### Tüm Kaydedilmiş Vurguları ve Notları Görüntüle
1. Vurgulanan metin ve eklediğiniz notların listesini görmek için Vurgu/Not simgesine dokunun.
2. Bir notu veya vurguyu kaldırmak için listeden seçin ve Çöp simgesine dokunun.
### Okuma İlerlemenizi Takip Edin
Omnivore, cihazlarınızdaki okuma ilerlemenizi otomatik olarak takip eder, böylece kaldığınız yerden kolayca devam edebilirsiniz. Her bir bağlantının üstünde bir ilerleme çubuğu görünecektir.
## Düzenleme
Varsayılan olarak, kitaplık gelen kutusu kaydettiğiniz tüm bağlantıları gösterir. Listeyi yönetmek ve okuma düzeninizi korumak için Omnivore aşağıdaki işlemleri sağlar:
- <span style="text-decoration:underline;">Arşivleme</span>
- <span style="text-decoration:underline;">Etiketler</span>
- <span style="text-decoration:underline;">Arama</span>
- <span style="text-decoration:underline;">Filtreler</span>
### Arşivleme
1. Arşivlemek istediğiniz bağlantının yanındaki menü simgesine dokunun (mobil uygulamada menüyü açmak için bağlantıyı uzun süre basılı tutun).
2. **Arşivle**'yi seçin.
3. Bağlantı, varsayılan kitaplık görünümünden kaybolur, ancak "arşivlenen" filtresini seçerseniz görünecektir (bkz. <span style="text-decoration:underline;">Filtreler</span>aşağıda).
### Etiketler
1. Herhangi bir bağlantının yanındaki menü simgesine dokunun ve **Etiket Ayarla**'yı seçin.
2. Listeden mevcut bir etiket seçin veya yeni bir tane oluşturmak için **Etiketleri Düzenle**'ye dokunun.
3. Etiket, kitaplığınızdaki bağlantının yanında görünecektir. Tıkladığınızda aynı etikete sahip tüm bağlantıları görüntülemek için dokunun.
4. _Yalnızca Omnivore mobil uygulaması_: Kullandığınız tüm etiketlerin tam listesini görmek için **Etiketler**'e dokunun; birini tıkladığınızda aynı etikete sahip tüm bağlantıları görüntülemek için dokunun.
5. Not: Omnivore, bazı etiketleri otomatik olarak atar, örneğin "Bültenler".
### Arama
1. Kaydettiğiniz tüm bağlantıları aramak için arama çubuğuna bir anahtar kelime veya ifade girin.
2. Aramanızı daha da daraltmak için etiketleri ve filtreleri anahtar kelimelerle birleştirebilirsiniz. [Daha fazla bilgi için gelişmiş arama hakkında bunu inceleyebilirsiniz](https://docs.omnivore.app/using/search.html).
### Filtreler
1. Kütüphane görünümünüzü iyileştirmek için **Filtreler** menüsünü kullanın (bazı filtreler varsayılan olarak görünebilir).
2. **Daha Sonra Oku**'yu seçerek arşivlenmemiş bağlantılarınızın ve Bültenler hariç tüm bağlantılarınızın bir listesini görüntüleyin.
3. **Vurgular**'ı seçerek kaydettiğiniz tüm sayfalardaki vurgulamaları görüntüleyin.
4. **Bugün**'ü seçerek bugün kaydettiğiniz bağlantıların bir listesini görüntüleyin.
5. **Bültenler**'i seçerek bülten abonelikleriniz aracılığıyla kaydedilen bağlantıları görüntüleyin.
## Entegrasyonlar
Omnivore, bilgi tabanları ve not alma uygulamaları ile entegrasyonlar yapmanıza izin verir, bunlar arasında şunlar bulunur:
- Logseq
- Webhooks
### Logseq
Omnivore'un Logseq eklentisi ile kaydettiğiniz tüm makaleleri, vurguları ve notları Logseq'e senkronize edebilirsiniz, bu popüler bir bilgi tabanıdır. Logseq eklentisi hakkında bilgi almak ve kullanmak için lütfen bu yardımcı [Omnivore Logseq Eklenti Kılavuzu'na](https://briansunter.com/graph/#/page/omnivore-logseq-guide) başvurun.
### Webhooks
Omnivore, bir bağlantıyı kaydettiğinizde veya okuduğunuz bir sayfaya vurgular eklediğinizde ayarladığınız bir webhook'u tetikleyebilir. <span style="text-decoration:underline;">Bu örnek</span>, tüm kaydedilen bağlantıları Google Drive üzerinde depolanan bir Google Sheets elektronik tabloya yazmak için webhooks'un nasıl kullanıldığını göstermektedir.

View file

@ -104,9 +104,6 @@ export class LibraryItem {
@Column('text', { nullable: true })
siteIcon?: string | null
@Column('json', { nullable: true })
metadata?: Record<string, unknown> | null
@Column('integer')
readingProgressLastReadAnchor!: number
@ -142,9 +139,6 @@ export class LibraryItem {
@Column('text')
readableContent!: string
@Column('text', { nullable: true })
modelName?: string | null
// NOT SUPPORTED IN TYPEORM
// @Column('vector', { nullable: true })
// embedding?: number[]
@ -152,9 +146,6 @@ export class LibraryItem {
@Column('text', { nullable: true })
textContentHash?: string | null
@Column('text', { nullable: true })
gcsArchiveId?: string | null
@Column('text', { nullable: true })
subscription?: string | null

View file

@ -3122,6 +3122,7 @@ export type User = {
followersCount?: Maybe<Scalars['Int']>;
friendsCount?: Maybe<Scalars['Int']>;
id: Scalars['ID'];
intercomHash?: Maybe<Scalars['String']>;
/** @deprecated isFriend has been replaced with viewerIsFollowing */
isFriend?: Maybe<Scalars['Boolean']>;
isFullUser?: Maybe<Scalars['Boolean']>;
@ -6076,6 +6077,7 @@ export type UserResolvers<ContextType = ResolverContext, ParentType extends Reso
followersCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
friendsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
intercomHash?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
isFriend?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
isFullUser?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;

View file

@ -2523,6 +2523,7 @@ type User {
followersCount: Int
friendsCount: Int
id: ID!
intercomHash: String
isFriend: Boolean @deprecated(reason: "isFriend has been replaced with viewerIsFollowing")
isFullUser: Boolean
name: String!

View file

@ -2,6 +2,7 @@ import { PubSub } from '@google-cloud/pubsub'
import express from 'express'
import { env } from './env'
import { ReportType } from './generated/graphql'
import { deepDelete } from './utils/helpers'
import { buildLogger } from './utils/logger'
const logger = buildLogger('pubsub')
@ -9,9 +10,11 @@ const logger = buildLogger('pubsub')
const client = new PubSub()
export const createPubSubClient = (): PubsubClient => {
const fieldsToDelete = ['user'] as const
const publish = (topicName: string, msg: Buffer): Promise<void> => {
if (env.dev.isLocal) {
logger.info(`Publishing ${topicName}`)
logger.info(`Publishing ${topicName}: ${msg.toString()}`)
return Promise.resolve()
}
@ -43,9 +46,14 @@ export const createPubSubClient = (): PubsubClient => {
data: T,
userId: string
): Promise<void> => {
const cleanData = deepDelete(
data as T & Record<typeof fieldsToDelete[number], unknown>,
[...fieldsToDelete]
)
return publish(
'entityCreated',
Buffer.from(JSON.stringify({ type, userId, ...data }))
Buffer.from(JSON.stringify({ type, userId, ...cleanData }))
)
},
entityUpdated: <T>(
@ -53,9 +61,14 @@ export const createPubSubClient = (): PubsubClient => {
data: T,
userId: string
): Promise<void> => {
const cleanData = deepDelete(
data as T & Record<typeof fieldsToDelete[number], unknown>,
[...fieldsToDelete]
)
return publish(
'entityUpdated',
Buffer.from(JSON.stringify({ type, userId, ...data }))
Buffer.from(JSON.stringify({ type, userId, ...cleanData }))
)
},
entityDeleted: (

View file

@ -4,6 +4,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Subscription } from '../entity/subscription'
import { env } from '../env'
import {
Article,
Highlight,
@ -11,6 +12,7 @@ import {
PageType,
Recommendation,
SearchItem,
User,
} from '../generated/graphql'
import { findHighlightsByLibraryItemId } from '../services/highlights'
import { findLabelsByLibraryItemId } from '../services/labels'
@ -126,6 +128,7 @@ import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { WithDataSourcesContext } from './types'
import { updateEmailResolver } from './user'
import { createHmac } from 'crypto'
/* eslint-disable @typescript-eslint/naming-convention */
type ResultResolveType = {
@ -247,141 +250,22 @@ export const functionResolvers = {
groups: groupsResolver,
recentEmails: recentEmailsResolver,
},
// User: {
// async sharedArticles(
// user: User,
// __: Record<string, unknown>,
// ctx: WithDataSourcesContext
// ) {
// return ctx.models.userArticle.getUserSharedArticles(user.id, ctx.kx)
// },
// async sharedArticlesCount(
// user: { id: string; sharedArticlesCount?: number },
// __: Record<string, unknown>,
// ctx: WithDataSourcesContext
// ) {
// if (user.sharedArticlesCount) return user.sharedArticlesCount
// return ctx.models.userArticle.getSharedArticlesCount(user.id, ctx.kx)
// },
// async sharedHighlightsCount(
// user: { id: string; sharedHighlightsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// // #TODO: restructure highlightStats and sharedArticlesCount in order to get it within a single query
// if (user.sharedHighlightsCount) return user.sharedHighlightsCount
// const { sharedHighlightsCount } =
// await ctx.models.user.getSharedHighlightsStats(user.id)
// return sharedHighlightsCount
// },
// async sharedNotesCount(
// user: User,
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (user.sharedNotesCount) return user.sharedNotesCount
// const { sharedNotesCount } =
// await ctx.models.user.getSharedHighlightsStats(user.id)
// return sharedNotesCount
// },
// },
// FeedArticle: {
// async article(
// feedArticle: { articleId: string; userId: string; article?: Article },
// __: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.article) return feedArticle.article
User: {
async intercomHash(
user: User,
__: Record<string, unknown>,
ctx: WithDataSourcesContext
) {
if (env.intercom.secretKey) {
const userIdentifier = user.id.toString()
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// let a: any
// const savedArticle =
// ctx.claims?.uid &&
// (await ctx.models.userArticle.getForUser(
// ctx.claims?.uid,
// feedArticle.articleId,
// ctx.kx
// ))
// if (savedArticle) {
// // If user has saved the article, use his version (slug) then
// a = {
// ...savedArticle,
// savedByViewer: true,
// postedByViewer: !!savedArticle.sharedAt,
// }
// } else {
// a = await ctx.models.userArticle.getForUser(
// feedArticle.userId,
// feedArticle.articleId,
// ctx.kx
// )
// }
// if (a && a.image) {
// a.image = createImageProxyUrl(a.image, 0, 180)
// } else {
// logger.info(
// 'error getting article for feedItem',
// feedArticle.userId,
// feedArticle.articleId
// )
// }
// return a
// },
// async sharedBy(
// feedArticle: { userId: string; sharedBy?: User },
// __: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.sharedBy) return feedArticle.sharedBy
// return userDataToUser(await ctx.models.user.get(feedArticle.userId))
// },
// async highlight(
// feedArticle: { highlightId?: string; highlight?: Highlight },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.highlight) return feedArticle.highlight
// return feedArticle.highlightId
// ? await ctx.models.highlight.get(feedArticle.highlightId)
// : null
// },
// async reactions(
// feedArticle: { id: string; reactions?: Reaction[] },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// const { reactions, id } = feedArticle
// if (reactions) return reactions
// return await ctx.models.reaction.batchGetFromArticle(id)
// },
// async highlightsCount(
// feedArticle: { id: string; highlightsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.highlightsCount) return feedArticle.highlightsCount
// const { highlightsCount } = await ctx.models.userArticle.getStats(
// feedArticle.id
// )
// return highlightsCount
// },
// async annotationsCount(
// feedArticle: { id: string; annotationsCount?: number },
// _: unknown,
// ctx: WithDataSourcesContext
// ) {
// if (feedArticle.annotationsCount) return feedArticle.annotationsCount
// const { annotationsCount } = await ctx.models.userArticle.getStats(
// feedArticle.id
// )
// return annotationsCount
// },
// },
return createHmac('sha256', env.intercom.secretKey)
.update(userIdentifier)
.digest('hex')
}
return undefined
},
},
Article: {
async url(article: Article, _: unknown, ctx: WithDataSourcesContext) {
if (

View file

@ -86,6 +86,7 @@ const schema = gql`
followersCount: Int
email: String
source: String
intercomHash: String
}
type Profile {

View file

@ -22,27 +22,14 @@ export const getHighlightUrl = (slug: string, highlightId: string): string =>
export const createHighlights = async (
highlights: DeepPartial<Highlight>[],
libraryItemId: string,
userId: string,
pubsub = createPubSubClient()
userId: string
) => {
const newHighlights = await authTrx(
return authTrx(
async (tx) =>
tx.withRepository(highlightRepository).createAndSaves(highlights),
undefined,
userId
)
await pubsub.entityCreated<CreateHighlightEvent[]>(
EntityType.HIGHLIGHT,
newHighlights.map((highlight) => ({
...highlight,
pageId: libraryItemId,
})),
userId
)
return newHighlights
}
export const createHighlight = async (

View file

@ -1,4 +1,4 @@
import { FindOptionsWhere, In } from 'typeorm'
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'
import { EntityLabel } from '../entity/entity_label'
import { Label } from '../entity/label'
@ -8,6 +8,15 @@ import { authTrx } from '../repository'
import { CreateLabelInput, labelRepository } from '../repository/label'
import { libraryItemRepository } from '../repository/library_item'
type AddLabelsToLibraryItemEvent = {
pageId: string
labels: DeepPartial<Label>[]
}
type AddLabelsToHighlightEvent = {
highlightId: string
labels: DeepPartial<Label>[]
}
// const batchGetLabelsFromLinkIds = async (
// linkIds: readonly string[]
// ): Promise<Label[][]> => {
@ -84,9 +93,9 @@ export const saveLabelsInLibraryItem = async (
)
// create pubsub event
await pubsub.entityCreated<(Label & { pageId: string })[]>(
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
labels.map((l) => ({ ...l, pageId: libraryItemId })),
{ pageId: libraryItemId, labels },
userId
)
}
@ -120,9 +129,9 @@ export const addLabelsToLibraryItem = async (
)
// create pubsub event
await pubsub.entityCreated<(Label & { pageId: string })[]>(
await pubsub.entityCreated<AddLabelsToLibraryItemEvent>(
EntityType.LABEL,
labels.map((l) => ({ ...l, pageId: libraryItemId })),
{ pageId: libraryItemId, labels },
userId
)
}
@ -151,9 +160,9 @@ export const saveLabelsInHighlight = async (
})
// create pubsub event
await pubsub.entityCreated<(Label & { highlightId: string })[]>(
await pubsub.entityCreated<AddLabelsToHighlightEvent>(
EntityType.LABEL,
labels.map((l) => ({ ...l, highlightId })),
{ highlightId, labels },
userId
)
}

View file

@ -59,7 +59,7 @@ export const addRecommendation = async (
color: highlight.color,
}))
if (highlights) {
await createHighlights(highlights, recommendedItem.id, userId)
await createHighlights(highlights, userId)
}
await createRecommendation(

View file

@ -41,6 +41,7 @@ interface BackendEnv {
}
intercom: {
token: string
secretKey: string
}
sentry: {
dsn: string
@ -118,6 +119,7 @@ export function isAppEngine(): boolean {
const nullableEnvVars = [
'INTERCOM_TOKEN',
'INTERCOM_SECRET_KEY',
'GAE_INSTANCE',
'SENTRY_DSN',
'SENTRY_AUTH_TOKEN',
@ -229,6 +231,7 @@ export function getEnv(): BackendEnv {
}
const intercom = {
token: parse('INTERCOM_TOKEN'),
secretKey: parse('INTERCOM_SECRET_KEY'),
}
const sentry = {
dsn: parse('SENTRY_DSN'),

View file

@ -385,5 +385,17 @@ export const cleanUrl = (url: string) => {
stripHash: true,
stripWWW: false,
removeQueryParameters: trackingParams,
removeTrailingSlash: false,
})
}
export const deepDelete = <T, K extends keyof T>(obj: T, keys: K[]) => {
// make a copy of the object
const copy = { ...obj }
keys.forEach((key) => {
delete copy[key]
})
return copy as Omit<T, K>
}

View file

@ -338,7 +338,7 @@ const parseFieldFilter = (
}
}
const parseIds = (field: string, str?: string): string[] | undefined => {
const parseIds = (str?: string): string[] | undefined => {
if (str === undefined) {
return undefined
}
@ -500,7 +500,7 @@ export const parseSearchQuery = (query: string | undefined): SearchFilter => {
break
}
case 'includes': {
const ids = parseIds(keyword.keyword, keyword.value)
const ids = parseIds(keyword.value)
ids && result.ids.push(...ids)
break
}

View file

@ -1568,6 +1568,85 @@ describe('Article API', () => {
).to.eq(group.name)
})
})
context('when site:youtube.com is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:youtube.com'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: 'https://www.youtube.com/watch?v=Omnivore',
itemType: PageType.Video,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns youtube videos', 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 site:wikipedia is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:wikipedia'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: 'https://en.wikipedia.org/wiki/Omnivore',
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns wikipedia pages', 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', () => {

View file

@ -0,0 +1,31 @@
-- Type: DO
-- Name: add_domain_to_site_name_tsv
-- Description: Convert domain to tsvector and add it to site_name_tsv column
BEGIN;
CREATE OR REPLACE FUNCTION update_library_item_tsv() RETURNS trigger AS $$
begin
new.content_tsv := to_tsvector('pg_catalog.english', coalesce(new.readable_content, ''));
new.site_name_tsv := to_tsvector('pg_catalog.english', coalesce(new.site_name, '')) ||
-- domain (eg omnivore.app)
to_tsvector('pg_catalog.english', coalesce(regexp_replace(new.original_url, '^((http[s]?):\/)?\/?(.*\.)?(([^:\/\s]+)\.[^:\/\s]+)(.*)$', '\4'), '')) ||
-- secondary hostname (eg omnivore)
to_tsvector('pg_catalog.english', coalesce(regexp_replace(new.original_url, '^((http[s]?):\/)?\/?(.*\.)?(([^:\/\s]+)\.[^:\/\s]+)(.*)$', '\5'), ''));
new.title_tsv := to_tsvector('pg_catalog.english', coalesce(new.title, ''));
new.author_tsv := to_tsvector('pg_catalog.english', coalesce(new.author, ''));
new.description_tsv := to_tsvector('pg_catalog.english', coalesce(new.description, ''));
-- note_tsv is generated by both note and highlight_annotations
new.note_tsv := to_tsvector('pg_catalog.english', coalesce(new.note, '') || ' ' || array_to_string(new.highlight_annotations, ' '));
new.search_tsv :=
setweight(new.title_tsv, 'A') ||
setweight(new.author_tsv, 'A') ||
setweight(new.site_name_tsv, 'A') ||
setweight(new.description_tsv, 'A') ||
setweight(new.note_tsv, 'A') ||
setweight(new.content_tsv, 'B');
return new;
end
$$ LANGUAGE plpgsql;
COMMIT;

View file

@ -0,0 +1,31 @@
-- Type: UNDO
-- Name: add_domain_to_site_name_tsv
-- Description: Convert domain to tsvector and add it to site_name_tsv column
BEGIN;
CREATE OR REPLACE FUNCTION update_library_item_tsv() RETURNS trigger AS $$
begin
new.content_tsv := to_tsvector('pg_catalog.english', coalesce(new.readable_content, ''));
new.site_name_tsv := to_tsvector('pg_catalog.english', coalesce(new.site_name, ''));
new.title_tsv := to_tsvector('pg_catalog.english', coalesce(new.title, ''));
new.author_tsv := to_tsvector('pg_catalog.english', coalesce(new.author, ''));
new.description_tsv := to_tsvector('pg_catalog.english', coalesce(new.description, ''));
-- note_tsv is generated by both note and highlight_annotations
new.note_tsv := to_tsvector('pg_catalog.english', coalesce(new.note, '') || ' ' || array_to_string(new.highlight_annotations, ' '));
new.search_tsv :=
setweight(new.title_tsv, 'A') ||
setweight(new.author_tsv, 'A') ||
setweight(new.site_name_tsv, 'A') ||
setweight(new.description_tsv, 'A') ||
-- full hostname (eg www.omnivore.app)
setweight(to_tsvector('pg_catalog.english', coalesce(regexp_replace(new.original_url, '^((http[s]?):\/)?\/?([^:\/\s]+)((\/\w+)*\/)([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$', '\3'), '')), 'A') ||
-- secondary hostname (eg omnivore)
setweight(to_tsvector('pg_catalog.english', coalesce(regexp_replace(new.original_url, '^((http[s]?):\/)?\/?(.*\.)?([^:\/\s]+)(\..*)((\/+)*\/)?([\w\-\.]+[^#?\s]+)(.*)?(#[\w\-]+)?$', '\4'), '')), 'A') ||
setweight(new.note_tsv, 'A') ||
setweight(new.content_tsv, 'B');
return new;
end
$$ LANGUAGE plpgsql;
COMMIT;

View file

@ -1,4 +1,4 @@
FROM node:14.18-alpine
FROM node:18.16-alpine
# Run everything after as non-privileged user.
WORKDIR /app

View file

@ -165,9 +165,7 @@ const parser = new Parser({
'created',
],
feed: [
'dc:date',
'lastBuildDate',
'pubDate',
'syn:updatePeriod',
'syn:updateFrequency',
'sy:updatePeriod',
@ -263,12 +261,13 @@ const processSubscription = async (
const feed = await parser.parseString(fetchResult.content)
console.log('Fetched feed', feed.title, new Date())
const feedPubDate = (feed['dc:date'] ||
feed.pubDate ||
feed.lastBuildDate) as string | undefined
console.log('Feed pub date', feedPubDate)
if (feedPubDate && new Date(feedPubDate) < new Date(lastFetchedAt)) {
console.log('Skipping old feed', feedPubDate)
const feedLastBuildDate = feed.lastBuildDate as string | undefined
console.log('Feed last build date', feedLastBuildDate)
if (
feedLastBuildDate &&
new Date(feedLastBuildDate) < new Date(lastFetchedAt)
) {
console.log('Skipping old feed', feedLastBuildDate)
return
}

View file

@ -36,8 +36,6 @@ import { LoadingBar } from '../../elements/LoadingBar'
export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
const router = useRouter()
const [isHovered, setIsHovered] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const { refs, floatingStyles, context } = useFloating({
@ -93,12 +91,6 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
}}
alignment="start"
distribution="start"
onMouseEnter={() => {
setIsHovered(true)
}}
onMouseLeave={() => {
setIsHovered(false)
}}
onClick={(event) => {
if (event.metaKey || event.ctrlKey) {
window.open(
@ -120,11 +112,11 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
item={props.item}
viewer={props.viewer}
handleAction={props.handleAction}
isHovered={isHovered ?? false}
isHovered={isOpen ?? false}
/>
</Box>
)}
<LibraryListCardContent {...props} isHovered={isHovered} />
<LibraryListCardContent {...props} isHovered={isOpen} />
</VStack>
)
}

View file

@ -163,15 +163,10 @@ export function HomeFeedContainer(): JSX.Element {
}
const mode = router.query['mode']
// intentionally not watching queryInputs here to prevent infinite looping
// intentionally not watching queryInputs and performActionOnItem
// here to prevent infinite looping
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
setMode,
setQueryInputs,
router.isReady,
router.query,
performActionOnItem,
])
}, [setMode, setQueryInputs, router.isReady, router.query])
const hasMore = useMemo(() => {
if (!itemsPages) {

View file

@ -2,10 +2,13 @@ import { UserBasicData } from './networking/queries/useGetViewerQuery'
import { intercomAppID, posthogApiKey, webBaseURL } from './appConfig'
import posthog from 'posthog-js'
const userInfo = (user: UserBasicData): { user_id: string; name: string } => {
const userInfo = (
user: UserBasicData
): { user_id: string; name: string; user_hash: string } => {
return {
user_id: user.id,
name: user.name,
user_hash: user.intercomHash,
}
}

View file

@ -19,6 +19,7 @@ export type UserBasicData = {
profile: UserProfile
email: string
source: string
intercomHash: string
}
export type UserProfile = {
@ -43,6 +44,7 @@ export function useGetViewerQuery(): ViewerQueryResponse {
}
email
source
intercomHash
}
}
`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 72 KiB