mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3062 from omnivore-app/main
Web production deployment
This commit is contained in:
commit
5a0014e4a1
23 changed files with 395 additions and 199 deletions
161
docs/guides/getting-started/tr/getting-started-guide.md
Normal file
161
docs/guides/getting-started/tr/getting-started-guide.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const schema = gql`
|
|||
followersCount: Int
|
||||
email: String
|
||||
source: String
|
||||
intercomHash: String
|
||||
}
|
||||
|
||||
type Profile {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
31
packages/db/migrations/0143.do.add_domain_to_site_name_tsv.sql
Executable file
31
packages/db/migrations/0143.do.add_domain_to_site_name_tsv.sql
Executable 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;
|
||||
31
packages/db/migrations/0143.undo.add_domain_to_site_name_tsv.sql
Executable file
31
packages/db/migrations/0143.undo.add_domain_to_site_name_tsv.sql
Executable 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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:14.18-alpine
|
||||
FROM node:18.16-alpine
|
||||
|
||||
# Run everything after as non-privileged user.
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
Loading…
Reference in a new issue