mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'omnivore-app:main' into main
This commit is contained in:
commit
68cfc8df80
197 changed files with 11781 additions and 6677 deletions
|
|
@ -3146,6 +3146,7 @@ export type SearchItem = {
|
|||
folder: Scalars['String'];
|
||||
format?: Maybe<Scalars['String']>;
|
||||
highlights?: Maybe<Array<Highlight>>;
|
||||
highlightsCount?: Maybe<Scalars['Int']>;
|
||||
id: Scalars['ID'];
|
||||
image?: Maybe<Scalars['String']>;
|
||||
isArchived: Scalars['Boolean'];
|
||||
|
|
@ -7481,6 +7482,7 @@ export type SearchItemResolvers<ContextType = ResolverContext, ParentType extend
|
|||
folder?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
format?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
highlights?: Resolver<Maybe<Array<ResolversTypes['Highlight']>>, ParentType, ContextType>;
|
||||
highlightsCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
image?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
isArchived?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
|
|
|
|||
|
|
@ -2415,6 +2415,7 @@ type SearchItem {
|
|||
folder: String!
|
||||
format: String
|
||||
highlights: [Highlight!]
|
||||
highlightsCount: Int
|
||||
id: ID!
|
||||
image: String
|
||||
isArchived: Boolean!
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ const sendEmail = async (user: User, digest: Digest, channels: Channel[]) => {
|
|||
</div>`
|
||||
|
||||
await enqueueSendEmail({
|
||||
userId: user.id,
|
||||
to: user.email,
|
||||
from: env.sender.message,
|
||||
subject: subTitle,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export const forwardEmailJob = async (data: EmailJobData) => {
|
|||
|
||||
// forward non-newsletter emails to the registered email address
|
||||
const result = await enqueueSendEmail({
|
||||
userId: user.id,
|
||||
from: env.sender.message,
|
||||
to: user.email,
|
||||
subject: `Fwd: ${subject}`,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { env } from '../../env'
|
||||
import { sendWithMailJet } from '../../services/send_emails'
|
||||
import { findActiveUser } from '../../services/user'
|
||||
import { Merge } from '../../util'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { sendEmail } from '../../utils/sendEmail'
|
||||
|
|
@ -9,7 +10,8 @@ export const SEND_EMAIL_JOB = 'send-email'
|
|||
type ContentType = { html: string } | { text: string } | { templateId: string }
|
||||
export type SendEmailJobData = Merge<
|
||||
{
|
||||
to: string
|
||||
userId: string
|
||||
to?: string
|
||||
from?: string
|
||||
subject?: string
|
||||
html?: string
|
||||
|
|
@ -22,6 +24,16 @@ export type SendEmailJobData = Merge<
|
|||
>
|
||||
|
||||
export const sendEmailJob = async (data: SendEmailJobData) => {
|
||||
if (!data.to) {
|
||||
const user = await findActiveUser(data.userId)
|
||||
if (!user) {
|
||||
logger.error('user not found', data.userId)
|
||||
return false
|
||||
}
|
||||
|
||||
data.to = user.email
|
||||
}
|
||||
|
||||
if (process.env.USE_MAILJET && data.dynamicTemplateData) {
|
||||
return sendWithMailJet(data.to, data.dynamicTemplateData.link)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,11 +78,6 @@ export const createPubSubClient = (): PubsubClient => {
|
|||
})
|
||||
}
|
||||
|
||||
await enqueueScoreJob({
|
||||
userId,
|
||||
libraryItemId: data.id,
|
||||
})
|
||||
|
||||
const hasThumbnail = (
|
||||
data: any
|
||||
): data is { thumbnail: string | null } => {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ export const libraryItemRepository = appDataSource
|
|||
INSERT INTO omnivore.library_item (
|
||||
slug,
|
||||
readable_content,
|
||||
original_content,
|
||||
description,
|
||||
title,
|
||||
author,
|
||||
|
|
@ -82,7 +81,6 @@ export const libraryItemRepository = appDataSource
|
|||
SELECT
|
||||
slug,
|
||||
readable_content,
|
||||
original_content,
|
||||
description,
|
||||
title,
|
||||
author,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
MutationSaveArticleReadingProgressArgs,
|
||||
MutationSetBookmarkArticleArgs,
|
||||
MutationSetFavoriteArticleArgs,
|
||||
PageInfo,
|
||||
PageType,
|
||||
QueryArticleArgs,
|
||||
QuerySearchArgs,
|
||||
|
|
@ -78,8 +79,10 @@ import {
|
|||
batchUpdateLibraryItems,
|
||||
countLibraryItems,
|
||||
createOrUpdateLibraryItem,
|
||||
deleteCachedTotalCount,
|
||||
findLibraryItemsByPrefix,
|
||||
searchAndCountLibraryItems,
|
||||
SearchArgs,
|
||||
searchLibraryItems,
|
||||
softDeleteLibraryItem,
|
||||
sortParamsToSort,
|
||||
updateLibraryItem,
|
||||
|
|
@ -114,7 +117,6 @@ import { getStorageFileDetails } from '../../utils/uploads'
|
|||
export enum ArticleFormat {
|
||||
Markdown = 'markdown',
|
||||
Html = 'html',
|
||||
Distiller = 'distiller',
|
||||
HighlightedMarkdown = 'highlightedMarkdown',
|
||||
}
|
||||
|
||||
|
|
@ -583,8 +585,15 @@ export const saveArticleReadingProgressResolver = authorized<
|
|||
|
||||
export type PartialLibraryItem = Merge<LibraryItem, { format?: string }>
|
||||
type PartialSearchItemEdge = Merge<SearchItemEdge, { node: PartialLibraryItem }>
|
||||
export type PartialPageInfo = Merge<
|
||||
PageInfo,
|
||||
{ searchLibraryItemArgs?: SearchArgs }
|
||||
>
|
||||
export const searchResolver = authorized<
|
||||
Merge<SearchSuccess, { edges: Array<PartialSearchItemEdge> }>,
|
||||
Merge<
|
||||
SearchSuccess,
|
||||
{ edges: Array<PartialSearchItemEdge>; pageInfo: PartialPageInfo }
|
||||
>,
|
||||
SearchError,
|
||||
QuerySearchArgs
|
||||
>(async (_obj, params, { uid }) => {
|
||||
|
|
@ -596,15 +605,19 @@ export const searchResolver = authorized<
|
|||
return { errorCodes: [SearchErrorCode.QueryTooLong] }
|
||||
}
|
||||
|
||||
const { libraryItems, count } = await searchAndCountLibraryItems(
|
||||
const searchLibraryItemArgs = {
|
||||
includePending: true,
|
||||
includeContent: params.includeContent ?? true, // by default include content for offline use for now
|
||||
includeDeleted: params.query?.includes('in:trash'),
|
||||
query: params.query,
|
||||
useFolders: params.query?.includes('use:folders'),
|
||||
}
|
||||
|
||||
const libraryItems = await searchLibraryItems(
|
||||
{
|
||||
...searchLibraryItemArgs,
|
||||
from: Number(startCursor),
|
||||
size: first + 1, // fetch one more item to get next cursor
|
||||
includePending: true,
|
||||
includeContent: params.includeContent ?? true, // by default include content for offline use for now
|
||||
includeDeleted: params.query?.includes('in:trash'),
|
||||
query: params.query,
|
||||
useFolders: params.query?.includes('use:folders'),
|
||||
},
|
||||
uid
|
||||
)
|
||||
|
|
@ -632,7 +645,7 @@ export const searchResolver = authorized<
|
|||
startCursor,
|
||||
hasNextPage,
|
||||
endCursor,
|
||||
totalCount: count,
|
||||
searchLibraryItemArgs,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -658,7 +671,10 @@ export const typeaheadSearchResolver = authorized<
|
|||
})
|
||||
|
||||
export const updatesSinceResolver = authorized<
|
||||
Merge<UpdatesSinceSuccess, { edges: Array<PartialSearchItemEdge> }>,
|
||||
Merge<
|
||||
UpdatesSinceSuccess,
|
||||
{ edges: Array<PartialSearchItemEdge>; pageInfo: PartialPageInfo }
|
||||
>,
|
||||
UpdatesSinceError,
|
||||
QueryUpdatesSinceArgs
|
||||
>(async (_obj, { since, first, after, sort: sortParams, folder }, { uid }) => {
|
||||
|
|
@ -676,13 +692,17 @@ export const updatesSinceResolver = authorized<
|
|||
folder ? ' in:' + folder : ''
|
||||
} sort:${sort.by}-${sort.order}`
|
||||
|
||||
const { libraryItems, count } = await searchAndCountLibraryItems(
|
||||
const searchLibraryItemArgs = {
|
||||
includeDeleted: true,
|
||||
query,
|
||||
includeContent: true, // by default include content for offline use for now
|
||||
}
|
||||
|
||||
const libraryItems = await searchLibraryItems(
|
||||
{
|
||||
...searchLibraryItemArgs,
|
||||
from: Number(startCursor),
|
||||
size: size + 1, // fetch one more item to get next cursor
|
||||
includeDeleted: true,
|
||||
query,
|
||||
includeContent: true, // by default include content for offline use for now
|
||||
},
|
||||
uid
|
||||
)
|
||||
|
|
@ -715,7 +735,7 @@ export const updatesSinceResolver = authorized<
|
|||
startCursor,
|
||||
hasNextPage,
|
||||
endCursor,
|
||||
totalCount: count,
|
||||
searchLibraryItemArgs,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
@ -762,6 +782,8 @@ export const bulkActionResolver = authorized<
|
|||
// if there are less than batchSize items, update them synchronously
|
||||
await batchUpdateLibraryItems(action, searchArgs, uid, labelIds, args)
|
||||
|
||||
await deleteCachedTotalCount(uid)
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
|
|
@ -781,6 +803,8 @@ export const bulkActionResolver = authorized<
|
|||
return { errorCodes: [BulkActionErrorCode.BadRequest] }
|
||||
}
|
||||
|
||||
await deleteCachedTotalCount(uid)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
log.error('bulkActionResolver error', error)
|
||||
|
|
@ -977,6 +1001,8 @@ export const emptyTrashResolver = authorized<
|
|||
state: LibraryItemState.Deleted,
|
||||
})
|
||||
|
||||
await deleteCachedTotalCount(uid)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import {
|
|||
} from '../../generated/graphql'
|
||||
import {
|
||||
getFeatureName,
|
||||
getFeaturesCache,
|
||||
isOptInFeatureErrorCode,
|
||||
optInFeature,
|
||||
setFeaturesCache,
|
||||
signFeatureToken,
|
||||
} from '../../services/features'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
|
|
@ -34,7 +36,8 @@ export const optInFeatureResolver = authorized<
|
|||
}
|
||||
}
|
||||
|
||||
const optedInFeature = await optInFeature(featureName, claims.uid)
|
||||
const userId = claims.uid
|
||||
const optedInFeature = await optInFeature(featureName, userId)
|
||||
if (isOptInFeatureErrorCode(optedInFeature)) {
|
||||
return {
|
||||
errorCodes: [optedInFeature],
|
||||
|
|
@ -42,7 +45,11 @@ export const optInFeatureResolver = authorized<
|
|||
}
|
||||
log.info('Opted in to a feature', optedInFeature)
|
||||
|
||||
const token = signFeatureToken(optedInFeature, claims.uid)
|
||||
const cachedFeatures = (await getFeaturesCache(userId)) || []
|
||||
const updatedFeatures = [...cachedFeatures, optedInFeature]
|
||||
await setFeaturesCache(userId, updatedFeatures)
|
||||
|
||||
const token = signFeatureToken(optedInFeature, userId)
|
||||
|
||||
return {
|
||||
feature: {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,16 @@ import {
|
|||
User,
|
||||
} from '../generated/graphql'
|
||||
import { getAISummary } from '../services/ai-summaries'
|
||||
import { findUserFeatures } from '../services/features'
|
||||
import {
|
||||
findUserFeatures,
|
||||
getFeaturesCache,
|
||||
setFeaturesCache,
|
||||
} from '../services/features'
|
||||
import {
|
||||
countLibraryItems,
|
||||
getCachedTotalCount,
|
||||
setCachedTotalCount,
|
||||
} from '../services/library_item'
|
||||
import { Merge } from '../util'
|
||||
import { isBase64Image, validatedDate, wordsCount } from '../utils/helpers'
|
||||
import { createImageProxyUrl } from '../utils/imageproxy'
|
||||
|
|
@ -43,6 +52,7 @@ import {
|
|||
emptyTrashResolver,
|
||||
fetchContentResolver,
|
||||
PartialLibraryItem,
|
||||
PartialPageInfo,
|
||||
} from './article'
|
||||
import {
|
||||
addDiscoverFeedResolver,
|
||||
|
|
@ -61,11 +71,6 @@ import {
|
|||
updateFolderPolicyResolver,
|
||||
} from './folder_policy'
|
||||
import { highlightsResolver } from './highlight'
|
||||
import {
|
||||
hiddenHomeSectionResolver,
|
||||
homeResolver,
|
||||
refreshHomeResolver,
|
||||
} from './home'
|
||||
import { uploadImportFileResolver } from './importers/uploadImportFileResolver'
|
||||
import {
|
||||
addPopularReadResolver,
|
||||
|
|
@ -321,7 +326,6 @@ export const functionResolvers = {
|
|||
fetchContent: fetchContentResolver,
|
||||
exportToIntegration: exportToIntegrationResolver,
|
||||
replyToEmail: replyToEmailResolver,
|
||||
refreshHome: refreshHomeResolver,
|
||||
createFolderPolicy: createFolderPolicyResolver,
|
||||
updateFolderPolicy: updateFolderPolicyResolver,
|
||||
deleteFolderPolicy: deleteFolderPolicyResolver,
|
||||
|
|
@ -359,9 +363,7 @@ export const functionResolvers = {
|
|||
feeds: feedsResolver,
|
||||
scanFeeds: scanFeedsResolver,
|
||||
integration: integrationResolver,
|
||||
home: homeResolver,
|
||||
subscription: subscriptionResolver,
|
||||
hiddenHomeSection: hiddenHomeSectionResolver,
|
||||
highlights: highlightsResolver,
|
||||
folderPolicies: folderPoliciesResolver,
|
||||
posts: postsResolver,
|
||||
|
|
@ -406,7 +408,16 @@ export const functionResolvers = {
|
|||
return undefined
|
||||
}
|
||||
|
||||
return findUserFeatures(ctx.claims.uid)
|
||||
const userId = ctx.claims.uid
|
||||
const cachedFeatures = await getFeaturesCache(userId)
|
||||
if (cachedFeatures) {
|
||||
return cachedFeatures
|
||||
}
|
||||
|
||||
const features = await findUserFeatures(userId)
|
||||
await setFeaturesCache(userId, features)
|
||||
|
||||
return features
|
||||
},
|
||||
picture: (user: UserEntity) => user.profile.pictureUrl,
|
||||
// not implemented yet
|
||||
|
|
@ -569,13 +580,11 @@ export const functionResolvers = {
|
|||
item.format !== ArticleFormat.Html &&
|
||||
item.readableContent
|
||||
) {
|
||||
let highlights: Highlight[] = []
|
||||
// load highlights if needed
|
||||
if (
|
||||
item.format === ArticleFormat.HighlightedMarkdown &&
|
||||
item.highlightAnnotations?.length
|
||||
) {
|
||||
highlights = await ctx.dataLoaders.highlights.load(item.id)
|
||||
if (item.format === ArticleFormat.HighlightedMarkdown) {
|
||||
// if the content is highlighted markdown, we will return markdown instead
|
||||
// because the conversion is very slow and we don't want to block the response
|
||||
// we will convert it to highlighted markdown in the client if needed
|
||||
item.format = ArticleFormat.Markdown
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -584,7 +593,7 @@ export const functionResolvers = {
|
|||
// convert html to the requested format
|
||||
const converter = contentConverter(item.format)
|
||||
if (converter) {
|
||||
return converter(item.readableContent, highlights)
|
||||
return converter(item.readableContent)
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.log.error('Error converting content', error)
|
||||
|
|
@ -595,8 +604,32 @@ export const functionResolvers = {
|
|||
},
|
||||
isArchived: (item: LibraryItem) => !!item.archivedAt,
|
||||
pageType: (item: LibraryItem) => item.itemType,
|
||||
highlightsCount: (item: LibraryItem) => item.highlightAnnotations?.length,
|
||||
...readingProgressHandlers,
|
||||
},
|
||||
PageInfo: {
|
||||
async totalCount(
|
||||
pageInfo: PartialPageInfo,
|
||||
_: unknown,
|
||||
ctx: ResolverContext
|
||||
) {
|
||||
if (pageInfo.totalCount) return pageInfo.totalCount
|
||||
|
||||
if (pageInfo.searchLibraryItemArgs && ctx.claims) {
|
||||
const args = pageInfo.searchLibraryItemArgs
|
||||
const userId = ctx.claims.uid
|
||||
const cachedCount = await getCachedTotalCount(userId, args)
|
||||
if (cachedCount) return cachedCount
|
||||
|
||||
const count = await countLibraryItems(args, userId)
|
||||
await setCachedTotalCount(userId, args, count)
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
return 0
|
||||
},
|
||||
},
|
||||
Subscription: {
|
||||
newsletterEmail(subscription: Subscription) {
|
||||
return subscription.newsletterEmail?.address
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ export const replyToEmailResolver = authorized<
|
|||
}
|
||||
|
||||
const result = await enqueueSendEmail({
|
||||
userId: uid,
|
||||
to: recentEmail.replyTo || recentEmail.from, // send to the reply-to address if it exists or the from address
|
||||
subject: 'Re: ' + recentEmail.subject,
|
||||
text: reply,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import {
|
|||
import { userRepository } from '../../repository/user'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { sendAccountChangeEmail } from '../../services/send_emails'
|
||||
import { softDeleteUser } from '../../services/user'
|
||||
import { cacheUser, getCachedUser, softDeleteUser } from '../../services/user'
|
||||
import { Merge } from '../../util'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
|
|
@ -82,6 +82,8 @@ export const updateUserResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
await cacheUser(updatedUser)
|
||||
|
||||
return { user: updatedUser }
|
||||
})
|
||||
|
||||
|
|
@ -139,6 +141,8 @@ export const updateUserProfileResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
await cacheUser(updatedUser)
|
||||
|
||||
return { user: updatedUser }
|
||||
})
|
||||
|
||||
|
|
@ -254,11 +258,19 @@ export const getMeUserResolver: ResolverFn<
|
|||
return undefined
|
||||
}
|
||||
|
||||
const userId = claims.uid
|
||||
const cachedUser = await getCachedUser(userId)
|
||||
if (cachedUser) {
|
||||
return cachedUser
|
||||
}
|
||||
|
||||
const user = await userRepository.findById(claims.uid)
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
await cacheUser(user)
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
return undefined
|
||||
|
|
@ -355,6 +367,11 @@ export const updateEmailResolver = authorized<
|
|||
})
|
||||
)
|
||||
|
||||
await cacheUser({
|
||||
...user,
|
||||
email,
|
||||
})
|
||||
|
||||
return { email }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { env } from '../env'
|
||||
import { getClaimsByToken, getTokenByRequest } from '../utils/auth'
|
||||
import { corsConfig } from '../utils/corsConfig'
|
||||
import { logger } from '../utils/logger'
|
||||
import {
|
||||
cacheShortcuts,
|
||||
getShortcuts,
|
||||
getShortcutsCache,
|
||||
resetShortcuts,
|
||||
setShortcuts,
|
||||
} from '../services/user_personalization'
|
||||
import { getClaimsByToken, getTokenByRequest } from '../utils/auth'
|
||||
import { corsConfig } from '../utils/corsConfig'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
export function shortcutsRouter() {
|
||||
const router = express.Router()
|
||||
|
|
@ -32,9 +33,19 @@ export function shortcutsRouter() {
|
|||
}
|
||||
|
||||
try {
|
||||
const shortcuts = await getShortcuts(claims.uid)
|
||||
const userId = claims.uid
|
||||
const cachedShortcuts = await getShortcutsCache(userId)
|
||||
if (cachedShortcuts) {
|
||||
return res.send({
|
||||
shortcuts: cachedShortcuts,
|
||||
})
|
||||
}
|
||||
|
||||
const shortcuts = await getShortcuts(userId)
|
||||
await cacheShortcuts(userId, shortcuts)
|
||||
|
||||
return res.send({
|
||||
shortcuts: shortcuts ?? [],
|
||||
shortcuts,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.info('error getting shortcuts', e)
|
||||
|
|
@ -61,9 +72,12 @@ export function shortcutsRouter() {
|
|||
}
|
||||
|
||||
try {
|
||||
const shortcuts = await setShortcuts(claims.uid, req.body.shortcuts)
|
||||
const userId = claims.uid
|
||||
const shortcuts = await setShortcuts(userId, req.body.shortcuts)
|
||||
await cacheShortcuts(userId, shortcuts)
|
||||
|
||||
return res.send({
|
||||
shortcuts: shortcuts ?? [],
|
||||
shortcuts,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.info('error settings shortcuts', e)
|
||||
|
|
@ -89,11 +103,14 @@ export function shortcutsRouter() {
|
|||
}
|
||||
|
||||
try {
|
||||
const success = await resetShortcuts(claims.uid)
|
||||
const userId = claims.uid
|
||||
const success = await resetShortcuts(userId)
|
||||
if (success) {
|
||||
const shortcuts = await getShortcuts(claims.uid)
|
||||
const shortcuts = await getShortcuts(userId)
|
||||
await cacheShortcuts(userId, shortcuts)
|
||||
|
||||
return res.send({
|
||||
shortcuts: shortcuts ?? [],
|
||||
shortcuts,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1680,6 +1680,7 @@ const schema = gql`
|
|||
format: String
|
||||
score: Float
|
||||
seenAt: Date
|
||||
highlightsCount: Int
|
||||
}
|
||||
|
||||
type SearchItemEdge {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ export const createApp = (): Express => {
|
|||
app.use('/api/auth', authLimiter, authRouter())
|
||||
app.use('/api/mobile-auth', authLimiter, mobileAuthRouter())
|
||||
app.use('/api/page', pageRouter())
|
||||
app.use('/api/user', userRouter())
|
||||
app.use('/api/shortcuts', shortcutsRouter())
|
||||
app.use('/api/article', articleRouter())
|
||||
app.use('/api/ai-summary', aiSummariesRouter())
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { LibraryItem } from '../entity/library_item'
|
|||
import { Subscription, SubscriptionStatus } from '../entity/subscription'
|
||||
import { env } from '../env'
|
||||
import { OptInFeatureErrorCode } from '../generated/graphql'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx, getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
|
|
@ -201,3 +202,31 @@ export const userDigestEligible = async (uid: string): Promise<boolean> => {
|
|||
|
||||
return subscriptionsCount >= 2 && libraryItemsCount >= 10
|
||||
}
|
||||
|
||||
const featuresCacheKey = (userId: string) => `cache:features:${userId}`
|
||||
|
||||
export const getFeaturesCache = async (userId: string) => {
|
||||
logger.debug('getFeaturesCache', { userId })
|
||||
|
||||
const cachedFeatures = await redisDataSource.redisClient?.get(
|
||||
featuresCacheKey(userId)
|
||||
)
|
||||
if (!cachedFeatures) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(cachedFeatures) as Feature[]
|
||||
}
|
||||
|
||||
export const setFeaturesCache = async (userId: string, features: Feature[]) => {
|
||||
const value = JSON.stringify(features)
|
||||
|
||||
logger.debug('setFeaturesCache', { userId, value })
|
||||
|
||||
return redisDataSource.redisClient?.set(
|
||||
featuresCacheKey(userId),
|
||||
value,
|
||||
'EX',
|
||||
600
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ import {
|
|||
} from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { Merge, PickTuple } from '../util'
|
||||
import { deepDelete, setRecentlySavedItemInRedis } from '../utils/helpers'
|
||||
import {
|
||||
deepDelete,
|
||||
setRecentlySavedItemInRedis,
|
||||
stringToHash,
|
||||
} from '../utils/helpers'
|
||||
import { logger } from '../utils/logger'
|
||||
import { parseSearchQuery } from '../utils/search'
|
||||
import { HighlightEvent } from './highlights'
|
||||
|
|
@ -1711,3 +1715,55 @@ export const filterItemEvents = (
|
|||
|
||||
throw new Error('Unexpected state.')
|
||||
}
|
||||
|
||||
const totalCountCacheKey = (userId: string, args: SearchArgs) => {
|
||||
// sort the args to make sure the cache key is consistent
|
||||
const sortedArgs = JSON.stringify(args, Object.keys(args).sort())
|
||||
|
||||
return `cache:library_items_count:${userId}:${stringToHash(sortedArgs)}`
|
||||
}
|
||||
|
||||
export const getCachedTotalCount = async (userId: string, args: SearchArgs) => {
|
||||
logger.debug('Getting cached total count:', {
|
||||
userId,
|
||||
args,
|
||||
})
|
||||
|
||||
const cacheKey = totalCountCacheKey(userId, args)
|
||||
const cachedCount = await redisDataSource.redisClient?.get(cacheKey)
|
||||
if (!cachedCount) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return parseInt(cachedCount, 10)
|
||||
}
|
||||
|
||||
export const setCachedTotalCount = async (
|
||||
userId: string,
|
||||
args: SearchArgs,
|
||||
count: number
|
||||
) => {
|
||||
const cacheKey = totalCountCacheKey(userId, args)
|
||||
|
||||
logger.debug('Setting cached total count:', {
|
||||
cacheKey,
|
||||
count,
|
||||
})
|
||||
|
||||
await redisDataSource.redisClient?.set(cacheKey, count, 'EX', 600)
|
||||
}
|
||||
|
||||
export const deleteCachedTotalCount = async (userId: string) => {
|
||||
const keyPattern = `cache:library_items_count:${userId}:*`
|
||||
const keys = await redisDataSource.redisClient?.keys(keyPattern)
|
||||
if (!keys || keys.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug('Deleting keys:', {
|
||||
keys,
|
||||
userId,
|
||||
})
|
||||
|
||||
await redisDataSource.redisClient?.del(keys)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const sendNewAccountVerificationEmail = async (user: {
|
|||
}
|
||||
|
||||
const result = await enqueueSendEmail({
|
||||
userId: user.id,
|
||||
to: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.confirmationTemplateId,
|
||||
|
|
@ -78,6 +79,7 @@ export const sendAccountChangeEmail = async (user: {
|
|||
}
|
||||
|
||||
const result = await enqueueSendEmail({
|
||||
userId: user.id,
|
||||
to: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.verificationTemplateId,
|
||||
|
|
@ -100,6 +102,7 @@ export const sendPasswordResetEmail = async (user: {
|
|||
}
|
||||
|
||||
const result = await enqueueSendEmail({
|
||||
userId: user.id,
|
||||
to: user.email,
|
||||
dynamicTemplateData: dynamicTemplateData,
|
||||
templateId: env.sendgrid.resetPasswordTemplateId,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Notification } from 'firebase-admin/messaging'
|
|||
import { DeepPartial, FindOptionsWhere, In } from 'typeorm'
|
||||
import { Profile } from '../entity/profile'
|
||||
import { StatusType, User } from '../entity/user'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx, getRepository, queryBuilderToRawSql } from '../repository'
|
||||
import { userRepository } from '../repository/user'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
|
@ -156,3 +157,27 @@ export const findUserAndPersonalization = async (id: string) => {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
const userCacheKey = (id: string) => `cache:user:${id}`
|
||||
|
||||
export const getCachedUser = async (id: string) => {
|
||||
logger.debug(`Getting user from cache: ${id}`)
|
||||
|
||||
const user = await redisDataSource.redisClient?.get(userCacheKey(id))
|
||||
if (!user) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return JSON.parse(user) as User
|
||||
}
|
||||
|
||||
export const cacheUser = async (user: User) => {
|
||||
logger.debug(`Caching user: ${user.id}`)
|
||||
|
||||
await redisDataSource.redisClient?.set(
|
||||
userCacheKey(user.id),
|
||||
JSON.stringify(user),
|
||||
'EX',
|
||||
600
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { DeepPartial, IsNull } from 'typeorm'
|
||||
import { Shortcut, UserPersonalization } from '../entity/user_personalization'
|
||||
import { authTrx } from '../repository'
|
||||
import { findLabelsByUserId } from './labels'
|
||||
import { findSubscriptionById } from './subscriptions'
|
||||
import { DeepPartial } from 'typeorm'
|
||||
import { Filter } from '../entity/filter'
|
||||
import { Subscription, SubscriptionStatus } from '../entity/subscription'
|
||||
import { Shortcut, UserPersonalization } from '../entity/user_personalization'
|
||||
import { redisDataSource } from '../redis_data_source'
|
||||
import { authTrx } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
import { findLabelsByUserId } from './labels'
|
||||
|
||||
export const findUserPersonalization = async (userId: string) => {
|
||||
return authTrx(
|
||||
|
|
@ -131,7 +132,7 @@ const userDefaultShortcuts = async (userId: string): Promise<Shortcut[]> => {
|
|||
id: label.id,
|
||||
type: 'label',
|
||||
name: label.name,
|
||||
section: 'library',
|
||||
section: 'search',
|
||||
label: label,
|
||||
filter: `in:all label:"${label.name}"`,
|
||||
}
|
||||
|
|
@ -166,10 +167,35 @@ const userDefaultShortcuts = async (userId: string): Promise<Shortcut[]> => {
|
|||
id: search.id,
|
||||
type: 'search',
|
||||
name: search.name,
|
||||
section: 'library',
|
||||
section: 'search',
|
||||
filter: search.filter,
|
||||
}
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const shortcutsCacheKey = (userId: string) => `cache:shortcuts:${userId}`
|
||||
|
||||
export const getShortcutsCache = async (userId: string) => {
|
||||
logger.debug(`Getting shortcuts from cache: ${userId}`)
|
||||
|
||||
const cachedShortcuts = await redisDataSource.redisClient?.get(
|
||||
shortcutsCacheKey(userId)
|
||||
)
|
||||
if (!cachedShortcuts) {
|
||||
return undefined
|
||||
}
|
||||
return JSON.parse(cachedShortcuts) as Shortcut[]
|
||||
}
|
||||
|
||||
export const cacheShortcuts = async (userId: string, shortcuts: Shortcut[]) => {
|
||||
logger.debug(`Caching shortcuts: ${userId}`)
|
||||
|
||||
await redisDataSource.redisClient?.set(
|
||||
shortcutsCacheKey(userId),
|
||||
JSON.stringify(shortcuts),
|
||||
'EX',
|
||||
600
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ export const corsConfig = {
|
|||
'capacitor://localhost',
|
||||
'http://localhost',
|
||||
],
|
||||
maxAge: 86400,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -647,9 +647,15 @@ export const contentConverter = (
|
|||
): contentConverterFunc | undefined => {
|
||||
switch (format) {
|
||||
case ArticleFormat.Markdown:
|
||||
return htmlToMarkdown
|
||||
return (html: string) => {
|
||||
return ''
|
||||
}
|
||||
// return htmlToMarkdown
|
||||
case ArticleFormat.HighlightedMarkdown:
|
||||
return htmlToHighlightedMarkdown
|
||||
return (html: string) => {
|
||||
return ''
|
||||
}
|
||||
// return htmlToHighlightedMarkdown
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ export const createTestLibraryItem = async (
|
|||
const item = {
|
||||
user: { id: userId },
|
||||
title: 'test title',
|
||||
originalContent: '<p>test content</p>',
|
||||
originalUrl: `https://blog.omnivore.app/test-url-${generateFakeUuid()}`,
|
||||
slug: 'test-with-omnivore',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { getRepository } from '../../src/repository'
|
|||
import { createGroup, deleteGroup } from '../../src/services/groups'
|
||||
import { createLabel, deleteLabels } from '../../src/services/labels'
|
||||
import {
|
||||
countLibraryItems,
|
||||
createLibraryItems,
|
||||
createOrUpdateLibraryItem,
|
||||
CreateOrUpdateLibraryItemArgs,
|
||||
|
|
@ -2451,19 +2452,16 @@ describe('Article API', () => {
|
|||
})
|
||||
|
||||
it('empties the trash', async () => {
|
||||
let response = await graphqlRequest(
|
||||
searchQuery('in:trash'),
|
||||
authToken
|
||||
).expect(200)
|
||||
expect(response.body.data.search.pageInfo.totalCount).to.eql(5)
|
||||
|
||||
await graphqlRequest(emptyTrashQuery(), authToken).expect(200)
|
||||
|
||||
response = await graphqlRequest(
|
||||
searchQuery('in:trash'),
|
||||
authToken
|
||||
).expect(200)
|
||||
expect(response.body.data.search.pageInfo.totalCount).to.eql(0)
|
||||
const count = await countLibraryItems(
|
||||
{
|
||||
query: 'in:trash',
|
||||
includeDeleted: true,
|
||||
},
|
||||
user.id
|
||||
)
|
||||
expect(count).to.eql(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
9
packages/db/migrations/0185.do.drop_original_content.sql
Executable file
9
packages/db/migrations/0185.do.drop_original_content.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: DO
|
||||
-- Name: drop_original_content
|
||||
-- Description: Drop original_content column from library_item table
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.library_item DROP COLUMN IF EXISTS original_content;
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0185.undo.drop_original_content.sql
Executable file
9
packages/db/migrations/0185.undo.drop_original_content.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: drop_original_content
|
||||
-- Description: Drop original_content column from library_item table
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.library_item ADD COLUMN IF NOT EXISTS original_content text;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -9,25 +9,27 @@ PG_USER = os.getenv('PG_USER', 'app_user')
|
|||
PG_PASSWORD = os.getenv('PG_PASSWORD', 'app_pass')
|
||||
PG_DB = os.getenv('PG_DB', 'omnivore')
|
||||
PG_TIMEOUT = os.getenv('PG_TIMEOUT', 10)
|
||||
BATCH_SIZE = os.getenv('BATCH_SIZE', 100)
|
||||
|
||||
|
||||
def batch_update_library_items(conn):
|
||||
batch_size = 100
|
||||
# update original_content to NULL in batches
|
||||
with conn.cursor() as cursor:
|
||||
while True:
|
||||
cursor.execute(f"""
|
||||
UPDATE omnivore.library_item
|
||||
SET original_content = NULL
|
||||
WHERE ctid IN (
|
||||
SELECT ctid
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM omnivore.library_item
|
||||
WHERE original_content IS NOT NULL
|
||||
LIMIT {batch_size}
|
||||
ORDER BY user_id
|
||||
LIMIT {BATCH_SIZE}
|
||||
)
|
||||
""")
|
||||
rows_updated = cursor.rowcount
|
||||
conn.commit()
|
||||
|
||||
rows_updated = cursor.rowcount
|
||||
if rows_updated == 0:
|
||||
break
|
||||
|
||||
|
|
|
|||
8
packages/export-handler/.dockerignore
Normal file
8
packages/export-handler/.dockerignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
node_modules
|
||||
build
|
||||
test
|
||||
.env*
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.eslintrc
|
||||
.eslintignore
|
||||
2
packages/export-handler/.eslintignore
Normal file
2
packages/export-handler/.eslintignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
build/
|
||||
6
packages/export-handler/.eslintrc
Normal file
6
packages/export-handler/.eslintrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "tsconfig.json"
|
||||
}
|
||||
}
|
||||
27
packages/export-handler/Dockerfile
Normal file
27
packages/export-handler/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FROM node:18.16-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add g++ make python3
|
||||
|
||||
ENV PORT 8080
|
||||
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
COPY tsconfig.json .
|
||||
|
||||
COPY /packages/export-handler/package.json ./packages/export-handler/package.json
|
||||
|
||||
RUN yarn install --pure-lockfile
|
||||
|
||||
COPY /packages/export-handler ./packages/export-handler
|
||||
RUN yarn workspace @omnivore/export-handler build
|
||||
|
||||
# After building, fetch the production dependencies
|
||||
RUN rm -rf /app/packages/export-handler/node_modules
|
||||
RUN rm -rf /app/node_modules
|
||||
RUN yarn install --pure-lockfile --production
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["yarn", "workspace", "@omnivore/export-handler", "start"]
|
||||
5
packages/export-handler/mocha-config.json
Normal file
5
packages/export-handler/mocha-config.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"timeout": 10000
|
||||
}
|
||||
40
packages/export-handler/package.json
Normal file
40
packages/export-handler/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "@omnivore/export-handler",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "build/src/index.js",
|
||||
"files": [
|
||||
"build/src"
|
||||
],
|
||||
"keywords": [],
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts,js,tsx,jsx",
|
||||
"compile": "tsc",
|
||||
"build": "tsc",
|
||||
"start": "functions-framework --target=exportHandler",
|
||||
"dev": "concurrently \"tsc -w\" \"nodemon --watch ./build/ --exec npm run start\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"@google-cloud/storage": "^7.0.1",
|
||||
"@omnivore-app/api": "^1.0.4",
|
||||
"@omnivore/utils": "1.0.0",
|
||||
"@sentry/serverless": "^7.77.0",
|
||||
"csv-stringify": "^6.4.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemon": "^2.0.15",
|
||||
"uuid": "^8.3.1"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
199
packages/export-handler/src/index.ts
Normal file
199
packages/export-handler/src/index.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { File, Storage } from '@google-cloud/storage'
|
||||
import { Omnivore } from '@omnivore-app/api'
|
||||
import { RedisDataSource } from '@omnivore/utils'
|
||||
import * as Sentry from '@sentry/serverless'
|
||||
import { stringify } from 'csv-stringify'
|
||||
import * as dotenv from 'dotenv'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { queueEmailJob } from './job'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
Sentry.GCPFunction.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
tracesSampleRate: 0,
|
||||
})
|
||||
|
||||
interface Claims {
|
||||
uid: string
|
||||
token: string
|
||||
}
|
||||
|
||||
const storage = new Storage()
|
||||
const GCS_BUCKET = process.env.GCS_UPLOAD_BUCKET || 'omnivore-export'
|
||||
|
||||
const createGCSFile = (bucket: string, filename: string): File => {
|
||||
return storage.bucket(bucket).file(filename)
|
||||
}
|
||||
|
||||
const createSignedUrl = async (file: File): Promise<string> => {
|
||||
const signedUrl = await file.getSignedUrl({
|
||||
action: 'read',
|
||||
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
|
||||
})
|
||||
return signedUrl[0]
|
||||
}
|
||||
|
||||
export const sendExportCompletedEmail = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
userId: string,
|
||||
urlToDownload: string
|
||||
) => {
|
||||
return queueEmailJob(redisDataSource, {
|
||||
userId,
|
||||
subject: 'Your Omnivore export is ready',
|
||||
html: `<p>Your export is ready. You can download it from the following link: <a href="${urlToDownload}">${urlToDownload}</a></p>`,
|
||||
})
|
||||
}
|
||||
|
||||
export const exporter = Sentry.GCPFunction.wrapHttpFunction(
|
||||
async (req, res) => {
|
||||
console.log('start to export')
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET
|
||||
if (!JWT_SECRET) {
|
||||
return res.status(500).send({ errorCode: 'ENV_NOT_CONFIGURED' })
|
||||
}
|
||||
|
||||
const token = req.get('Omnivore-Authorization')
|
||||
if (!token) {
|
||||
return res.status(401).send({ errorCode: 'INVALID_TOKEN' })
|
||||
}
|
||||
|
||||
let claims: Claims
|
||||
try {
|
||||
claims = jwt.verify(token, JWT_SECRET) as Claims
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return res.status(401).send({ errorCode: 'INVALID_TOKEN' })
|
||||
}
|
||||
|
||||
const redisDataSource = new RedisDataSource({
|
||||
cache: {
|
||||
url: process.env.REDIS_URL,
|
||||
cert: process.env.REDIS_CERT,
|
||||
},
|
||||
mq: {
|
||||
url: process.env.MQ_REDIS_URL,
|
||||
cert: process.env.MQ_REDIS_CERT,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
// write the exported data to a csv file and upload it to gcs
|
||||
// path style: exports/<uid>/<date>/<uuid>.csv
|
||||
const dateStr = new Date().toISOString()
|
||||
const fileUuid = uuidv4()
|
||||
const fullPath = `exports/${claims.uid}/${dateStr}/${fileUuid}.csv`
|
||||
const file = createGCSFile(GCS_BUCKET, fullPath)
|
||||
|
||||
// stringify the data and pipe it to the write_stream
|
||||
const stringifier = stringify({
|
||||
header: true,
|
||||
columns: [
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'labels',
|
||||
'author',
|
||||
'site_name',
|
||||
'original_url',
|
||||
'slug',
|
||||
'updated_at',
|
||||
'saved_at',
|
||||
'type',
|
||||
'published_at',
|
||||
'url',
|
||||
'thumbnail',
|
||||
'read_at',
|
||||
'word_count',
|
||||
'reading_progress_percent',
|
||||
'archived_at',
|
||||
],
|
||||
})
|
||||
|
||||
stringifier
|
||||
.pipe(
|
||||
file.createWriteStream({
|
||||
contentType: 'text/csv',
|
||||
})
|
||||
)
|
||||
.on('error', (err) => {
|
||||
console.error('error writing to file', err)
|
||||
})
|
||||
.on('finish', () => {
|
||||
console.log('done writing to file')
|
||||
})
|
||||
|
||||
// fetch data from the database
|
||||
const omnivore = new Omnivore({
|
||||
apiKey: claims.token,
|
||||
})
|
||||
|
||||
let cursor = 0
|
||||
let hasNext = false
|
||||
do {
|
||||
const response = await omnivore.items.search({
|
||||
first: 100,
|
||||
after: cursor,
|
||||
includeContent: false,
|
||||
})
|
||||
|
||||
const items = response.edges.map((edge) => edge.node)
|
||||
cursor = response.pageInfo.endCursor
|
||||
? parseInt(response.pageInfo.endCursor)
|
||||
: 0
|
||||
hasNext = response.pageInfo.hasNextPage
|
||||
|
||||
// write data to the csv file
|
||||
if (items.length > 0) {
|
||||
// write the list of urls, state and labels to the stream
|
||||
items.forEach((item) =>
|
||||
stringifier.write({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
labels: item.labels?.map((label) => label.name).join(','),
|
||||
author: item.author,
|
||||
site_name: item.siteName,
|
||||
original_url: item.originalArticleUrl,
|
||||
slug: item.slug,
|
||||
updated_at: item.updatedAt,
|
||||
saved_at: item.savedAt,
|
||||
type: item.pageType,
|
||||
published_at: item.publishedAt,
|
||||
url: item.url,
|
||||
thumbnail: item.image,
|
||||
read_at: item.readAt,
|
||||
word_count: item.wordsCount,
|
||||
reading_progress_percent: item.readingProgressPercent,
|
||||
archived_at: item.archivedAt,
|
||||
})
|
||||
)
|
||||
|
||||
// sleep for 1 second to avoid rate limiting
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
} while (hasNext)
|
||||
|
||||
stringifier.end()
|
||||
|
||||
// generate a temporary signed url for the csv file
|
||||
const signedUrl = await createSignedUrl(file)
|
||||
console.log('signed url', signedUrl)
|
||||
|
||||
await sendExportCompletedEmail(redisDataSource, claims.uid, signedUrl)
|
||||
|
||||
console.log('done')
|
||||
} catch (err) {
|
||||
console.error('export failed', err)
|
||||
|
||||
return res.status(500).send({ errorCode: 'INTERNAL_SERVER_ERROR' })
|
||||
} finally {
|
||||
await redisDataSource.shutdown()
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
23
packages/export-handler/src/job.ts
Normal file
23
packages/export-handler/src/job.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { RedisDataSource } from '@omnivore/utils'
|
||||
import { Queue } from 'bullmq'
|
||||
|
||||
const QUEUE_NAME = 'omnivore-backend-queue'
|
||||
export const SEND_EMAIL_JOB = 'send-email'
|
||||
|
||||
interface SendEmailJobData {
|
||||
userId: string
|
||||
from?: string
|
||||
subject?: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
export const queueEmailJob = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
data: SendEmailJobData
|
||||
) => {
|
||||
const queue = new Queue(QUEUE_NAME, {
|
||||
connection: redisDataSource.queueRedisClient,
|
||||
})
|
||||
|
||||
await queue.add(SEND_EMAIL_JOB, data)
|
||||
}
|
||||
8
packages/export-handler/test/stub.test.ts
Normal file
8
packages/export-handler/test/stub.test.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('stub test', () => {
|
||||
it('should pass', () => {
|
||||
expect(true).to.be.true
|
||||
})
|
||||
})
|
||||
8
packages/export-handler/tsconfig.json
Normal file
8
packages/export-handler/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
|
|
@ -1,4 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
||||
readabilityjs/
|
||||
src/generated/
|
||||
build/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ FROM node:18.16-alpine
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
RUN apk add g++ make python3
|
||||
|
||||
ENV PORT 8080
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ FROM node:18.16-alpine
|
|||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
RUN apk add g++ make python3
|
||||
|
||||
ENV PORT 8080
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"@sentry/serverless": "^7.77.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"axios": "^1.2.2",
|
||||
"bullmq": "^5.1.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"dompurify": "^2.4.3",
|
||||
"fs-extra": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,12 @@ const parseDate = (date: string): Date => {
|
|||
|
||||
export const importCsv = async (ctx: ImportContext, stream: Stream) => {
|
||||
// create metrics in redis
|
||||
await createMetrics(ctx.redisClient, ctx.userId, ctx.taskId, ctx.source)
|
||||
await createMetrics(
|
||||
ctx.redisDataSource.cacheClient,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ctx.source
|
||||
)
|
||||
|
||||
const parser = parse({
|
||||
headers: true,
|
||||
|
|
@ -68,7 +73,7 @@ export const importCsv = async (ctx: ImportContext, stream: Stream) => {
|
|||
|
||||
// update total counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.TOTAL
|
||||
|
|
@ -79,7 +84,7 @@ export const importCsv = async (ctx: ImportContext, stream: Stream) => {
|
|||
ctx.countImported += 1
|
||||
// update started counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.STARTED
|
||||
|
|
@ -96,7 +101,7 @@ export const importCsv = async (ctx: ImportContext, stream: Stream) => {
|
|||
ctx.countFailed += 1
|
||||
// update invalid counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.INVALID
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ import { RedisDataSource } from '@omnivore/utils'
|
|||
import * as Sentry from '@sentry/serverless'
|
||||
import axios from 'axios'
|
||||
import 'dotenv/config'
|
||||
import Redis from 'ioredis'
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { Stream } from 'node:stream'
|
||||
import * as path from 'path'
|
||||
import { promisify } from 'util'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { importCsv } from './csv'
|
||||
import { queueEmailJob } from './job'
|
||||
import { importMatterArchive } from './matterHistory'
|
||||
import { ImportStatus, updateMetrics } from './metrics'
|
||||
import { CONTENT_FETCH_URL, createCloudTask, emailUserUrl } from './task'
|
||||
import { CONTENT_FETCH_URL, createCloudTask } from './task'
|
||||
|
||||
export enum ArticleSavingRequestStatus {
|
||||
Failed = 'FAILED',
|
||||
|
|
@ -57,7 +57,7 @@ export type ImportContext = {
|
|||
countFailed: number
|
||||
urlHandler: UrlHandler
|
||||
contentHandler: ContentHandler
|
||||
redisClient: Redis
|
||||
redisDataSource: RedisDataSource
|
||||
taskId: string
|
||||
source: string
|
||||
}
|
||||
|
|
@ -118,54 +118,40 @@ const importURL = async (
|
|||
})
|
||||
}
|
||||
|
||||
const createEmailCloudTask = async (userId: string, payload: unknown) => {
|
||||
if (!process.env.JWT_SECRET) {
|
||||
throw 'Envrionment not setup correctly'
|
||||
}
|
||||
|
||||
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 1 day
|
||||
const authToken = (await signToken(
|
||||
{ uid: userId, exp },
|
||||
process.env.JWT_SECRET
|
||||
)) as string
|
||||
const headers = {
|
||||
'Omnivore-Authorization': authToken,
|
||||
}
|
||||
|
||||
return createCloudTask(
|
||||
emailUserUrl(),
|
||||
payload,
|
||||
headers,
|
||||
'omnivore-email-queue'
|
||||
)
|
||||
}
|
||||
|
||||
const sendImportFailedEmail = async (userId: string) => {
|
||||
return createEmailCloudTask(userId, {
|
||||
const sendImportFailedEmail = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
userId: string
|
||||
) => {
|
||||
return queueEmailJob(redisDataSource, {
|
||||
userId,
|
||||
subject: 'Your Omnivore import failed.',
|
||||
body: `There was an error importing your file. Please ensure you uploaded the correct file type, if you need help, please email feedback@omnivore.app`,
|
||||
html: `There was an error importing your file. Please ensure you uploaded the correct file type, if you need help, please email feedback@omnivore.app`,
|
||||
})
|
||||
}
|
||||
|
||||
export const sendImportStartedEmail = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
userId: string,
|
||||
urlsEnqueued: number,
|
||||
urlsFailed: number
|
||||
) => {
|
||||
return createEmailCloudTask(userId, {
|
||||
return queueEmailJob(redisDataSource, {
|
||||
userId,
|
||||
subject: 'Your Omnivore import has started',
|
||||
body: `We have started processing ${urlsEnqueued} URLs. ${urlsFailed} URLs are invalid.`,
|
||||
html: `We have started processing ${urlsEnqueued} URLs. ${urlsFailed} URLs are invalid.`,
|
||||
})
|
||||
}
|
||||
|
||||
export const sendImportCompletedEmail = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
userId: string,
|
||||
urlsImported: number,
|
||||
urlsFailed: number
|
||||
) => {
|
||||
return createEmailCloudTask(userId, {
|
||||
return queueEmailJob(redisDataSource, {
|
||||
userId,
|
||||
subject: 'Your Omnivore import has finished',
|
||||
body: `We have finished processing ${
|
||||
html: `We have finished processing ${
|
||||
urlsImported + urlsFailed
|
||||
} URLs. ${urlsImported} URLs have been added to your library. ${urlsFailed} URLs failed to be parsed.`,
|
||||
})
|
||||
|
|
@ -298,7 +284,10 @@ const contentHandler = async (
|
|||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const handleEvent = async (data: StorageEvent, redisClient: Redis) => {
|
||||
const handleEvent = async (
|
||||
data: StorageEvent,
|
||||
redisDataSource: RedisDataSource
|
||||
) => {
|
||||
if (shouldHandle(data)) {
|
||||
const handler = handlerForFile(data.name)
|
||||
if (!handler) {
|
||||
|
|
@ -329,7 +318,7 @@ const handleEvent = async (data: StorageEvent, redisClient: Redis) => {
|
|||
countFailed: 0,
|
||||
urlHandler,
|
||||
contentHandler,
|
||||
redisClient,
|
||||
redisDataSource,
|
||||
taskId: data.name,
|
||||
source: importSource(data.name),
|
||||
}
|
||||
|
|
@ -337,9 +326,14 @@ const handleEvent = async (data: StorageEvent, redisClient: Redis) => {
|
|||
await handler(ctx, stream)
|
||||
|
||||
if (ctx.countImported > 0) {
|
||||
await sendImportStartedEmail(userId, ctx.countImported, ctx.countFailed)
|
||||
await sendImportStartedEmail(
|
||||
ctx.redisDataSource,
|
||||
userId,
|
||||
ctx.countImported,
|
||||
ctx.countFailed
|
||||
)
|
||||
} else {
|
||||
await sendImportFailedEmail(userId)
|
||||
await sendImportFailedEmail(ctx.redisDataSource, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +371,7 @@ export const importHandler = Sentry.GCPFunction.wrapHttpFunction(
|
|||
})
|
||||
|
||||
try {
|
||||
await handleEvent(obj, redisDataSource.cacheClient)
|
||||
await handleEvent(obj, redisDataSource)
|
||||
} catch (err) {
|
||||
console.log('error handling event', { err, obj })
|
||||
throw err
|
||||
|
|
@ -436,7 +430,7 @@ export const importMetricsCollector = Sentry.GCPFunction.wrapHttpFunction(
|
|||
try {
|
||||
// update metrics
|
||||
await updateMetrics(
|
||||
redisDataSource.cacheClient,
|
||||
redisDataSource,
|
||||
userId,
|
||||
req.body.taskId,
|
||||
req.body.status
|
||||
|
|
|
|||
23
packages/import-handler/src/job.ts
Normal file
23
packages/import-handler/src/job.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { RedisDataSource } from '@omnivore/utils'
|
||||
import { Queue } from 'bullmq'
|
||||
|
||||
const QUEUE_NAME = 'omnivore-backend-queue'
|
||||
export const SEND_EMAIL_JOB = 'send-email'
|
||||
|
||||
interface SendEmailJobData {
|
||||
userId: string
|
||||
from?: string
|
||||
subject?: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
export const queueEmailJob = async (
|
||||
redisDataSource: RedisDataSource,
|
||||
data: SendEmailJobData
|
||||
) => {
|
||||
const queue = new Queue(QUEUE_NAME, {
|
||||
connection: redisDataSource.queueRedisClient,
|
||||
})
|
||||
|
||||
await queue.add(SEND_EMAIL_JOB, data)
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ export const importMatterHistoryCsv = async (
|
|||
const url = new URL(row['URL'])
|
||||
// update total counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.TOTAL
|
||||
|
|
@ -46,7 +46,7 @@ export const importMatterHistoryCsv = async (
|
|||
ctx.countImported += 1
|
||||
// update started counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.STARTED
|
||||
|
|
@ -219,7 +219,7 @@ const handleMatterHistoryRow = async (
|
|||
ctx.countFailed += 1
|
||||
// update failed counter
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.FAILED
|
||||
|
|
@ -254,7 +254,7 @@ export const importMatterArchive = async (
|
|||
try {
|
||||
// create metrics in redis
|
||||
await createMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource.cacheClient,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
'matter-importer'
|
||||
|
|
@ -273,7 +273,7 @@ export const importMatterArchive = async (
|
|||
try {
|
||||
// update total metrics
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.TOTAL
|
||||
|
|
@ -284,7 +284,7 @@ export const importMatterArchive = async (
|
|||
ctx.countImported += 1
|
||||
// update started metrics
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.STARTED
|
||||
|
|
@ -294,7 +294,7 @@ export const importMatterArchive = async (
|
|||
ctx.countFailed += 1
|
||||
// update failed metrics
|
||||
await updateMetrics(
|
||||
ctx.redisClient,
|
||||
ctx.redisDataSource,
|
||||
ctx.userId,
|
||||
ctx.taskId,
|
||||
ImportStatus.FAILED
|
||||
|
|
|
|||
|
|
@ -47,13 +47,14 @@ export const createMetrics = async (
|
|||
}
|
||||
|
||||
export const updateMetrics = async (
|
||||
redisClient: Redis,
|
||||
redisDataSource: RedisDataSource,
|
||||
userId: string,
|
||||
taskId: string,
|
||||
status: ImportStatus
|
||||
) => {
|
||||
const key = `import:${userId}:${taskId}`
|
||||
|
||||
const redisClient = redisDataSource.cacheClient
|
||||
/**
|
||||
* Define our command
|
||||
*/
|
||||
|
|
@ -109,7 +110,12 @@ export const updateMetrics = async (
|
|||
if ((state as ImportTaskState) == ImportTaskState.FINISHED) {
|
||||
const metrics = await getMetrics(redisClient, userId, taskId)
|
||||
if (metrics) {
|
||||
await sendImportCompletedEmail(userId, metrics.imported, metrics.failed)
|
||||
await sendImportCompletedEmail(
|
||||
redisDataSource,
|
||||
userId,
|
||||
metrics.imported,
|
||||
metrics.failed
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,6 @@ import { CloudTasksClient, protos } from '@google-cloud/tasks'
|
|||
|
||||
const cloudTask = new CloudTasksClient()
|
||||
|
||||
export const emailUserUrl = () => {
|
||||
const envar = process.env.INTERNAL_SVC_ENDPOINT
|
||||
if (envar) {
|
||||
return envar + 'api/user/email'
|
||||
}
|
||||
throw 'INTERNAL_SVC_ENDPOINT not set'
|
||||
}
|
||||
|
||||
export const CONTENT_FETCH_URL = process.env.CONTENT_FETCH_GCF_URL
|
||||
|
||||
export const createCloudTask = async (
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe('Test csv importer', () => {
|
|||
},
|
||||
})
|
||||
|
||||
stub = stubImportCtx(redisDataSource.cacheClient)
|
||||
stub = stubImportCtx(redisDataSource)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ describe('matter importer', () => {
|
|||
},
|
||||
})
|
||||
|
||||
stub = stubImportCtx(redisDataSource.cacheClient)
|
||||
stub = stubImportCtx(redisDataSource)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { Readability } from '@omnivore/readability'
|
||||
import Redis from 'ioredis'
|
||||
import { RedisDataSource } from '@omnivore/utils'
|
||||
import { ArticleSavingRequestStatus, ImportContext } from '../src'
|
||||
|
||||
export const stubImportCtx = (redisClient: Redis): ImportContext => {
|
||||
export const stubImportCtx = (
|
||||
redisDataSource: RedisDataSource
|
||||
): ImportContext => {
|
||||
return {
|
||||
userId: '',
|
||||
countImported: 0,
|
||||
|
|
@ -24,7 +26,7 @@ export const stubImportCtx = (redisClient: Redis): ImportContext => {
|
|||
): Promise<void> => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
redisClient,
|
||||
redisDataSource,
|
||||
taskId: '',
|
||||
source: 'csv-importer',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@
|
|||
"linkedom": "^0.14.9",
|
||||
"mocha": "^8.2.0",
|
||||
"nock": "^13.3.1",
|
||||
"puppeteer-core": "^22.8.0",
|
||||
"puppeteer-extra": "^3.3.4",
|
||||
"puppeteer-extra-plugin-adblocker": "^2.13.5",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.1",
|
||||
"puppeteer-core": "^22.12.1",
|
||||
"puppeteer-extra": "^3.3.6",
|
||||
"puppeteer-extra-plugin-adblocker": "^2.13.6",
|
||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||
"sinon": "^7.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -108,27 +108,25 @@ async function fetchSource(url, callbackFn) {
|
|||
|
||||
const browser = await puppeteer.launch({
|
||||
args: [
|
||||
'--allow-running-insecure-content',
|
||||
'--autoplay-policy=user-gesture-required',
|
||||
'--disable-component-update',
|
||||
'--disable-domain-reliability',
|
||||
'--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process',
|
||||
'--disable-print-preview',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-site-isolation-trials',
|
||||
'--disable-speech-api',
|
||||
'--disable-web-security',
|
||||
'--disk-cache-size=33554432',
|
||||
'--enable-features=SharedArrayBuffer',
|
||||
'--hide-scrollbars',
|
||||
'--disable-gpu',
|
||||
'--mute-audio',
|
||||
'--no-default-browser-check',
|
||||
'--no-pings',
|
||||
'--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--window-size=1920,1080',
|
||||
'--disable-extensions',
|
||||
'--disable-dev-shm-usage',
|
||||
'--no-first-run',
|
||||
'--disable-background-networking',
|
||||
'--disable-gpu',
|
||||
'--disable-software-rasterizer',
|
||||
],
|
||||
defaultViewport: {
|
||||
deviceScaleFactor: 1,
|
||||
|
|
@ -138,15 +136,18 @@ async function fetchSource(url, callbackFn) {
|
|||
isMobile: false,
|
||||
width: 1920,
|
||||
},
|
||||
headless: true,
|
||||
headless: 'shell',
|
||||
dumpio: true, // show console logs in the terminal
|
||||
executablePath: process.env.CHROMIUM_PATH || '/opt/homebrew/bin/chromium',
|
||||
// filter out targets
|
||||
targetFilter: (target) =>
|
||||
target.type() !== 'other' || !!target.url(),
|
||||
})
|
||||
|
||||
const page = await browser.newPage()
|
||||
if (!enableJavascriptForUrl(url)) {
|
||||
await page.setJavaScriptEnabled(false)
|
||||
}
|
||||
await page.setUserAgent(userAgentForUrl(url))
|
||||
|
||||
try {
|
||||
/*
|
||||
|
|
@ -155,18 +156,29 @@ async function fetchSource(url, callbackFn) {
|
|||
* mathjax content when present.
|
||||
*/
|
||||
await page.setRequestInterception(true)
|
||||
|
||||
let requestCount = 0
|
||||
page.on('request', (request) => {
|
||||
if (
|
||||
request.resourceType() === 'script' &&
|
||||
request.url().toLowerCase().indexOf('mathjax') > -1
|
||||
) {
|
||||
request.abort()
|
||||
} else {
|
||||
request.continue()
|
||||
}
|
||||
;(async () => {
|
||||
if (request.resourceType() === 'font') {
|
||||
// Disallow fonts from loading
|
||||
return request.abort()
|
||||
}
|
||||
if (requestCount++ > 100) {
|
||||
return request.abort()
|
||||
}
|
||||
if (
|
||||
request.resourceType() === 'script' &&
|
||||
request.url().toLowerCase().indexOf('mathjax') > -1
|
||||
) {
|
||||
return request.abort()
|
||||
}
|
||||
|
||||
await request.continue()
|
||||
})()
|
||||
})
|
||||
|
||||
await page.goto(url, { waitUntil: ['networkidle2'] })
|
||||
await page.goto(url, { waitUntil: ['networkidle0'] })
|
||||
|
||||
/* scroll with a 5 second timeout */
|
||||
await Promise.race([
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,33 @@
|
|||
<div><p>
|
||||
Former President <a href="https://www.rawstory.com/trump-musk/" target="_blank">Donald Trump's</a> rambling interview with <a href="https://www.rawstory.com/elon-musk-trump-2668952236/" target="_blank">Elon Musk</a> touched on climate change, border security, foreign dictators and inflation.
|
||||
</p><p>
|
||||
But it was his apparent appreciation of "beautiful" <a href="https://www.rawstory.com/kamala-harris/">Kamala Harris</a> — and his comparison to his wife, Melania <a href="https://www.rawstory.com/trump-news/">Trump</a> — that left the internet with raised eyebrows.
|
||||
</p><p>
|
||||
During Trump's <a href="https://www.rawstory.com/elon-musk-trump-2668951495/" target="_blank">interview on X with tech billionaire Musk</a>, the MAGA leader said his Democratic rival has received a "free ride" from the media so far.
|
||||
</p><p>
|
||||
"But I saw a picture of her on Time magazine today, she looks like the most beautiful actress ever to live," Trump said. "It was a drawing. And actually she looked very much like a great first lady: Melania. She didn't look like Camilla. That's right. But of course she's a beautiful woman. So we'll leave it at that."
|
||||
</p><p>
|
||||
The comment left the internet collectively cringed at the comment, including journalist Aaron Rupar, who called the recording "<a href="https://x.com/atrupar/status/1823183367380034024" target="_blank">weird stuff</a>!"
|
||||
</p><p>
|
||||
"I KNEW IT. His <a href="https://x.com/AshaRangappa_/status/1823182857533337645" target="_blank">brain is short-circuiting</a> because she is attractive AND formidable. lol he is toast," chided former FBI special agent and legal contributer Asha Rangappa.
|
||||
</p><p>
|
||||
She added that Trump "recognizes and is slightly scared of — and in a weird way, craves the approval of — strong, intelligent women," such as his former Democratic opponent Hillary Clinton and former House Speaker <a href="https://www.rawstory.com/tag/nancy-pelosi">Nancy Pelosi</a>.
|
||||
</p><p>
|
||||
"Some Mommy issues going on there," she added, noting that Trump equates attractive women with "'<a href="https://x.com/AshaRangappa_/status/1823183719148171361" target="_blank">dumb' or weak</a>," so he can "sexualize and exploit and dismiss."
|
||||
</p><p>
|
||||
"This is the point of the call where I say, 'Well, it's been <a href="https://x.com/kelsientaggart/status/1823186094495846647" target="_blank">great catching up grandpa</a>, but I've got to go,' quipped Kelsie Taggart, digital media director at the left-leaning American Bridge PAC.
|
||||
</p><p>
|
||||
"Donald Trump mentions this Time magazine cover, says Kamala Harris looks like an actress in the illustration and then says she looks like … his wife, <a href="https://x.com/ccadelago/status/1823182901875449929" target="_blank">Melania Trump</a>. ???" questioned Christopher Cadelago, Politico's <a href="https://www.rawstory.com/tag/california">California</a> bureau chief, wrote on X, attaching a photo of the image.
|
||||
</p><p>
|
||||
"<a href="https://x.com/RpsAgainstTrump/status/1823189790864376209" target="_blank">What??</a>" questioned the Never-Trump account Republicans against Trump.
|
||||
</p><p>
|
||||
"Trump basically just said the drawing of Kamala on time magazine was <a href="https://x.com/JessicaLBurbank/status/1823182494440816787" target="_blank">hot and compared it to his wife Melania</a> lol," laughed Jessica Burbank, of the More Perfect Union podcast.
|
||||
</p><div>
|
||||
<span>CONTINUE READING</span><span>Show less</span>
|
||||
</div><p>
|
||||
A 72-year-old Colorado woman who stormed the Capitol on <a href="https://www.rawstory.com/trump-mini-trial/" target="_blank">January 6, 2021</a>, will avoid jail time after a judge sentenced her to a year of probation and a $103,000 fine Monday, <a href="https://coloradosun.com/2024/08/12/rebecca-lavrenz-praying-grandma-jan-6-sentenc/" target="_blank">the Colorado Sun reports.</a>
|
||||
</p><p>
|
||||
Rebecca Lavrenz, who took on the moniker “<a href="https://www.rawstory.com/trump-accused-of-invoking-jan-6/" target="_blank">J6</a> Praying Grandma,” was convicted on four misdemeanor counts for entering the Capitol and disorderly conduct for participating in the insurrection, according to the Sun. Prosecutors initially pushed for Lavrenz to serve up to 10 months in prison for continuing to defend the insurrection, writing in a recent filing that “her unrepentant promotion of the riot is powerful evidence that she continues to pose a threat to future acts of political violence.”
|
||||
</p><p>
|
||||
The Colorado Sun noted that Lavrenz has <a href="https://www.givesendgo.com/rebeccalavrenz" target="_blank">raised</a> <a href="https://www.givesendgo.com/rebeccalavrenzJ6" target="_blank">more than</a> $230,000 via online fundraisers by promoting her participation in the riot.
|
||||
</p></div>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"title": "'Interview crashed and burst into flames': Trump and Musk mocked amid stream's launch woes",
|
||||
"byline": "Leigh Tauss",
|
||||
"dir": null,
|
||||
"excerpt": "The failure to launch of Elon Musk's planned audio live stream on X with former President Donald Trump quickly became the subject of online mockery on the very same social platform, including by the official campaign of Democratic presidential nominee Kamala Harris.\"BREAKING: Twitter,\" the pro-Harri...",
|
||||
"siteName": "Raw Story - Celebrating 20 Years of Independent Journalism",
|
||||
"siteIcon": "https://assets.rbl.ms/24986912/origin.png",
|
||||
"previewImage": "https://www.rawstory.com/media-library/chief-executive-officer-of-spacex-and-tesla-and-owner-of-twitter-elon-musk-attends-the-viva-technology-conference-dedicated-to.jpg?id=53161798&width=1200&height=600&coordinates=0%2C0%2C0%2C104",
|
||||
"publishedDate": "2024-08-13T01:09:23.000Z",
|
||||
"language": "English",
|
||||
"readerable": true
|
||||
}
|
||||
216
packages/readabilityjs/test/test-pages/raw-story/expected.html
Normal file
216
packages/readabilityjs/test/test-pages/raw-story/expected.html
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<DIV class="page" id="readability-page-1">
|
||||
<div>
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- [END ITP COOKIES] -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- Chartbeat Subscriptions -->
|
||||
<!-- End Chartbeat Subscriptions -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<div id="sTop_Bar_0_0_11_0_0_19">
|
||||
<div id="sTop_Bar_0_0_11_0_0_19_0_0_0">
|
||||
<p><a id="sTop_Bar_0_0_11_0_0_19_0_0_0_0" href="https://www.rawstory.com/st/Puzzler" target="_blank"><img alt="" width="80" height="81" src="https://www.rawstory.com/media-library/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbWFnZSI6Imh0dHBzOi8vYXNzZXRzLnJibC5tcy81MDU1NTU0NS9vcmlnaW4ucG5nIiwiZXhwaXJlc19hdCI6MTc2MzMyOTkwM30.C-AXJe9KD0htsRtiDfInMKrHSznDd3XXhA3WVdEauBk/image.png?width=80&height=81"></a></p><!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
</div>
|
||||
<p><a href="http://fakehost/" target="_self"><img src="https://assets.rbl.ms/33391448/origin.png" alt="RawStory"></a></p>
|
||||
</div>
|
||||
<div id="sTop_Bar_0_0_11_0_0_20">
|
||||
<div id="sTop_Bar_0_0_11_0_0_20_0_0_0">
|
||||
<p><a id="sTop_Bar_0_0_11_0_0_20_0_0_0_0" href="https://www.rawstory.com/st/Puzzler" target="_blank"><img alt="" width="80" height="81" src="https://www.rawstory.com/media-library/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbWFnZSI6Imh0dHBzOi8vYXNzZXRzLnJibC5tcy81MDU1NTU0NS9vcmlnaW4ucG5nIiwiZXhwaXJlc19hdCI6MTc2MzMyOTkwM30.C-AXJe9KD0htsRtiDfInMKrHSznDd3XXhA3WVdEauBk/image.png?width=80&height=81"></a></p><!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
</div>
|
||||
<p><a href="http://fakehost/" target="_self"><img src="https://assets.rbl.ms/33391448/origin.png" alt="RawStory"></a></p>
|
||||
</div>
|
||||
<!-- User Code -->
|
||||
<div id="mySidenav">
|
||||
<p><a href="http://fakehost/"><img src="http://fakehost/test/ezgif.com-webp-to-jpg.jpg" alt="RawStory" type="lazy-image" data-runner-src="https://assets.rbl.ms/23278157/origin.jpg" image_type="jpg" image_width="405" image_id="23278157" image_height="65" image_filename="ezgif.com-webp-to-jpg.jpg" style_image_all_default_height="30px" style_image_all_default_width="138px" link_href="/" link_target="_self"></a>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://fakehost/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.teepublic.com/stores/raw-story-store?ref_id=9578&sort=popular">Shop to Support Independent Journalism</a>
|
||||
</li><!-- <li><a href="/presidential-campaign-issues/">We Have Issues</a></li> -->
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/trump-news/">Trump</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/us-news/">U.S. News</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/world/">World</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/enviro-science/">Science</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/all-video/">Video</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/st/raw_story_investigates">Investigations</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.rawstory.com/st/ethics-policy">Ethics Policy</a>
|
||||
</li>
|
||||
<!--<li><a onclick="(function(){window.admiral('show','transact.login')})()">Ad-Free Login</a></li>-->
|
||||
<li>
|
||||
<a id="piano_login_button_sidenav">RawStory+ Login</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- NOT A SURVEY -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- <div id="story-top-ad" class="three-seconds-sticky leaderboard-ad" style="min-height: 280px;" align="center"> -->
|
||||
<!-- <div class="proper-ad-unit desktop-only no-gray story_page_top" style="min-height: 280px;"> ==
|
||||
<div id="story-top-ad" style="min-height: 280px;" align="center"><div id="proper-ad-rawstory_story_page_top"></div></div> -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5">
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5_0">
|
||||
<div>
|
||||
<p><img src="https://www.rawstory.com/media-library/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbWFnZSI6Imh0dHBzOi8vYXNzZXRzLnJibC5tcy81MjgyMzczNi9vcmlnaW4uanBnIiwiZXhwaXJlc19hdCI6MTc3MjEwOTgyOX0.x0nIAqMuWTsAG5Pr5wehDbjaaj9FAOkdifmxr2foTgw/image.jpg?width=210" alt="Leigh Tauss">
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h3> Leigh Tauss </h3>
|
||||
<p> Leigh Tauss is a newswriter for Raw Story based in Raleigh, N.C. Her work has been picked up by the Associated Press and has appeared in The Washington Post and The Daily Beast, among other outlets. </p>
|
||||
<p> Before joining Raw Story, Tauss worked as an editor for the Bangor Daily News, a senior staff writer at INDY Week and a reporter for the Meriden Record-Journal. Her writing has received numerous awards from the the North Carolina Press Association, the Green Eyeshade Awards, the Connecticut Society of Professional Journalists and the Association of Alternative Newsmedia. </p>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5_1">
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5_1_0" data-block="None" data-format="posts-custom" data-source="current_post" data-source-type="current_post" data-source-unique="true" data-section-id="" data-is-reordable="false" data-using-stickers="false" data-has-more="false" data-attr-v="2" data-attr-posts_id="sPost_Default_Page_0_0_6_0_0_7_3_5_1_0" data-attr-layout_quality="5" data-attr-header_template="jinja/post/custom-css.html" data-attr-node_id="/root/blocks/block[post_default_page]/abtests/abtest[1]/choose[2]/otherwise/row/column[2]/posts[1]-" data-attr-source_url="current_post" data-attr-use_tag_image_for_lead_media="true" data-attr-all_element_order="photo_credit,body,post_shares,all_sections,author,date,section,community_name,headline,badges,badges_sponsored,subheadline,photo_caption,snark_line,page_views,follow_button,community_comments,like_button,source_link,collection_button,tags,primary_tag,main_author,date_modified,custom_field_smart_news_feed_headline,custom_field_Send-alternative-headline-to-smartnews,custom_field_is_survey_post,custom_field_survey_text_italic,custom_field_survey_text_red,product_prices,product_vendor,product_buy_link,words_count,time_to_read" data-attr-limit="1" data-attr-element_classes="post-head post-body article-body" data-attr-layout_all_image_crop="original" data-attr-layout_post_shares="bottom" data-attr-all_share_buttons="Facebook,Twitter,Flipboard,CopyLink,Email,FacebookMessenger,Linkedin,GooglePlus,Pinterest,Whatsapp,Reddit,Separator,Tumblr,SMS,Slack" data-attr-layout_linkedin="active" data-attr-layout_separator="inactive" data-attr-layout_copylink="active" data-attr-layout_flipboard="active" data-attr-layout_email="active" data-attr-layout_tumblr="inactive" data-attr-layout_reddit="inactive" data-attr-layout_whatsapp="inactive" data-attr-layout_pinterest="inactive" data-attr-layout_googleplus="inactive" data-attr-layout_sms="inactive" data-attr-layout_slack="inactive" data-attr-layout_all_subheadline_tag="h4" data-attr-layout_body="bottom" data-attr-show_more_button_text="Continue Reading" data-attr-show_full_post_body="true" data-attr-layout_photo_credit="bottom" data-attr-data-rm-advanced="true" data-attr-data-rm-device-crops="true" data-attr-layout_all_sections="bottom" data-attr-layout_all_show_video="true" data-attr-allow_crop_override="true" data-attr-filters="section,post_body_pager" data-attr-section_url="" data-attr-source="" data-attr-format="posts-custom" data-attr-is_current_post="true" data-category="MSN">
|
||||
<article elid="2668951502" data-frozen-sections="[]">
|
||||
<div>
|
||||
<picture>
|
||||
<source srcset="https://www.rawstory.com/media-library/chief-executive-officer-of-spacex-and-tesla-and-owner-of-twitter-elon-musk-attends-the-viva-technology-conference-dedicated-to.jpg?id=53161798&width=3600&height=2165 3x, https://www.rawstory.com/media-library/chief-executive-officer-of-spacex-and-tesla-and-owner-of-twitter-elon-musk-attends-the-viva-technology-conference-dedicated-to.jpg?id=53161798&width=2400&height=1443 2x, https://www.rawstory.com/media-library/chief-executive-officer-of-spacex-and-tesla-and-owner-of-twitter-elon-musk-attends-the-viva-technology-conference-dedicated-to.jpg?id=53161798&width=1200&height=721 1x"><img fetchpriority="high" role="img" alt="'Interview crashed and burst into flames': Trump and Musk mocked amid stream's launch woes" aria-label="'Interview crashed and burst into flames': Trump and Musk mocked amid stream's launch woes" src="https://www.rawstory.com/media-library/chief-executive-officer-of-spacex-and-tesla-and-owner-of-twitter-elon-musk-attends-the-viva-technology-conference-dedicated-to.jpg?id=53161798&width=1200&height=721" width="1024" height="616" pinger-seen="true">
|
||||
</picture>
|
||||
</div>
|
||||
</article>
|
||||
</div><!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<div id="corrections-text">
|
||||
<p> For customer support contact <a href="mailto:support@rawstory.com">support@rawstory.com</a>. Report typos and corrections to <a href="mailto:corrections@rawstory.com">corrections@rawstory.com</a>. </p>
|
||||
</div>
|
||||
<!-- <div class="donate-appeal-text">
|
||||
Like this article ? Text `Support` to 50123 to donate.
|
||||
</div> -->
|
||||
<h2> Stories Chosen For You </h2><!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- <div class="connatix-hodler"><div id="3c873ced6dcf4282952e7744badecace"></div></div> -->
|
||||
<!-- End User Code -->
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5_1_14_0_0">
|
||||
<!-- User Code -->
|
||||
<!--a href="https://www.rawstory.com/elon-musk-trump-2668951502/?comments=disqus" title="Load Comments" rel="nofollow">Read Comments - Join the Discussion</a-->
|
||||
<p><a href="#comments_section_start" rel="noopener">READ COMMENTS - JOIN THE DISCUSSION</a></p><!-- End User Code -->
|
||||
</div>
|
||||
<!-- User Code -->
|
||||
<!-- <div id="post-article-rawstoryplus-mobile"></div><style>
|
||||
@media only screen and (min-width: 599px) {
|
||||
#mobile_post_article {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style> -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- MGID - 3x2 Under-Article - Composite Start -->
|
||||
<!-- MGID - 3x2 Under-Article - Composite End -->
|
||||
<!-- End User Code -->
|
||||
<div id="spink_appeal_box_0_0_14_0_0_0">
|
||||
<h2> Worried about democracy in 2024? </h2>
|
||||
<p> So are we. </p>
|
||||
<p> We’re gearing up for the most consequential presidential election of our lifetime. Candidates are promising to endanger our checks and balances and undermine voting. Politicians and neo-Nazis threaten women’s health and LGBTQ rights. </p>
|
||||
<p> We expose extremism, regardless of party. But we can’t do it without your help. Raw Story is independent, with no corporate owner, and this allows us to bring you the unvarnished truth. </p>
|
||||
<p> We need your support in this difficult time. Every reader contribution, no matter the amount, makes a difference. Your vital backing allows our newsroom to bring you the stories that matter. </p>
|
||||
<p>
|
||||
<b><a href="https://donate.rawstory.com/441256-keep-democracy-alive-invest-in-courageous-progressive-journalism?utm_medium=other" target="_blank">Invest in democracy by making a contribution.</a> Or <a href="https://www.rawstory.com/st/Rawstory_Plus_Signup" target="_blank">click here to become a subscriber.</a></b>
|
||||
</p>
|
||||
<p>
|
||||
<b>Thank you.</b>
|
||||
</p>
|
||||
</div><!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<div id="disqus_thread">
|
||||
<p><img src="https://c.disquscdn.com/next/embed/assets/img/loader-bg.a5b321d890ffdd553322adc8decaf4ed.png">
|
||||
</p>
|
||||
</div><!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- MGID - RawStory - 3x2 below content -->
|
||||
<!-- End User Code -->
|
||||
</div>
|
||||
<div id="sPost_Default_Page_0_0_6_0_0_7_3_5_2">
|
||||
<p><a id="sPost_Default_Page_0_0_6_0_0_7_3_5_2_0_1_0_0"><img alt="" width="1800" height="260" src="https://www.rawstory.com/media-library/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbWFnZSI6Imh0dHBzOi8vYXNzZXRzLnJibC5tcy8yNzM2MDcxMC9vcmlnaW4ucG5nIiwiZXhwaXJlc19hdCI6MTc3MTkzMTU1MX0.w4TIC_SifCDgMyj6TH6QHDrdsMoCI1Qa1hhlpv4JBZY/image.png?width=1800&height=260"></a></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- RM VERSION
|
||||
<div id='div-gpt-ad-1599648564111-0'><script>
|
||||
googletag.cmd.push(function() { googletag.display('div-gpt-ad-1599648564111-0'); });
|
||||
</script></div>
|
||||
-->
|
||||
<!-- OLD SCRIPT FROM WP -->
|
||||
<!-- Undertone AdX Body -->
|
||||
<!-- /1010624/Rawstory_Undertone_Desktop -->
|
||||
<!-- <div id="div-gpt-ad-1578154939325-0"></div><!== <div id="div-gpt-ad-1599648564111-0"></div><!== /1010624/justpremium2021 -->
|
||||
<!-- <div id="div-gpt-ad-1612555365328-0" style="width: 1px; height: 1px;"></div> -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<!-- FOOTER ENDS HERE -->
|
||||
<!-- End User Code -->
|
||||
<!-- User Code -->
|
||||
<p>
|
||||
{{ post.roar_specific_data.api_data.analytics }}
|
||||
</p><!-- End User Code -->
|
||||
</div>
|
||||
</DIV>
|
||||
4651
packages/readabilityjs/test/test-pages/raw-story/source.html
Normal file
4651
packages/readabilityjs/test/test-pages/raw-story/source.html
Normal file
File diff suppressed because one or more lines are too long
1
packages/readabilityjs/test/test-pages/raw-story/url.txt
Normal file
1
packages/readabilityjs/test/test-pages/raw-story/url.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
https://www.rawstory.com/elon-musk-trump-2668951502/
|
||||
|
|
@ -6,6 +6,7 @@ import {
|
|||
Trigger,
|
||||
Arrow,
|
||||
Label,
|
||||
Portal,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
import { PopperContentProps } from '@radix-ui/react-popover'
|
||||
import { CSS } from '@stitches/react'
|
||||
|
|
@ -181,24 +182,26 @@ export function Dropdown(
|
|||
>
|
||||
{triggerElement}
|
||||
</DropdownTrigger>
|
||||
<DropdownContent
|
||||
css={css}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align ? align : 'center'}
|
||||
alignOffset={alignOffset}
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{labelText && <StyledLabel>{labelText}</StyledLabel>}
|
||||
{children}
|
||||
<StyledArrow />
|
||||
</DropdownContent>
|
||||
<Portal>
|
||||
<DropdownContent
|
||||
css={css}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align ? align : 'center'}
|
||||
alignOffset={alignOffset}
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{labelText && <StyledLabel>{labelText}</StyledLabel>}
|
||||
{children}
|
||||
<StyledArrow />
|
||||
</DropdownContent>
|
||||
</Portal>
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
|
|||
import { Box, SpanBox } from './LayoutPrimitives'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Label } from '../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { isTouchScreenDevice } from '../../lib/deviceType'
|
||||
import { EditLabelChip } from './EditLabelChip'
|
||||
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
|
||||
import { EditLabelChipStack } from './EditLabelChipStack'
|
||||
import { useGetLabels } from '../../lib/networking/labels/useLabels'
|
||||
|
||||
// AutosizeInput is a Class component, but the types are broken in React 18.
|
||||
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
|
||||
// https://github.com/JedWatson/react-input-autosize/issues
|
||||
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
const AutosizeInput =
|
||||
AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
|
||||
|
||||
const MaxUnstackedLabels = 7
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ type LabelsPickerProps = {
|
|||
|
||||
export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
||||
const inputRef = useRef<HTMLInputElement | null>()
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [isStackExpanded, setIsStackExpanded] = useState(false)
|
||||
const {
|
||||
focused,
|
||||
|
|
@ -80,9 +81,10 @@ export const LabelsPicker = (props: LabelsPickerProps): JSX.Element => {
|
|||
setTabCount(_tabCount)
|
||||
}
|
||||
|
||||
const matches = availableLabels.labels.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
)
|
||||
const matches =
|
||||
availableLabels?.filter((l) =>
|
||||
l.name.toLowerCase().startsWith(_tabStartValue)
|
||||
) ?? []
|
||||
|
||||
if (_tabCount < matches.length) {
|
||||
setInputValue(matches[_tabCount].name)
|
||||
|
|
|
|||
|
|
@ -39,11 +39,10 @@ const textVariants = {
|
|||
},
|
||||
settingsSection: {
|
||||
fontWeight: '600',
|
||||
fontSize: '17px',
|
||||
fontSize: '22px',
|
||||
fontFamily: '$inter',
|
||||
color: '$grayText',
|
||||
m: '0px',
|
||||
my: '15px',
|
||||
marginBlockStart: '0px',
|
||||
marginBlockEnd: '0px',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import React from 'react'
|
|||
|
||||
export function ConfusedSlothIcon(): JSX.Element {
|
||||
const { currentThemeIsDark } = useCurrentTheme()
|
||||
console.log('is dark mdoe: ', currentThemeIsDark)
|
||||
return currentThemeIsDark ? (
|
||||
<ConfusedSlothIconDark />
|
||||
) : (
|
||||
|
|
|
|||
110
packages/web/components/elements/icons/EmptyHighlightsIcon.tsx
Normal file
110
packages/web/components/elements/icons/EmptyHighlightsIcon.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme'
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export function EmptyHighlightsIcon(): JSX.Element {
|
||||
const { currentTheme } = useCurrentTheme()
|
||||
switch (currentTheme) {
|
||||
case 'Sepia':
|
||||
return <EmptyHighlightsIconSepia />
|
||||
case 'Apollo':
|
||||
case 'Dark':
|
||||
return <EmptyHighlightsIconDark />
|
||||
case 'Light':
|
||||
return <EmptyHighlightsIconLight />
|
||||
}
|
||||
return <EmptyHighlightsIconLight />
|
||||
}
|
||||
|
||||
class EmptyHighlightsIconDark extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="130"
|
||||
height="89"
|
||||
viewBox="0 0 130 89"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M42.1055 11.8008C39.1909 10.118 35.464 11.1167 33.7813 14.0312L29.7188 21.0677C28.036 23.9823 29.0346 27.7092 31.9492 29.3919L63.6133 47.6732C66.5278 49.3559 70.2547 48.3573 71.9375 45.4427L76 38.4062C77.6827 35.4917 76.684 31.7647 73.7695 30.082L42.1055 11.8008Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M28.3159 34.3294L29.3315 32.5703L62.1683 51.5286L61.1527 53.2878C59.47 56.2023 55.743 57.2009 52.8285 55.5182L30.5464 42.6536C27.6318 40.9709 26.6332 37.2439 28.3159 34.3294Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M29.1084 46.5156L50.2139 58.7009L48.4706 61.7227C47.3669 63.6342 45.324 64.7887 43.141 64.7695L42.6713 64.7473L22.9034 63.0484C21.5143 62.929 20.6829 61.4943 21.1971 60.2541L21.3182 60.009L29.1084 46.5156Z"
|
||||
fill="#898989"
|
||||
/>
|
||||
</g>
|
||||
<rect x="65.0005" y="63" width="65" height="2" rx="1" fill="#898989" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyHighlightsIconLight extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="130"
|
||||
height="89"
|
||||
viewBox="0 0 130 89"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M42.1055 11.8008C39.1909 10.118 35.464 11.1167 33.7813 14.0312L29.7188 21.0677C28.036 23.9823 29.0346 27.7092 31.9492 29.3919L63.6133 47.6732C66.5278 49.3559 70.2547 48.3573 71.9375 45.4427L76 38.4062C77.6827 35.4917 76.684 31.7647 73.7695 30.082L42.1055 11.8008Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
<path
|
||||
d="M28.3159 34.3294L29.3315 32.5703L62.1683 51.5286L61.1527 53.2878C59.47 56.2023 55.743 57.2009 52.8285 55.5182L30.5464 42.6536C27.6318 40.9709 26.6332 37.2439 28.3159 34.3294Z"
|
||||
fill="#898989"
|
||||
/>
|
||||
<path
|
||||
d="M29.1084 46.5156L50.2139 58.7009L48.4706 61.7227C47.3669 63.6342 45.324 64.7887 43.141 64.7695L42.6713 64.7473L22.9034 63.0484C21.5143 62.929 20.6829 61.4943 21.1971 60.2541L21.3182 60.009L29.1084 46.5156Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
</g>
|
||||
<rect x="65.0005" y="63" width="65" height="2" rx="1" fill="#D9D9D9" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyHighlightsIconSepia extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="130"
|
||||
height="89"
|
||||
viewBox="0 0 130 89"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M42.1055 11.8008C39.1909 10.118 35.464 11.1167 33.7813 14.0312L29.7188 21.0677C28.036 23.9823 29.0346 27.7092 31.9492 29.3919L63.6133 47.6732C66.5278 49.3559 70.2547 48.3573 71.9375 45.4427L76 38.4062C77.6827 35.4917 76.684 31.7647 73.7695 30.082L42.1055 11.8008Z"
|
||||
fill="#E6DFC9"
|
||||
/>
|
||||
<path
|
||||
d="M28.3159 34.3294L29.3315 32.5703L62.1683 51.5286L61.1527 53.2878C59.47 56.2023 55.743 57.2009 52.8285 55.5182L30.5464 42.6536C27.6318 40.9709 26.6332 37.2439 28.3159 34.3294Z"
|
||||
fill="#D2CBB5"
|
||||
/>
|
||||
<path
|
||||
d="M29.1084 46.5156L50.2139 58.7009L48.4706 61.7227C47.3669 63.6342 45.324 64.7887 43.141 64.7695L42.6713 64.7473L22.9034 63.0484C21.5143 62.929 20.6829 61.4943 21.1971 60.2541L21.3182 60.009L29.1084 46.5156Z"
|
||||
fill="#ACA590"
|
||||
/>
|
||||
</g>
|
||||
<rect x="65.0005" y="63" width="65" height="2" rx="1" fill="#ACA590" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
175
packages/web/components/elements/icons/EmptyLibraryNotes.tsx
Normal file
175
packages/web/components/elements/icons/EmptyLibraryNotes.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme'
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export function EmptyLibraryIcon(): JSX.Element {
|
||||
const { currentTheme } = useCurrentTheme()
|
||||
switch (currentTheme) {
|
||||
case 'Sepia':
|
||||
return <EmptyLibraryIconSepia />
|
||||
case 'Apollo':
|
||||
case 'Dark':
|
||||
return <EmptyLibraryIconDark />
|
||||
case 'Light':
|
||||
return <EmptyLibraryIconLight />
|
||||
}
|
||||
return <EmptyLibraryIconLight />
|
||||
}
|
||||
|
||||
class EmptyLibraryIconDark extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="79"
|
||||
viewBox="0 0 68 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M51.9818 10.7298L59.1106 62.7233L59.751 67.9395C59.9574 69.62 58.7624 71.1495 57.0819 71.3558L12.2024 76.8663C10.522 77.0727 8.99247 75.8777 8.78614 74.1972L1.87662 17.9237C1.77345 17.0835 2.37095 16.3187 3.21116 16.2155C3.21647 16.2149 3.22178 16.2143 3.22709 16.2137L6.95046 15.796M9.96143 15.4576L13.4769 15.0632L9.96143 15.4576Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M53.2202 10.56C53.1264 9.87609 52.4959 9.39765 51.812 9.49143C51.128 9.5852 50.6496 10.2157 50.7433 10.8996L53.2202 10.56ZM59.1106 62.7233L60.3512 62.571C60.3505 62.5652 60.3498 62.5593 60.349 62.5535L59.1106 62.7233ZM59.751 67.9395L60.9917 67.7872L59.751 67.9395ZM57.0819 71.3558L57.2343 72.5965L57.0819 71.3558ZM12.2024 76.8663L12.3548 78.107L12.2024 76.8663ZM8.78614 74.1972L10.0268 74.0449L8.78614 74.1972ZM1.87662 17.9237L0.635934 18.076L1.87662 17.9237ZM3.22709 16.2137L3.36644 17.4559L3.22709 16.2137ZM7.0898 17.0382C7.77586 16.9613 8.26963 16.3427 8.19267 15.6567C8.11571 14.9706 7.49717 14.4768 6.81112 14.5538L7.0898 17.0382ZM9.82209 14.2154C9.13604 14.2923 8.64227 14.9109 8.71922 15.5969C8.79618 16.283 9.41472 16.7768 10.1008 16.6998L9.82209 14.2154ZM13.6162 16.3055C14.3023 16.2285 14.796 15.61 14.7191 14.9239C14.6421 14.2378 14.0236 13.7441 13.3375 13.821L13.6162 16.3055ZM50.7433 10.8996L57.8721 62.8931L60.349 62.5535L53.2202 10.56L50.7433 10.8996ZM57.8699 62.8757L58.5103 68.0919L60.9917 67.7872L60.3512 62.571L57.8699 62.8757ZM58.5103 68.0919C58.6325 69.0871 57.9248 69.9929 56.9296 70.1151L57.2343 72.5965C59.5999 72.306 61.2822 70.1528 60.9917 67.7872L58.5103 68.0919ZM56.9296 70.1151L12.0501 75.6257L12.3548 78.107L57.2343 72.5965L56.9296 70.1151ZM12.0501 75.6257C11.0549 75.7478 10.149 75.0401 10.0268 74.0449L7.54546 74.3496C7.83592 76.7152 9.98912 78.3975 12.3548 78.107L12.0501 75.6257ZM10.0268 74.0449L3.1173 17.7713L0.635934 18.076L7.54546 74.3496L10.0268 74.0449ZM3.1173 17.7713C3.09827 17.6163 3.20849 17.4753 3.3635 17.4562L3.05882 14.9749C1.5334 15.1622 0.448635 16.5506 0.635934 18.076L3.1173 17.7713ZM3.3635 17.4562C3.36448 17.4561 3.36545 17.456 3.36644 17.4559L3.08775 14.9715C3.0781 14.9725 3.06846 14.9737 3.05882 14.9749L3.3635 17.4562ZM3.36644 17.4559L7.0898 17.0382L6.81112 14.5538L3.08775 14.9715L3.36644 17.4559ZM10.1008 16.6998L13.6162 16.3055L13.3375 13.821L9.82209 14.2154L10.1008 16.6998Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M50.0416 14.0005L56.4942 61.1206L57.0746 65.8478C57.2616 67.3707 56.1932 68.755 54.6884 68.9398L14.4978 73.8745C12.9929 74.0593 11.6214 72.9746 11.4344 71.4517L5.24533 21.0456C5.11071 19.9493 5.89034 18.9514 6.98668 18.8168L11.4949 18.2633"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
d="M17.212 4C17.212 2.48122 18.4433 1.25 19.962 1.25H53.5546C54.2836 1.25 54.9828 1.5395 55.4985 2.05485L65.2536 11.8039C65.7697 12.3197 66.0597 13.0194 66.0597 13.7491V60.3761C66.0597 61.8949 64.8284 63.1261 63.3097 63.1261H19.962C18.4433 63.1261 17.212 61.8949 17.212 60.3761V4Z"
|
||||
fill="#898989"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M54.2806 1.83984V10.7277C54.2806 11.9975 55.31 13.0269 56.5798 13.0269H62.66"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24.6434 13.0273H44.5694M24.6434 22.2239H57.5979M24.6434 32.1869H57.5979M24.6434 42.1499H57.5979M24.6434 52.1128H44.5694"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyLibraryIconLight extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="79"
|
||||
viewBox="0 0 68 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M51.9819 11.2298L59.1107 63.2233L59.7512 68.4395C59.9575 70.12 58.7625 71.6495 57.0821 71.8558L12.2025 77.3663C10.5221 77.5727 8.99259 76.3777 8.78626 74.6972L1.87674 18.4237C1.77357 17.5835 2.37107 16.8187 3.21128 16.7155C3.21659 16.7149 3.2219 16.7143 3.22721 16.7137L6.95058 16.296M9.96156 15.9576L13.477 15.5632L9.96156 15.9576Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
<path
|
||||
d="M53.2203 11.06C53.1265 10.3761 52.496 9.89765 51.8121 9.99143C51.1281 10.0852 50.6497 10.7157 50.7435 11.3996L53.2203 11.06ZM59.1107 63.2233L60.3514 63.071C60.3506 63.0652 60.3499 63.0593 60.3491 63.0535L59.1107 63.2233ZM59.7512 68.4395L60.9918 68.2872L59.7512 68.4395ZM57.0821 71.8558L57.2344 73.0965L57.0821 71.8558ZM12.2025 77.3663L12.3549 78.607L12.2025 77.3663ZM8.78626 74.6972L10.0269 74.5449L8.78626 74.6972ZM1.87674 18.4237L0.636056 18.576L1.87674 18.4237ZM3.22721 16.7137L3.36656 17.9559L3.22721 16.7137ZM7.08992 17.5382C7.77598 17.4613 8.26975 16.8427 8.19279 16.1567C8.11583 15.4706 7.49729 14.9768 6.81124 15.0538L7.08992 17.5382ZM9.82221 14.7154C9.13616 14.7923 8.64239 15.4109 8.71935 16.0969C8.7963 16.783 9.41485 17.2768 10.1009 17.1998L9.82221 14.7154ZM13.6163 16.8055C14.3024 16.7285 14.7962 16.11 14.7192 15.4239C14.6422 14.7378 14.0237 14.2441 13.3376 14.321L13.6163 16.8055ZM50.7435 11.3996L57.8723 63.3931L60.3491 63.0535L53.2203 11.06L50.7435 11.3996ZM57.87 63.3757L58.5105 68.5919L60.9918 68.2872L60.3514 63.071L57.87 63.3757ZM58.5105 68.5919C58.6327 69.5871 57.9249 70.4929 56.9297 70.6151L57.2344 73.0965C59.6 72.806 61.2823 70.6528 60.9918 68.2872L58.5105 68.5919ZM56.9297 70.6151L12.0502 76.1257L12.3549 78.607L57.2344 73.0965L56.9297 70.6151ZM12.0502 76.1257C11.055 76.2478 10.1491 75.5401 10.0269 74.5449L7.54558 74.8496C7.83605 77.2152 9.98924 78.8975 12.3549 78.607L12.0502 76.1257ZM10.0269 74.5449L3.11742 18.2713L0.636056 18.576L7.54558 74.8496L10.0269 74.5449ZM3.11742 18.2713C3.09839 18.1163 3.20862 17.9753 3.36362 17.9562L3.05894 15.4749C1.53352 15.6622 0.448757 17.0506 0.636056 18.576L3.11742 18.2713ZM3.36362 17.9562C3.3646 17.9561 3.36558 17.956 3.36656 17.9559L3.08787 15.4715C3.07822 15.4725 3.06858 15.4737 3.05894 15.4749L3.36362 17.9562ZM3.36656 17.9559L7.08992 17.5382L6.81124 15.0538L3.08787 15.4715L3.36656 17.9559ZM10.1009 17.1998L13.6163 16.8055L13.3376 14.321L9.82221 14.7154L10.1009 17.1998Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M50.0417 14.5005L56.4943 61.6206L57.0747 66.3478C57.2617 67.8707 56.1934 69.255 54.6885 69.4398L14.4979 74.3745C12.993 74.5593 11.6215 73.4746 11.4345 71.9517L5.24545 21.5456C5.11083 20.4493 5.89047 19.4514 6.9868 19.3168L11.495 18.7633"
|
||||
fill="#2A2A2A"
|
||||
/>
|
||||
<path
|
||||
d="M17.2122 4.5C17.2122 2.98122 18.4434 1.75 19.9622 1.75H53.5547C54.2837 1.75 54.983 2.0395 55.4986 2.55485L65.2537 12.3039C65.7698 12.8197 66.0598 13.5194 66.0598 14.2491V60.8761C66.0598 62.3949 64.8286 63.6261 63.3098 63.6261H19.9622C18.4434 63.6261 17.2122 62.3949 17.2122 60.8761V4.5Z"
|
||||
fill="#EDEDED"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M54.2808 2.33984V11.2277C54.2808 12.4975 55.3101 13.5269 56.5799 13.5269H62.6601"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24.6436 13.5273H44.5695M24.6436 22.7239H57.598M24.6436 32.6869H57.598M24.6436 42.6499H57.598M24.6436 52.6128H44.5695"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyLibraryIconSepia extends React.Component<IconProps> {
|
||||
render() {
|
||||
console.log('rendering sepia icon')
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="79"
|
||||
viewBox="0 0 68 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M51.9818 10.7298L59.1106 62.7233L59.751 67.9395C59.9574 69.62 58.7624 71.1495 57.0819 71.3558L12.2024 76.8663C10.522 77.0727 8.99247 75.8777 8.78614 74.1972L1.87662 17.9237C1.77345 17.0835 2.37095 16.3187 3.21116 16.2155C3.21647 16.2149 3.22178 16.2143 3.22709 16.2137L6.95046 15.796M9.96143 15.4576L13.4769 15.0632L9.96143 15.4576Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
<path
|
||||
d="M53.2202 10.56C53.1264 9.87609 52.4959 9.39765 51.812 9.49143C51.128 9.5852 50.6496 10.2157 50.7433 10.8996L53.2202 10.56ZM59.1106 62.7233L60.3512 62.571C60.3505 62.5652 60.3498 62.5593 60.349 62.5535L59.1106 62.7233ZM59.751 67.9395L60.9917 67.7872L59.751 67.9395ZM57.0819 71.3558L57.2343 72.5965L57.0819 71.3558ZM12.2024 76.8663L12.3548 78.107L12.2024 76.8663ZM8.78614 74.1972L10.0268 74.0449L8.78614 74.1972ZM1.87662 17.9237L3.1173 17.7713L1.87662 17.9237ZM3.22709 16.2137L3.36644 17.4559L3.22709 16.2137ZM7.0898 17.0382C7.77586 16.9613 8.26963 16.3427 8.19267 15.6567C8.11571 14.9706 7.49717 14.4768 6.81112 14.5538L7.0898 17.0382ZM9.82209 14.2154C9.13604 14.2923 8.64227 14.9109 8.71922 15.5969C8.79618 16.283 9.41472 16.7768 10.1008 16.6998L9.82209 14.2154ZM13.6162 16.3055C14.3023 16.2285 14.796 15.61 14.7191 14.9239C14.6421 14.2378 14.0236 13.7441 13.3375 13.821L13.6162 16.3055ZM50.7433 10.8996L57.8721 62.8931L60.349 62.5535L53.2202 10.56L50.7433 10.8996ZM57.8699 62.8757L58.5103 68.0919L60.9917 67.7872L60.3512 62.571L57.8699 62.8757ZM58.5103 68.0919C58.6325 69.0871 57.9248 69.9929 56.9296 70.1151L57.2343 72.5965C59.5999 72.306 61.2822 70.1528 60.9917 67.7872L58.5103 68.0919ZM56.9296 70.1151L12.0501 75.6257L12.3548 78.107L57.2343 72.5965L56.9296 70.1151ZM12.0501 75.6257C11.0549 75.7478 10.149 75.0401 10.0268 74.0449L7.54546 74.3496C7.83592 76.7152 9.98912 78.3975 12.3548 78.107L12.0501 75.6257ZM10.0268 74.0449L3.1173 17.7713L0.635934 18.076L7.54546 74.3496L10.0268 74.0449ZM3.1173 17.7713C3.09827 17.6163 3.20849 17.4753 3.3635 17.4562L3.05882 14.9749C1.5334 15.1622 0.448635 16.5506 0.635934 18.076L3.1173 17.7713ZM3.3635 17.4562C3.36448 17.4561 3.36545 17.456 3.36644 17.4559L3.08775 14.9715C3.0781 14.9725 3.06846 14.9737 3.05882 14.9749L3.3635 17.4562ZM3.36644 17.4559L7.0898 17.0382L6.81112 14.5538L3.08775 14.9715L3.36644 17.4559ZM10.1008 16.6998L13.6162 16.3055L13.3375 13.821L9.82209 14.2154L10.1008 16.6998Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M50.0416 14.0005L56.4942 61.1206L57.0746 65.8478C57.2616 67.3707 56.1932 68.755 54.6884 68.9398L14.4978 73.8745C12.9929 74.0593 11.6214 72.9746 11.4344 71.4517L5.24533 21.0456C5.11071 19.9493 5.89034 18.9514 6.98668 18.8168L11.4949 18.2633"
|
||||
fill="#2A2A2A"
|
||||
/>
|
||||
<path
|
||||
d="M17.212 4C17.212 2.48122 18.4433 1.25 19.962 1.25H53.5546C54.2836 1.25 54.9828 1.5395 55.4985 2.05485L65.2536 11.8039C65.7697 12.3197 66.0597 13.0194 66.0597 13.7491V60.3761C66.0597 61.8949 64.8284 63.1261 63.3097 63.1261H19.962C18.4433 63.1261 17.212 61.8949 17.212 60.3761V4Z"
|
||||
fill="#EEE8D5"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M54.2806 1.83984V10.7277C54.2806 11.9975 55.31 13.0269 56.5798 13.0269H62.66"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24.6434 13.0273H44.5694M24.6434 22.2239H57.5979M24.6434 32.1869H57.5979M24.6434 42.1499H57.5979M24.6434 52.1128H44.5694"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
159
packages/web/components/elements/icons/EmptyTrashIcon.tsx
Normal file
159
packages/web/components/elements/icons/EmptyTrashIcon.tsx
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { useCurrentTheme } from '../../../lib/hooks/useCurrentTheme'
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export function EmptyTrashIcon(): JSX.Element {
|
||||
const { currentTheme } = useCurrentTheme()
|
||||
switch (currentTheme) {
|
||||
case 'Sepia':
|
||||
return <EmptyTrashIconSepia />
|
||||
case 'Apollo':
|
||||
case 'Dark':
|
||||
return <EmptyTrashIconDark />
|
||||
case 'Light':
|
||||
return <EmptyTrashIconLight />
|
||||
}
|
||||
return <EmptyTrashIconLight />
|
||||
}
|
||||
|
||||
class EmptyTrashIconDark extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="79"
|
||||
viewBox="0 0 68 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M51.9818 10.7298L59.1106 62.7233L59.751 67.9395C59.9574 69.62 58.7624 71.1495 57.0819 71.3558L12.2024 76.8663C10.522 77.0727 8.99247 75.8777 8.78614 74.1972L1.87662 17.9237C1.77345 17.0835 2.37095 16.3187 3.21116 16.2155C3.21647 16.2149 3.22178 16.2143 3.22709 16.2137L6.95046 15.796M9.96143 15.4576L13.4769 15.0632L9.96143 15.4576Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M53.2202 10.56C53.1264 9.87609 52.4959 9.39765 51.812 9.49143C51.128 9.5852 50.6496 10.2157 50.7433 10.8996L53.2202 10.56ZM59.1106 62.7233L60.3512 62.571C60.3505 62.5652 60.3498 62.5593 60.349 62.5535L59.1106 62.7233ZM59.751 67.9395L60.9917 67.7872L59.751 67.9395ZM57.0819 71.3558L57.2343 72.5965L57.0819 71.3558ZM12.2024 76.8663L12.3548 78.107L12.2024 76.8663ZM8.78614 74.1972L10.0268 74.0449L8.78614 74.1972ZM1.87662 17.9237L0.635934 18.076L1.87662 17.9237ZM3.22709 16.2137L3.36644 17.4559L3.22709 16.2137ZM7.0898 17.0382C7.77586 16.9613 8.26963 16.3427 8.19267 15.6567C8.11571 14.9706 7.49717 14.4768 6.81112 14.5538L7.0898 17.0382ZM9.82209 14.2154C9.13604 14.2923 8.64227 14.9109 8.71922 15.5969C8.79618 16.283 9.41472 16.7768 10.1008 16.6998L9.82209 14.2154ZM13.6162 16.3055C14.3023 16.2285 14.796 15.61 14.7191 14.9239C14.6421 14.2378 14.0236 13.7441 13.3375 13.821L13.6162 16.3055ZM50.7433 10.8996L57.8721 62.8931L60.349 62.5535L53.2202 10.56L50.7433 10.8996ZM57.8699 62.8757L58.5103 68.0919L60.9917 67.7872L60.3512 62.571L57.8699 62.8757ZM58.5103 68.0919C58.6325 69.0871 57.9248 69.9929 56.9296 70.1151L57.2343 72.5965C59.5999 72.306 61.2822 70.1528 60.9917 67.7872L58.5103 68.0919ZM56.9296 70.1151L12.0501 75.6257L12.3548 78.107L57.2343 72.5965L56.9296 70.1151ZM12.0501 75.6257C11.0549 75.7478 10.149 75.0401 10.0268 74.0449L7.54546 74.3496C7.83592 76.7152 9.98912 78.3975 12.3548 78.107L12.0501 75.6257ZM10.0268 74.0449L3.1173 17.7713L0.635934 18.076L7.54546 74.3496L10.0268 74.0449ZM3.1173 17.7713C3.09827 17.6163 3.20849 17.4753 3.3635 17.4562L3.05882 14.9749C1.5334 15.1622 0.448635 16.5506 0.635934 18.076L3.1173 17.7713ZM3.3635 17.4562C3.36448 17.4561 3.36545 17.456 3.36644 17.4559L3.08775 14.9715C3.0781 14.9725 3.06846 14.9737 3.05882 14.9749L3.3635 17.4562ZM3.36644 17.4559L7.0898 17.0382L6.81112 14.5538L3.08775 14.9715L3.36644 17.4559ZM10.1008 16.6998L13.6162 16.3055L13.3375 13.821L9.82209 14.2154L10.1008 16.6998Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M50.0416 14.0005L56.4942 61.1206L57.0746 65.8478C57.2616 67.3707 56.1932 68.755 54.6884 68.9398L14.4978 73.8745C12.9929 74.0593 11.6214 72.9746 11.4344 71.4517L5.24533 21.0456C5.11071 19.9493 5.89034 18.9514 6.98668 18.8168L11.4949 18.2633"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
d="M17.212 4C17.212 2.48122 18.4433 1.25 19.962 1.25H53.5546C54.2836 1.25 54.9828 1.5395 55.4985 2.05485L65.2536 11.8039C65.7697 12.3197 66.0597 13.0194 66.0597 13.7491V60.3761C66.0597 61.8949 64.8284 63.1261 63.3097 63.1261H19.962C18.4433 63.1261 17.212 61.8949 17.212 60.3761V4Z"
|
||||
fill="#898989"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M54.2806 1.83984V10.7277C54.2806 11.9975 55.31 13.0269 56.5798 13.0269H62.66"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24.6434 13.0273H44.5694M24.6434 22.2239H57.5979M24.6434 32.1869H57.5979M24.6434 42.1499H57.5979M24.6434 52.1128H44.5694"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyTrashIconLight extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="68"
|
||||
height="79"
|
||||
viewBox="0 0 68 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M51.9819 11.2298L59.1107 63.2233L59.7512 68.4395C59.9575 70.12 58.7625 71.6495 57.0821 71.8558L12.2025 77.3663C10.5221 77.5727 8.99259 76.3777 8.78626 74.6972L1.87674 18.4237C1.77357 17.5835 2.37107 16.8187 3.21128 16.7155C3.21659 16.7149 3.2219 16.7143 3.22721 16.7137L6.95058 16.296M9.96156 15.9576L13.477 15.5632L9.96156 15.9576Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
<path
|
||||
d="M53.2203 11.06C53.1265 10.3761 52.496 9.89765 51.8121 9.99143C51.1281 10.0852 50.6497 10.7157 50.7435 11.3996L53.2203 11.06ZM59.1107 63.2233L60.3514 63.071C60.3506 63.0652 60.3499 63.0593 60.3491 63.0535L59.1107 63.2233ZM59.7512 68.4395L60.9918 68.2872L59.7512 68.4395ZM57.0821 71.8558L57.2344 73.0965L57.0821 71.8558ZM12.2025 77.3663L12.3549 78.607L12.2025 77.3663ZM8.78626 74.6972L10.0269 74.5449L8.78626 74.6972ZM1.87674 18.4237L0.636056 18.576L1.87674 18.4237ZM3.22721 16.7137L3.36656 17.9559L3.22721 16.7137ZM7.08992 17.5382C7.77598 17.4613 8.26975 16.8427 8.19279 16.1567C8.11583 15.4706 7.49729 14.9768 6.81124 15.0538L7.08992 17.5382ZM9.82221 14.7154C9.13616 14.7923 8.64239 15.4109 8.71935 16.0969C8.7963 16.783 9.41485 17.2768 10.1009 17.1998L9.82221 14.7154ZM13.6163 16.8055C14.3024 16.7285 14.7962 16.11 14.7192 15.4239C14.6422 14.7378 14.0237 14.2441 13.3376 14.321L13.6163 16.8055ZM50.7435 11.3996L57.8723 63.3931L60.3491 63.0535L53.2203 11.06L50.7435 11.3996ZM57.87 63.3757L58.5105 68.5919L60.9918 68.2872L60.3514 63.071L57.87 63.3757ZM58.5105 68.5919C58.6327 69.5871 57.9249 70.4929 56.9297 70.6151L57.2344 73.0965C59.6 72.806 61.2823 70.6528 60.9918 68.2872L58.5105 68.5919ZM56.9297 70.6151L12.0502 76.1257L12.3549 78.607L57.2344 73.0965L56.9297 70.6151ZM12.0502 76.1257C11.055 76.2478 10.1491 75.5401 10.0269 74.5449L7.54558 74.8496C7.83605 77.2152 9.98924 78.8975 12.3549 78.607L12.0502 76.1257ZM10.0269 74.5449L3.11742 18.2713L0.636056 18.576L7.54558 74.8496L10.0269 74.5449ZM3.11742 18.2713C3.09839 18.1163 3.20862 17.9753 3.36362 17.9562L3.05894 15.4749C1.53352 15.6622 0.448757 17.0506 0.636056 18.576L3.11742 18.2713ZM3.36362 17.9562C3.3646 17.9561 3.36558 17.956 3.36656 17.9559L3.08787 15.4715C3.07822 15.4725 3.06858 15.4737 3.05894 15.4749L3.36362 17.9562ZM3.36656 17.9559L7.08992 17.5382L6.81124 15.0538L3.08787 15.4715L3.36656 17.9559ZM10.1009 17.1998L13.6163 16.8055L13.3376 14.321L9.82221 14.7154L10.1009 17.1998Z"
|
||||
fill="#3D3D3D"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M50.0417 14.5005L56.4943 61.6206L57.0747 66.3478C57.2617 67.8707 56.1934 69.255 54.6885 69.4398L14.4979 74.3745C12.993 74.5593 11.6215 73.4746 11.4345 71.9517L5.24545 21.5456C5.11083 20.4493 5.89047 19.4514 6.9868 19.3168L11.495 18.7633"
|
||||
fill="#2A2A2A"
|
||||
/>
|
||||
<path
|
||||
d="M17.2122 4.5C17.2122 2.98122 18.4434 1.75 19.9622 1.75H53.5547C54.2837 1.75 54.983 2.0395 55.4986 2.55485L65.2537 12.3039C65.7698 12.8197 66.0598 13.5194 66.0598 14.2491V60.8761C66.0598 62.3949 64.8286 63.6261 63.3098 63.6261H19.9622C18.4434 63.6261 17.2122 62.3949 17.2122 60.8761V4.5Z"
|
||||
fill="#EDEDED"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M54.2808 2.33984V11.2277C54.2808 12.4975 55.3101 13.5269 56.5799 13.5269H62.6601"
|
||||
stroke="#3D3D3D"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M24.6436 13.5273H44.5695M24.6436 22.7239H57.598M24.6436 32.6869H57.598M24.6436 42.6499H57.598M24.6436 52.6128H44.5695"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmptyTrashIconSepia extends React.Component<IconProps> {
|
||||
render() {
|
||||
return (
|
||||
<svg
|
||||
width="58"
|
||||
height="62"
|
||||
viewBox="0 0 58 62"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 41.1093V59.1209C2 59.7782 2.53278 60.3109 3.19 60.3109H54.87C55.5272 60.3109 56.06 59.7782 56.06 59.1209V41.1093L49.6037 22.0197C49.4402 21.5363 48.9867 21.2109 48.4764 21.2109H9.58357C9.07327 21.2109 8.61978 21.5363 8.4563 22.0197L2 41.1093Z"
|
||||
fill="#EEE8D5"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
<path
|
||||
d="M10.67 40.7598C13.3468 40.7598 16.2357 40.7598 19.3365 40.7598C20.6124 40.7598 20.6124 41.6564 20.6124 42.1198C20.6124 46.6264 24.3498 50.2798 28.9601 50.2798C33.5705 50.2798 37.3079 46.6264 37.3079 42.1198C37.3079 41.6564 37.3079 40.7598 38.5837 40.7598H55.55M5.62012 40.7598H7.26998H5.62012Z"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M47.4277 4.92586L39.88 13.394M29.7477 2V13.394V2ZM12 4.92586L19.5477 13.394L12 4.92586Z"
|
||||
stroke="#2A2A2A"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal file
35
packages/web/components/elements/icons/UntrashIcon.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-disable functional/no-class */
|
||||
/* eslint-disable functional/no-this-expression */
|
||||
import { IconProps } from './IconProps'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export class UntrashIcon extends React.Component<IconProps> {
|
||||
render() {
|
||||
const size = (this.props.size || 26).toString()
|
||||
const color = (this.props.color || '#2A2A2A').toString()
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
strokeWidth="1.5"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 3l18 18" />
|
||||
<path d="M4 7h3m4 0h9" />
|
||||
<path d="M10 11l0 6" />
|
||||
<path d="M14 14l0 3" />
|
||||
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l.077 -.923" />
|
||||
<path d="M18.384 14.373l.616 -7.373" />
|
||||
<path d="M9 5v-1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DEFAULT_HOME_PATH } from '../../../lib/navigations'
|
||||
import { Box } from '../LayoutPrimitives'
|
||||
export type OmnivoreLogoBaseProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
|
|
@ -9,35 +10,33 @@ export type OmnivoreLogoBaseProps = {
|
|||
}
|
||||
|
||||
export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
|
||||
const href = props.href || '/home'
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Link
|
||||
passHref
|
||||
href={href}
|
||||
<Box
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(event) => {
|
||||
const navReturn = window.localStorage.getItem('nav-return')
|
||||
if (navReturn) {
|
||||
window.location.assign(navReturn)
|
||||
router.push(navReturn)
|
||||
return
|
||||
}
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
if (query) {
|
||||
window.location.assign(`${DEFAULT_HOME_PATH}?${query}`)
|
||||
router.push(`${DEFAULT_HOME_PATH}?${query}`)
|
||||
} else {
|
||||
window.location.replace(DEFAULT_HOME_PATH)
|
||||
router.push(DEFAULT_HOME_PATH)
|
||||
}
|
||||
}}
|
||||
tabIndex={-1}
|
||||
aria-label="Omnivore logo"
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,14 +31,15 @@ import { highlightColor } from '../../lib/themeUpdater'
|
|||
|
||||
import { HighlightViewNote } from '../patterns/HighlightNotes'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { useDeleteHighlight } from '../../lib/networking/highlights/useItemHighlights'
|
||||
import { EmptyLibrary } from '../templates/homeFeed/EmptyLibrary'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
export function HighlightsContainer(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const viewer = useGetViewerQuery()
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false)
|
||||
const [_, setShowAddLinkModal] = useState(false)
|
||||
const { data: viewerData } = useGetViewer()
|
||||
|
||||
const { isLoading, setSize, size, data, mutate } = useGetHighlights({
|
||||
first: PAGE_SIZE,
|
||||
|
|
@ -72,19 +73,25 @@ export function HighlightsContainer(): JSX.Element {
|
|||
css={{
|
||||
padding: '20px',
|
||||
margin: '30px',
|
||||
width: '100%',
|
||||
'@mdDown': {
|
||||
margin: '0px',
|
||||
marginTop: '50px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!isLoading && highlights.length < 1 && (
|
||||
<Box css={{ width: '100%' }}>
|
||||
<EmptyLibrary folder="highlights" />
|
||||
</Box>
|
||||
)}
|
||||
{highlights.map((highlight) => {
|
||||
return (
|
||||
viewer.viewerData?.me && (
|
||||
viewerData && (
|
||||
<HighlightCard
|
||||
key={highlight.id}
|
||||
highlight={highlight}
|
||||
viewer={viewer.viewerData.me}
|
||||
viewer={viewerData}
|
||||
router={router}
|
||||
mutate={mutate}
|
||||
/>
|
||||
|
|
@ -133,6 +140,7 @@ function HighlightCard(props: HighlightCardProps): JSX.Element {
|
|||
useState<undefined | string>(undefined)
|
||||
const [labelsTarget, setLabelsTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
|
||||
const viewInReader = useCallback(
|
||||
(highlightId: string) => {
|
||||
|
|
@ -283,24 +291,22 @@ function HighlightCard(props: HighlightCardProps): JSX.Element {
|
|||
message={'Are you sure you want to delete this highlight?'}
|
||||
onAccept={() => {
|
||||
;(async () => {
|
||||
const highlightId = showConfirmDeleteHighlightId
|
||||
const success = await deleteHighlightMutation(
|
||||
props.highlight.libraryItem?.id || '',
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
props.mutate()
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
const event = new CustomEvent('deleteHighlightbyId', {
|
||||
detail: highlightId,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight', {
|
||||
position: 'bottom-right',
|
||||
if (props.highlight.libraryItem) {
|
||||
const success = await deleteHighlight.mutateAsync({
|
||||
itemId: props.highlight.libraryItem?.id,
|
||||
slug: props.highlight.libraryItem?.slug,
|
||||
highlightId: showConfirmDeleteHighlightId,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error deleting highlight', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
setShowConfirmDeleteHighlightId(undefined)
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ import {
|
|||
} from '../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { Box, HStack, SpanBox, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import useLibraryItemActions from '../../lib/hooks/useLibraryItemActions'
|
||||
import { SyncLoader } from 'react-spinners'
|
||||
import { useGetRawSearchItemsQuery } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetLibraryItems } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
type HomeState = {
|
||||
items: HomeItem[]
|
||||
|
|
@ -198,7 +198,7 @@ export function HomeContainer(): JSX.Element {
|
|||
const homeData = useGetHomeItems()
|
||||
|
||||
const router = useRouter()
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { data: viewerData } = useGetViewer()
|
||||
|
||||
const hasTopPicks = (homeData: HomeItemResponse) => {
|
||||
const topPicks = homeData.sections?.find(
|
||||
|
|
@ -210,44 +210,47 @@ export function HomeContainer(): JSX.Element {
|
|||
|
||||
const shouldFallback =
|
||||
homeData.error || (!homeData.isValidating && !hasTopPicks(homeData))
|
||||
const searchData = useGetRawSearchItemsQuery(
|
||||
const searchData = useGetLibraryItems(
|
||||
undefined,
|
||||
{
|
||||
limit: 10,
|
||||
searchQuery: 'in:inbox',
|
||||
includeContent: false,
|
||||
sortDescending: true,
|
||||
},
|
||||
// only enable this search if we didn't get home data
|
||||
shouldFallback
|
||||
)
|
||||
|
||||
useApplyLocalTheme()
|
||||
|
||||
const viewerUsername = useMemo(() => {
|
||||
return viewerData?.me?.profile.username
|
||||
return viewerData?.profile.username
|
||||
}, [viewerData])
|
||||
|
||||
const searchItems = useMemo(() => {
|
||||
return searchData.items.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
date: item.savedAt,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
slug: item.slug,
|
||||
score: 1.0,
|
||||
thumbnail: item.image,
|
||||
previewContent: item.description,
|
||||
source: {
|
||||
name: item.folder == 'following' ? item.subscription : item.siteName,
|
||||
icon: item.siteIcon,
|
||||
type: 'LIBRARY',
|
||||
},
|
||||
canArchive: true,
|
||||
canDelete: true,
|
||||
canShare: true,
|
||||
canMove: item.folder == 'following',
|
||||
} as HomeItem
|
||||
})
|
||||
return []
|
||||
// return searchData.items.map((item) => {
|
||||
// return {
|
||||
// id: item.id,
|
||||
// date: item.savedAt,
|
||||
// title: item.title,
|
||||
// url: item.url,
|
||||
// slug: item.slug,
|
||||
// score: 1.0,
|
||||
// thumbnail: item.image,
|
||||
// previewContent: item.description,
|
||||
// source: {
|
||||
// name: item.folder == 'following' ? item.subscription : item.siteName,
|
||||
// icon: item.siteIcon,
|
||||
// type: 'LIBRARY',
|
||||
// },
|
||||
// canArchive: true,
|
||||
// canDelete: true,
|
||||
// canShare: true,
|
||||
// canMove: item.folder == 'following',
|
||||
// } as HomeItem
|
||||
// })
|
||||
}, [searchData])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -383,7 +386,7 @@ export function HomeContainer(): JSX.Element {
|
|||
)
|
||||
|
||||
const dataReady =
|
||||
!homeData.isValidating && (!shouldFallback || !searchData.isValidating)
|
||||
!homeData.isValidating && (!shouldFallback || !searchData.isLoading)
|
||||
if (!dataReady || (homeData.error && homeData.errorMessage == 'PENDING')) {
|
||||
console.log('showing pending')
|
||||
return (
|
||||
|
|
@ -544,7 +547,7 @@ const JustAddedHomeSection = (props: HomeSectionProps): JSX.Element => {
|
|||
<Button
|
||||
style="link"
|
||||
onClick={(event) => {
|
||||
router.push('/l/library')
|
||||
router.push('/library')
|
||||
event.preventDefault()
|
||||
}}
|
||||
css={{
|
||||
|
|
@ -946,12 +949,12 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
useLibraryItemActions()
|
||||
|
||||
const doArchiveItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
})
|
||||
if (!(await archiveItem(libraryItemId))) {
|
||||
if (!(await archiveItem(libraryItemId, slug))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// itemId: libraryItemId,
|
||||
|
|
@ -962,7 +965,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
)
|
||||
|
||||
const doDeleteItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
|
|
@ -973,7 +976,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
// : libraryItemId,
|
||||
// })
|
||||
}
|
||||
if (!(await deleteItem(libraryItemId, undo))) {
|
||||
if (!(await deleteItem(libraryItemId, slug, undo))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// payload: libraryItemId,
|
||||
|
|
@ -984,12 +987,12 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
)
|
||||
|
||||
const doMoveItem = useCallback(
|
||||
async (libraryItemId: string) => {
|
||||
async (libraryItemId: string, slug: string) => {
|
||||
dispatch({
|
||||
type: 'REMOVE_ITEM',
|
||||
payload: libraryItemId,
|
||||
})
|
||||
if (!(await moveItem(libraryItemId))) {
|
||||
if (!(await moveItem(libraryItemId, slug))) {
|
||||
// dispatch({
|
||||
// type: 'REPLACE_ITEM',
|
||||
// payload: libraryItemId,
|
||||
|
|
@ -1039,13 +1042,13 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
;(event.target as HTMLElement).click()
|
||||
break
|
||||
case 'e':
|
||||
doArchiveItem(props.homeItem.id)
|
||||
doArchiveItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case '#':
|
||||
doDeleteItem(props.homeItem.id)
|
||||
doDeleteItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case 'm':
|
||||
doMoveItem(props.homeItem.id)
|
||||
doMoveItem(props.homeItem.id, props.homeItem.slug)
|
||||
break
|
||||
case 'o':
|
||||
window.open(props.homeItem.url, '_blank')
|
||||
|
|
@ -1093,8 +1096,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doMoveItem(props.homeItem.id)
|
||||
await doMoveItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<AddToLibraryActionIcon />
|
||||
|
|
@ -1107,8 +1109,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doArchiveItem(props.homeItem.id)
|
||||
await doArchiveItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<ArchiveActionIcon />
|
||||
|
|
@ -1121,8 +1122,7 @@ const TopPicksItemView = (props: HomeItemViewProps): JSX.Element => {
|
|||
onClick={async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
await doDeleteItem(props.homeItem.id)
|
||||
await doDeleteItem(props.homeItem.id, props.homeItem.slug)
|
||||
}}
|
||||
>
|
||||
<RemoveActionIcon />
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const saveText = useCallback(
|
||||
(text: string) => {
|
||||
;(async () => {
|
||||
console.log('saving text: ', text)
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
|
||||
import { LibraryItemNode } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
LibraryItemNode,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { State } from '../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type CardMenuDropdownAction =
|
||||
| 'mark-read'
|
||||
| 'mark-unread'
|
||||
| 'archive'
|
||||
| 'unarchive'
|
||||
| 'delete'
|
||||
|
|
@ -24,12 +26,15 @@ type CardMenuProps = {
|
|||
}
|
||||
|
||||
export function CardMenu(props: CardMenuProps): JSX.Element {
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
triggerElement={props.triggerElement}
|
||||
onOpenChange={props.onOpenChange}
|
||||
css={{ bg: '$thNavMenuFooter' }}
|
||||
>
|
||||
{!props.item.isArchived ? (
|
||||
{props.item.state != State.ARCHIVED ? (
|
||||
<DropdownOption
|
||||
onSelect={() => props.actionHandler('archive')}
|
||||
title="Archive"
|
||||
|
|
@ -62,15 +67,31 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
|
|||
/>
|
||||
{props.item.readingProgressPercent < 98 ? (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('mark-read')
|
||||
onSelect={async () => {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: props.item.id,
|
||||
readingProgressPercent: 100,
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
}}
|
||||
title="Mark read"
|
||||
/>
|
||||
) : (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('mark-unread')
|
||||
onSelect={async () => {
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: props.item.id,
|
||||
readingProgressPercent: 0,
|
||||
force: true,
|
||||
},
|
||||
})
|
||||
}}
|
||||
title="Mark unread"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
|
|||
const saveText = useCallback(
|
||||
(text: string, updateTime: Date, interactive: boolean) => {
|
||||
;(async () => {
|
||||
console.log('updating highlight text')
|
||||
const success = await updateHighlightMutation({
|
||||
annotation: text,
|
||||
libraryItemId: props.targetId,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { HighlightViewNote } from './HighlightNotes'
|
|||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { highlightColorVar } from '../../lib/themeUpdater'
|
||||
import { ReadableItem } from '../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import {
|
||||
autoUpdate,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { LayoutType } from '../../templates/homeFeed/HomeFeedContainer'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import type { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import type { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader'
|
||||
|
||||
export type LinkedItemCardAction =
|
||||
|
|
@ -18,6 +18,7 @@ export type LinkedItemCardAction =
|
|||
| 'update-item'
|
||||
| 'move-to-inbox'
|
||||
| 'refresh'
|
||||
| 'restore'
|
||||
|
||||
export type LinkedItemCardProps = {
|
||||
item: LibraryItemNode
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useMemo } from 'react'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { HStack, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { RecommendedFlairIcon } from '../../elements/icons/RecommendedFlairIcon'
|
||||
import { PinnedFlairIcon } from '../../elements/icons/PinnedFlairIcon'
|
||||
|
|
@ -140,9 +140,7 @@ type FlairIconProps = {
|
|||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function FlairIcon(
|
||||
props: FlairIconProps
|
||||
): JSX.Element {
|
||||
export function FlairIcon(props: FlairIconProps): JSX.Element {
|
||||
return (
|
||||
<SpanBox title={props.title} css={{ lineHeight: '1' }}>
|
||||
{props.children}
|
||||
|
|
@ -179,11 +177,7 @@ type LibraryItemMetadataProps = {
|
|||
export function LibraryItemMetadata(
|
||||
props: LibraryItemMetadataProps
|
||||
): JSX.Element {
|
||||
const highlightCount = useMemo(() => {
|
||||
return (
|
||||
props.item.highlights?.filter((h) => h.type == 'HIGHLIGHT').length ?? 0
|
||||
)
|
||||
}, [props.item.highlights])
|
||||
const highlightCount = props.item.highlightsCount ?? 0
|
||||
|
||||
return (
|
||||
<HStack css={{ gap: '5px', alignItems: 'center' }}>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react'
|
|||
import { MetaStyle, timeAgo, TitleStyle } from './LibraryCardStyles'
|
||||
import { styled } from '@stitches/react'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { useState } from 'react'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { LibraryItemNode } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
LibraryItemNode,
|
||||
useArchiveItem,
|
||||
useDeleteItem,
|
||||
useRestoreItem,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { LinkedItemCardAction } from './CardTypes'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -14,6 +19,8 @@ import { LabelIcon } from '../../elements/icons/LabelIcon'
|
|||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { BrowserIcon } from '../../elements/icons/BrowserIcon'
|
||||
import { MoveToInboxIcon } from '../../elements/icons/MoveToInboxIcon'
|
||||
import { UntrashIcon } from '../../elements/icons/UntrashIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
type LibraryHoverActionsProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -26,6 +33,9 @@ type LibraryHoverActionsProps = {
|
|||
|
||||
export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const archiveItem = useArchiveItem()
|
||||
const deleteItem = useDeleteItem()
|
||||
const restoreItem = useRestoreItem()
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -89,16 +99,26 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
|||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
title={props.item.isArchived ? 'Unarchive (e)' : 'Archive (e)'}
|
||||
title={
|
||||
props.item.state === State.ARCHIVED
|
||||
? 'Unarchive (e)'
|
||||
: 'Archive (e)'
|
||||
}
|
||||
style="hoverActionIcon"
|
||||
onClick={(event) => {
|
||||
const action = props.item.isArchived ? 'unarchive' : 'archive'
|
||||
props.handleAction(action)
|
||||
onClick={async (event) => {
|
||||
await archiveItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
linkId: props.item.id,
|
||||
archived: props.item.state !== State.ARCHIVED,
|
||||
},
|
||||
})
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{props.item.isArchived ? (
|
||||
{props.item.state === State.ARCHIVED ? (
|
||||
<UnarchiveIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
|
|
@ -112,15 +132,35 @@ export const LibraryHoverActions = (props: LibraryHoverActionsProps) => {
|
|||
</Button>
|
||||
)}
|
||||
<Button
|
||||
title="Remove (#)"
|
||||
title={props.item.state == State.DELETED ? 'Restore' : 'Remove (#)'}
|
||||
style="hoverActionIcon"
|
||||
onClick={(event) => {
|
||||
props.handleAction('delete')
|
||||
onClick={async (event) => {
|
||||
if (props.item.state == State.DELETED) {
|
||||
await restoreItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
})
|
||||
} else {
|
||||
await deleteItem.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
})
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={21} color={theme.colors.thNotebookSubtle.toString()} />
|
||||
{props.item.state == State.DELETED ? (
|
||||
<UntrashIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
/>
|
||||
) : (
|
||||
<TrashIcon
|
||||
size={21}
|
||||
color={theme.colors.thNotebookSubtle.toString()}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
title="Edit labels (l)"
|
||||
|
|
|
|||
|
|
@ -85,12 +85,15 @@ export function LibraryListCard(props: LinkedItemCardProps): JSX.Element {
|
|||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
gap: '10px',
|
||||
borderStyle: 'none',
|
||||
borderBottom: 'none',
|
||||
borderRadius: '6px',
|
||||
borderBottom: props.legacyLayout
|
||||
? 'unset'
|
||||
: '1px solid $thLeftMenuBackground',
|
||||
'@media (max-width: 930px)': {
|
||||
borderRadius: '0px',
|
||||
},
|
||||
'&:hover': {
|
||||
borderBottom: 'unset',
|
||||
},
|
||||
...layoutWidths,
|
||||
}}
|
||||
alignment="start"
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import {
|
|||
DropdownOption,
|
||||
DropdownSeparator,
|
||||
} from '../elements/DropdownElements'
|
||||
import { ArticleAttributes } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { State } from '../../lib/networking/fragments/articleFragment'
|
||||
|
||||
type DropdownMenuProps = {
|
||||
triggerElement: ReactNode
|
||||
libraryItem?: ArticleAttributes
|
||||
articleActionHandler: (action: string, arg?: unknown) => void
|
||||
}
|
||||
|
||||
|
|
@ -14,8 +17,18 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
|
|||
return (
|
||||
<Dropdown triggerElement={props.triggerElement}>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('archive')}
|
||||
title="Archive (e)"
|
||||
onSelect={async () => {
|
||||
if (props.libraryItem?.state === State.ARCHIVED) {
|
||||
props.articleActionHandler('unarchive')
|
||||
} else {
|
||||
props.articleActionHandler('archive')
|
||||
}
|
||||
}}
|
||||
title={
|
||||
props.libraryItem?.state === State.ARCHIVED
|
||||
? 'Unarchive (e)'
|
||||
: 'Archive (e)'
|
||||
}
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('setLabels')}
|
||||
|
|
@ -26,7 +39,9 @@ export function ReaderDropdownMenu(props: DropdownMenuProps): JSX.Element {
|
|||
title="Edit info (i)"
|
||||
/>
|
||||
<DropdownOption
|
||||
onSelect={() => props.articleActionHandler('delete')}
|
||||
onSelect={async () => {
|
||||
props.articleActionHandler('delete')
|
||||
}}
|
||||
title="Remove (#)"
|
||||
/>
|
||||
<DropdownSeparator />
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { StyledText } from '../elements/StyledText'
|
|||
import Link from 'next/link'
|
||||
import { Button } from '../elements/Button'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
type ErrorPageStatusCode = 404 | 500
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ type ErrorLayoutProps = {
|
|||
}
|
||||
|
||||
export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { data: viewerData } = useGetViewer()
|
||||
|
||||
return (
|
||||
<VStack alignment="center" distribution="start" css={{ height: '100%' }}>
|
||||
|
|
@ -32,11 +33,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
|
|||
</StyledText>
|
||||
</HStack>
|
||||
<SpanBox css={{ height: '64px' }} />
|
||||
<Link passHref href={viewerData?.me ? '/home' : '/login'} legacyBehavior>
|
||||
<Link passHref href={viewerData ? '/home' : '/login'} legacyBehavior>
|
||||
<Button style="ctaDarkYellow">
|
||||
{viewerData?.me ? 'Go Home' : 'Login'}
|
||||
{viewerData ? 'Go Home' : 'Login'}
|
||||
</Button>
|
||||
</Link>
|
||||
</VStack>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,20 +11,22 @@ import { setupAnalytics } from '../../lib/analytics'
|
|||
import { primaryCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { logout } from '../../lib/logout'
|
||||
import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
|
||||
import { updateTheme } from '../../lib/themeUpdater'
|
||||
import { Priority, useRegisterActions } from 'kbar'
|
||||
import { ThemeId, theme } from '../tokens/stitches.config'
|
||||
import { useRegisterActions } from 'kbar'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { NavigationMenu } from './navMenu/NavigationMenu'
|
||||
import { Button } from '../elements/Button'
|
||||
import { List } from '@phosphor-icons/react'
|
||||
import { LIBRARY_LEFT_MENU_WIDTH } from './navMenu/LibraryLegacyMenu'
|
||||
import { AddLinkModal } from './AddLinkModal'
|
||||
import { saveUrlMutation } from '../../lib/networking/mutations/saveUrlMutation'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {
|
||||
showErrorToast,
|
||||
showSuccessToastWithAction,
|
||||
} from '../../lib/toastHelpers'
|
||||
import useWindowDimensions from '../../lib/hooks/useGetWindowDimensions'
|
||||
import { useAddItem } from '../../lib/networking/library_items/useLibraryItems'
|
||||
import { useHandleAddUrl } from '../../lib/hooks/useHandleAddUrl'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
export type NavigationSection =
|
||||
| 'home'
|
||||
|
|
@ -47,11 +49,12 @@ type NavigationLayoutProps = {
|
|||
export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
|
||||
useApplyLocalTheme()
|
||||
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { data: viewerData } = useGetViewer()
|
||||
const router = useRouter()
|
||||
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
|
||||
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
|
||||
useState(false)
|
||||
const addItem = useAddItem()
|
||||
|
||||
useRegisterActions(navigationCommands(router))
|
||||
|
||||
|
|
@ -67,8 +70,10 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
|
|||
|
||||
// Attempt to identify the user if they are logged in.
|
||||
useEffect(() => {
|
||||
setupAnalytics(viewerData?.me)
|
||||
}, [viewerData?.me])
|
||||
if (viewerData) {
|
||||
setupAnalytics(viewerData)
|
||||
}
|
||||
}, [viewerData])
|
||||
|
||||
const showLogout = useCallback(() => {
|
||||
setShowLogoutConfirmation(true)
|
||||
|
|
@ -84,22 +89,7 @@ export function NavigationLayout(props: NavigationLayoutProps): JSX.Element {
|
|||
|
||||
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
|
||||
|
||||
const handleLinkAdded = useCallback(
|
||||
async (link: string, timezone: string, locale: string) => {
|
||||
const result = await saveUrlMutation(link, timezone, locale)
|
||||
if (result) {
|
||||
showSuccessToastWithAction('Link saved', 'Read now', async () => {
|
||||
window.location.href = `/article?url=${encodeURIComponent(link)}`
|
||||
return Promise.resolve()
|
||||
})
|
||||
// const id = result.url?.match(/[^/]+$/)?.[0] ?? ''
|
||||
// performActionOnItem('refresh', undefined as unknown as any)
|
||||
} else {
|
||||
showErrorToast('Error saving link', { position: 'bottom-right' })
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
const handleLinkAdded = useHandleAddUrl()
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('logout', showLogout)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { styled, theme, ThemeId } from '../tokens/stitches.config'
|
|||
import { LayoutType } from './homeFeed/HomeFeedContainer'
|
||||
import { useCurrentTheme } from '../../lib/hooks/useCurrentTheme'
|
||||
import { ThemeSelector } from './article/ReaderSettingsControl'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
type PrimaryDropdownProps = {
|
||||
children?: ReactNode
|
||||
|
|
@ -82,7 +83,7 @@ const TriggerButton = (props: TriggerButtonProps): JSX.Element => {
|
|||
}
|
||||
|
||||
export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { data: viewerData } = useGetViewer()
|
||||
const router = useRouter()
|
||||
|
||||
const headerDropdownActionHandler = useCallback(
|
||||
|
|
@ -129,7 +130,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
|||
<Dropdown
|
||||
side="top"
|
||||
triggerElement={
|
||||
props.children ?? <TriggerButton name={viewerData?.me?.name} />
|
||||
props.children ?? <TriggerButton name={viewerData?.name} />
|
||||
}
|
||||
css={{ width: '240px', ml: '15px', bg: '$thNavMenuFooter' }}
|
||||
>
|
||||
|
|
@ -150,16 +151,16 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<Avatar
|
||||
imageURL={viewerData?.me?.profile.pictureUrl}
|
||||
imageURL={viewerData?.profile.pictureUrl}
|
||||
height="40px"
|
||||
fallbackText={viewerData?.me?.name.charAt(0) ?? ''}
|
||||
fallbackText={viewerData?.name.charAt(0) ?? ''}
|
||||
/>
|
||||
<VStack
|
||||
css={{ height: '40px', maxWidth: '240px' }}
|
||||
alignment="start"
|
||||
distribution="around"
|
||||
>
|
||||
{viewerData?.me && (
|
||||
{viewerData && (
|
||||
<>
|
||||
<StyledText
|
||||
css={{
|
||||
|
|
@ -173,7 +174,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
|||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{viewerData.me.name}
|
||||
{viewerData.name}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
|
|
@ -186,7 +187,7 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
|
|||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{`@${viewerData.me.profile.username}`}
|
||||
{`@${viewerData.profile.username}`}
|
||||
</StyledText>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { updateTheme } from '../../lib/themeUpdater'
|
|||
import { Priority, useRegisterActions } from 'kbar'
|
||||
import { ThemeId } from '../tokens/stitches.config'
|
||||
import { useVerifyAuth } from '../../lib/hooks/useVerifyAuth'
|
||||
import { useGetViewer } from '../../lib/networking/viewer/useGetViewer'
|
||||
|
||||
type PrimaryLayoutProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -28,7 +29,7 @@ type PrimaryLayoutProps = {
|
|||
export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
useApplyLocalTheme()
|
||||
|
||||
const { viewerData } = useGetViewerQuery()
|
||||
const { data: viewerData } = useGetViewer()
|
||||
const router = useRouter()
|
||||
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
|
||||
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
|
||||
|
|
@ -78,8 +79,10 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
|||
|
||||
// Attempt to identify the user if they are logged in.
|
||||
useEffect(() => {
|
||||
setupAnalytics(viewerData?.me)
|
||||
}, [viewerData?.me])
|
||||
if (viewerData) {
|
||||
setupAnalytics(viewerData)
|
||||
}
|
||||
}, [viewerData])
|
||||
|
||||
const showLogout = useCallback(() => {
|
||||
setShowLogoutConfirmation(true)
|
||||
|
|
|
|||
443
packages/web/components/templates/ShortcutsTree.tsx
Normal file
443
packages/web/components/templates/ShortcutsTree.tsx
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { NodeApi, SimpleTree, Tree, TreeApi } from 'react-arborist'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import {
|
||||
Shortcut,
|
||||
useGetShortcuts,
|
||||
useSetShortcuts,
|
||||
} from '../../lib/networking/shortcuts/useShortcuts'
|
||||
import { usePersistedState } from '../../lib/hooks/usePersistedState'
|
||||
import { CSSProperties, useCallback, useMemo, useState } from 'react'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Box, HStack, SpanBox } from '../elements/LayoutPrimitives'
|
||||
import { Dropdown, DropdownOption } from '../elements/DropdownElements'
|
||||
import { DotsThree, ListMagnifyingGlass, Tag } from '@phosphor-icons/react'
|
||||
import { ShortcutFolderClosed } from '../elements/icons/ShortcutFolderClosed'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { ShortcutFolderOpen } from '../elements/icons/ShortcutFolderOpen'
|
||||
import { CoverImage } from '../elements/CoverImage'
|
||||
import { NewsletterIcon } from '../elements/icons/NewsletterIcon'
|
||||
import { FollowingIcon } from '../elements/icons/FollowingIcon'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { OpenMap } from 'react-arborist/dist/module/state/open-slice'
|
||||
|
||||
type ShortcutsTreeProps = {
|
||||
treeRef: React.MutableRefObject<TreeApi<Shortcut> | undefined>
|
||||
}
|
||||
|
||||
export const ShortcutsTree = (props: ShortcutsTreeProps): JSX.Element => {
|
||||
const router = useRouter()
|
||||
const { ref, width, height } = useResizeObserver()
|
||||
const { data, isLoading } = useGetShortcuts()
|
||||
const setShorcuts = useSetShortcuts()
|
||||
|
||||
const [folderOpenState, setFolderOpenState] = usePersistedState<
|
||||
Record<string, boolean>
|
||||
>({
|
||||
key: 'nav-menu-open-state',
|
||||
isSessionStorage: false,
|
||||
initialValue: {},
|
||||
})
|
||||
const tree = useMemo(() => {
|
||||
const result = new SimpleTree<Shortcut>((data ?? []) as Shortcut[])
|
||||
return result
|
||||
}, [data])
|
||||
|
||||
const syncTreeData = async (data: Shortcut[]) => {
|
||||
await setShorcuts.mutateAsync({ shortcuts: data })
|
||||
}
|
||||
|
||||
const onMove = useCallback(
|
||||
async (args: {
|
||||
dragIds: string[]
|
||||
parentId: null | string
|
||||
index: number
|
||||
}) => {
|
||||
for (const id of args.dragIds) {
|
||||
tree?.move({ id, parentId: args.parentId, index: args.index })
|
||||
}
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onCreate = useCallback(
|
||||
async (args: { parentId: string | null; index: number; type: string }) => {
|
||||
const data = { id: uuidv4(), name: '', type: 'folder' } as any
|
||||
if (args.type === 'internal') {
|
||||
data.children = []
|
||||
}
|
||||
tree.create({ parentId: args.parentId, index: args.index, data })
|
||||
await syncTreeData(tree.data)
|
||||
return data
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onDelete = useCallback(
|
||||
async (args: { ids: string[] }) => {
|
||||
args.ids.forEach((id) => tree.drop({ id }))
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onRename = useCallback(
|
||||
async (args: { name: string; id: string }) => {
|
||||
tree.update({ id: args.id, changes: { name: args.name } as any })
|
||||
await syncTreeData(tree.data)
|
||||
},
|
||||
[tree, data]
|
||||
)
|
||||
|
||||
const onToggle = useCallback(
|
||||
(id: string) => {
|
||||
if (id && props.treeRef.current) {
|
||||
const isOpen = props.treeRef.current?.isOpen(id)
|
||||
const newItem: OpenMap = {}
|
||||
newItem[id] = isOpen
|
||||
setFolderOpenState({ ...folderOpenState, ...newItem })
|
||||
}
|
||||
},
|
||||
[props, folderOpenState, setFolderOpenState]
|
||||
)
|
||||
|
||||
const onActivate = useCallback(
|
||||
(node: NodeApi<Shortcut>) => {
|
||||
if (node.data.type == 'folder') {
|
||||
const join = node.data.join
|
||||
if (join == 'or') {
|
||||
const query = node.children
|
||||
?.map((child) => {
|
||||
return `(${child.data.filter})`
|
||||
})
|
||||
.join(' OR ')
|
||||
}
|
||||
} else if (node.data.section != null && node.data.filter != null) {
|
||||
router.push(`/${node.data.section}?q=${node.data.filter}`)
|
||||
}
|
||||
},
|
||||
[tree, router]
|
||||
)
|
||||
|
||||
function countTotalShortcuts(shortcuts: Shortcut[]): number {
|
||||
let total = 0
|
||||
|
||||
for (const shortcut of shortcuts) {
|
||||
// Count the current shortcut
|
||||
total++
|
||||
|
||||
// If the shortcut has children, recursively count them
|
||||
if (shortcut.children && shortcut.children.length > 0) {
|
||||
total += countTotalShortcuts(shortcut.children)
|
||||
}
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const maximumHeight = useMemo(() => {
|
||||
if (!data) {
|
||||
return 320
|
||||
}
|
||||
return countTotalShortcuts(data as Shortcut[]) * 36
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
css={{
|
||||
height: maximumHeight,
|
||||
flexGrow: 1,
|
||||
minBlockSize: 0,
|
||||
}}
|
||||
>
|
||||
{!isLoading && (
|
||||
<Tree
|
||||
ref={props.treeRef}
|
||||
data={data as Shortcut[]}
|
||||
onCreate={onCreate}
|
||||
onMove={onMove}
|
||||
onDelete={onDelete}
|
||||
onRename={onRename}
|
||||
onToggle={onToggle}
|
||||
onActivate={onActivate}
|
||||
rowHeight={36}
|
||||
initialOpenState={folderOpenState}
|
||||
width={width}
|
||||
height={maximumHeight}
|
||||
>
|
||||
{NodeRenderer}
|
||||
</Tree>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function NodeRenderer(args: {
|
||||
style: CSSProperties
|
||||
node: NodeApi<Shortcut>
|
||||
tree: TreeApi<Shortcut>
|
||||
dragHandle?: (el: HTMLDivElement | null) => void
|
||||
preview?: boolean
|
||||
}) {
|
||||
const isSelected = false
|
||||
const [menuVisible, setMenuVisible] = useState(false)
|
||||
const [menuOpened, setMenuOpened] = useState(false)
|
||||
|
||||
return (
|
||||
<HStack
|
||||
ref={args.dragHandle}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
pl: `${20 + args.node.level * 15}px`,
|
||||
mb: '2px',
|
||||
gap: '10px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
height: '34px',
|
||||
|
||||
backgroundColor: isSelected ? '$thLibrarySelectionColor' : 'unset',
|
||||
fontSize: '15px',
|
||||
fontWeight: 'regular',
|
||||
fontFamily: '$display',
|
||||
color: isSelected
|
||||
? '$thLibraryMenuSecondary'
|
||||
: '$thLibraryMenuUnselected',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:active': {
|
||||
outline: 'unset',
|
||||
backgroundColor: isSelected
|
||||
? '$thLibrarySelectionColor'
|
||||
: '$thBackground4',
|
||||
},
|
||||
'&:hover [role="hover-menu"]': {
|
||||
opacity: '1',
|
||||
},
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setMenuVisible(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setMenuVisible(false)
|
||||
}}
|
||||
title={args.node.data.name}
|
||||
onClick={(e) => {
|
||||
// router.push(`/` + props.section)
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<NodeItemContents node={args.node} />
|
||||
<SpanBox
|
||||
role="hover-menu"
|
||||
css={{
|
||||
display: 'flex',
|
||||
ml: 'auto',
|
||||
mr: '15px',
|
||||
opacity: menuVisible || menuOpened ? '1' : '0',
|
||||
}}
|
||||
>
|
||||
<Dropdown
|
||||
side="bottom"
|
||||
triggerElement={<DotsThree size={20} />}
|
||||
css={{ ml: 'auto' }}
|
||||
onOpenChange={(open) => {
|
||||
setMenuOpened(open)
|
||||
}}
|
||||
>
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.tree.delete(args.node)
|
||||
}}
|
||||
title="Remove"
|
||||
/>
|
||||
{/* {args.node.data.type == 'folder' && (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
args.node.data.join = 'or'
|
||||
}}
|
||||
title="Folder query: OR"
|
||||
/>
|
||||
)} */}
|
||||
</Dropdown>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
type NodeItemContentsProps = {
|
||||
node: NodeApi<Shortcut>
|
||||
}
|
||||
|
||||
const NodeItemContents = (props: NodeItemContentsProps): JSX.Element => {
|
||||
if (props.node.isEditing) {
|
||||
return (
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
defaultValue={props.node.data.name}
|
||||
onFocus={(e) => e.currentTarget.select()}
|
||||
onBlur={() => props.node.reset()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
props.node.reset()
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
// props.node.data = {
|
||||
// id: 'new-folder',
|
||||
// type: 'folder',
|
||||
// name: e.currentTarget.value,
|
||||
// }
|
||||
props.node.submit(e.currentTarget.value)
|
||||
props.node.activate()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (props.node.isLeaf) {
|
||||
const shortcut = props.node.data
|
||||
if (shortcut) {
|
||||
switch (shortcut.type) {
|
||||
case 'feed':
|
||||
case 'newsletter':
|
||||
return (
|
||||
<SpanBox>
|
||||
<FeedOrNewsletterShortcut shortcut={shortcut} />
|
||||
</SpanBox>
|
||||
)
|
||||
case 'label':
|
||||
return (
|
||||
<Box>
|
||||
<LabelShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
case 'search':
|
||||
return (
|
||||
<Box>
|
||||
<SearchShortcut shortcut={shortcut} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ gap: '10px', width: '100%' }}
|
||||
onClick={(event) => {
|
||||
props.node.toggle()
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.node.isClosed ? (
|
||||
<ShortcutFolderClosed
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
) : (
|
||||
<ShortcutFolderOpen
|
||||
color={theme.colors.thLibraryMenuPrimary.toString()}
|
||||
/>
|
||||
)}
|
||||
{props.node.data.name}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
type ShortcutItemProps = {
|
||||
shortcut: Shortcut
|
||||
}
|
||||
|
||||
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '10px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
{props.shortcut.icon ? (
|
||||
<CoverImage
|
||||
src={props.shortcut.icon}
|
||||
width={20}
|
||||
height={20}
|
||||
css={{ borderRadius: '20px' }}
|
||||
/>
|
||||
) : props.shortcut.type == 'newsletter' ? (
|
||||
<NewsletterIcon color="#F59932" size={18} />
|
||||
) : (
|
||||
<FollowingIcon color="#F59932" size={21} />
|
||||
)}
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ pl: '10px', width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ minWidth: '20px' }}
|
||||
>
|
||||
<ListMagnifyingGlass size={17} />
|
||||
</HStack>
|
||||
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', gap: '7px' }}
|
||||
key={`search-${props.shortcut.id}`}
|
||||
>
|
||||
<Tag
|
||||
size={15}
|
||||
color={props.shortcut.label?.color ?? 'gray'}
|
||||
weight="fill"
|
||||
/>
|
||||
<StyledText style="settingsItem" css={{ pb: '1px' }}>
|
||||
{props.shortcut.name}
|
||||
</StyledText>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelAction } from '../../../lib/hooks/useSetPageLabels'
|
||||
import { Button } from '../../elements/Button'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type AddBulkLabelsModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
|
@ -24,12 +26,14 @@ type AddBulkLabelsModalProps = {
|
|||
export function AddBulkLabelsModal(
|
||||
props: AddBulkLabelsModalProps
|
||||
): JSX.Element {
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] =
|
||||
useState<string | undefined>(undefined)
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
|
@ -97,10 +101,11 @@ export function AddBulkLabelsModal(
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -132,7 +137,7 @@ export function AddBulkLabelsModal(
|
|||
const trimmedValue = value.trim()
|
||||
const current = selectedLabels.labels ?? []
|
||||
const lowerCasedValue = trimmedValue.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Separator } from '@radix-ui/react-separator'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { Box, SpanBox } from '../../elements/LayoutPrimitives'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -11,6 +11,7 @@ import { TrashIcon } from '../../elements/icons/TrashIcon'
|
|||
import { LabelIcon } from '../../elements/icons/LabelIcon'
|
||||
import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
|
||||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ export function ArticleActionsMenu(
|
|||
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
{props.article?.state !== State.ARCHIVED ? (
|
||||
<Button
|
||||
title="Archive (e)"
|
||||
style="articleActionIcon"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
import {
|
||||
ArticleAttributes,
|
||||
TextDirection,
|
||||
} from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Article } from './../../../components/templates/article/Article'
|
||||
import { Box, HStack, SpanBox, VStack } from './../../elements/LayoutPrimitives'
|
||||
import { StyledText } from './../../elements/StyledText'
|
||||
|
|
@ -19,11 +15,14 @@ import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater'
|
|||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { Recommendation } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
Recommendation,
|
||||
TextDirection,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Avatar } from '../../elements/Avatar'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { AISummary } from './AISummary'
|
||||
import { userHasFeature } from '../../../lib/featureFlag'
|
||||
|
||||
type ArticleContainerProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -117,23 +116,28 @@ const RecommendationComments = (
|
|||
|
||||
export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
||||
const [labels, setLabels] = useState(props.labels)
|
||||
const [title, setTitle] = useState(props.article.title)
|
||||
const [title, setTitle] = useState<string | undefined>(undefined)
|
||||
const [showReportIssuesModal, setShowReportIssuesModal] = useState(false)
|
||||
const [fontSize, setFontSize] = useState(props.fontSize ?? 20)
|
||||
const [highlightOnRelease, setHighlightOnRelease] = useState(
|
||||
props.highlightOnRelease
|
||||
)
|
||||
// iOS app embed can overide the original margin and line height
|
||||
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] =
|
||||
useState<number | null>(null)
|
||||
const [lineHeightOverride, setLineHeightOverride] =
|
||||
useState<number | null>(null)
|
||||
const [fontFamilyOverride, setFontFamilyOverride] =
|
||||
useState<string | null>(null)
|
||||
const [highContrastTextOverride, setHighContrastTextOverride] =
|
||||
useState<boolean | undefined>(undefined)
|
||||
const [justifyTextOverride, setJustifyTextOverride] =
|
||||
useState<boolean | undefined>(undefined)
|
||||
const [maxWidthPercentageOverride, setMaxWidthPercentageOverride] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
const [lineHeightOverride, setLineHeightOverride] = useState<number | null>(
|
||||
null
|
||||
)
|
||||
const [fontFamilyOverride, setFontFamilyOverride] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [highContrastTextOverride, setHighContrastTextOverride] = useState<
|
||||
boolean | undefined
|
||||
>(undefined)
|
||||
const [justifyTextOverride, setJustifyTextOverride] = useState<
|
||||
boolean | undefined
|
||||
>(undefined)
|
||||
const highlightHref = useRef(
|
||||
window.location.hash ? window.location.hash.split('#')[1] : null
|
||||
)
|
||||
|
|
@ -444,9 +448,9 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
'-webkit-line-clamp': '6',
|
||||
},
|
||||
}}
|
||||
title={title}
|
||||
title={title ?? props.article.title}
|
||||
>
|
||||
{title}
|
||||
{title ?? props.article.title}
|
||||
</StyledText>
|
||||
<ArticleSubtitle
|
||||
author={props.article.author}
|
||||
|
|
@ -520,9 +524,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
|
|||
item={props.article}
|
||||
scrollToHighlight={highlightHref}
|
||||
highlights={props.article.highlights}
|
||||
articleTitle={title}
|
||||
articleAuthor={props.article.author ?? ''}
|
||||
articleId={props.article.id}
|
||||
isAppleAppEmbed={props.isAppleAppEmbed}
|
||||
highlightBarDisabled={props.highlightBarDisabled}
|
||||
showHighlightsModal={props.showHighlightsModal}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Box, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
|
@ -11,10 +11,6 @@ import {
|
|||
import PSPDFKit from 'pspdfkit'
|
||||
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { useCanShareNative } from '../../../lib/hooks/useCanShareNative'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
import { NotebookModal } from './NotebookModal'
|
||||
|
|
@ -42,13 +38,15 @@ type EpubPatch = {
|
|||
export default function EpubContainer(props: EpubContainerProps): JSX.Element {
|
||||
const epubRef = useRef<HTMLDivElement | null>(null)
|
||||
const renditionRef = useRef<Rendition | undefined>(undefined)
|
||||
const [shareTarget, setShareTarget] =
|
||||
useState<Highlight | undefined>(undefined)
|
||||
const [shareTarget, setShareTarget] = useState<Highlight | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [touchStart, setTouchStart] = useState(0)
|
||||
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
|
||||
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] =
|
||||
useState<number | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] = useState<
|
||||
number | undefined
|
||||
>(undefined)
|
||||
const highlightsRef = useRef<Highlight[]>([])
|
||||
|
||||
const book = useMemo(() => {
|
||||
|
|
@ -309,56 +307,6 @@ export default function EpubContainer(props: EpubContainerProps): JSX.Element {
|
|||
{/* EPUB CONTAINER
|
||||
<div ></div> */}
|
||||
</Box>
|
||||
{noteTarget && (
|
||||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
const savedHighlight = highlightsRef.current.find(
|
||||
(other: Highlight) => {
|
||||
return other.id == highlight.id
|
||||
}
|
||||
)
|
||||
|
||||
if (savedHighlight) {
|
||||
savedHighlight.annotation = highlight.annotation
|
||||
}
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
setNoteTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.showHighlightsModal && (
|
||||
<NotebookModal
|
||||
key={notebookKey}
|
||||
viewer={props.viewer}
|
||||
item={props.article}
|
||||
onClose={(updatedHighlights, deletedAnnotations) => {
|
||||
console.log(
|
||||
'closed PDF notebook: ',
|
||||
updatedHighlights,
|
||||
deletedAnnotations
|
||||
)
|
||||
deletedAnnotations.forEach((highlight) => {
|
||||
const event = new CustomEvent('deleteHighlightbyId', {
|
||||
detail: highlight.id,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
})
|
||||
props.setShowHighlightsModal(false)
|
||||
}}
|
||||
viewHighlightInReader={(highlightId) => {
|
||||
const event = new CustomEvent('scrollToHighlightId', {
|
||||
detail: highlightId,
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
props.setShowHighlightsModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,13 @@ import { VStack } from '../../elements/LayoutPrimitives'
|
|||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { StyledTextArea } from '../../elements/StyledTextArea'
|
||||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { useUpdateHighlight } from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
type HighlightNoteModalProps = {
|
||||
author: string
|
||||
title: string
|
||||
highlight?: Highlight
|
||||
libraryItemId: string
|
||||
libraryItemSlug: string
|
||||
onUpdate: (updatedHighlight: Highlight) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
createHighlightForNote?: (note?: string) => Promise<Highlight | undefined>
|
||||
|
|
@ -25,6 +24,7 @@ type HighlightNoteModalProps = {
|
|||
export function HighlightNoteModal(
|
||||
props: HighlightNoteModalProps
|
||||
): JSX.Element {
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const [noteContent, setNoteContent] = useState(
|
||||
props.highlight?.annotation ?? ''
|
||||
)
|
||||
|
|
@ -38,20 +38,25 @@ export function HighlightNoteModal(
|
|||
|
||||
const saveNoteChanges = useCallback(async () => {
|
||||
if (noteContent != props.highlight?.annotation && props.highlight?.id) {
|
||||
const result = await updateHighlightMutation({
|
||||
libraryItemId: props.libraryItemId,
|
||||
highlightId: props.highlight?.id,
|
||||
annotation: noteContent,
|
||||
color: props.highlight?.color,
|
||||
})
|
||||
|
||||
if (result) {
|
||||
console.log('updating highlight textsdsdfsd')
|
||||
try {
|
||||
const result = await updateHighlight.mutateAsync({
|
||||
itemId: props.libraryItemId,
|
||||
slug: props.libraryItemSlug,
|
||||
input: {
|
||||
libraryItemId: props.libraryItemId,
|
||||
highlightId: props.highlight?.id,
|
||||
annotation: noteContent,
|
||||
color: props.highlight?.color,
|
||||
},
|
||||
})
|
||||
props.onUpdate({ ...props.highlight, annotation: noteContent })
|
||||
props.onOpenChange(false)
|
||||
} else {
|
||||
return result?.id
|
||||
} catch (err) {
|
||||
showErrorToast('Error updating your note', { position: 'bottom-right' })
|
||||
return undefined
|
||||
}
|
||||
document.dispatchEvent(new Event('highlightsUpdated'))
|
||||
}
|
||||
if (!props.highlight && props.createHighlightForNote) {
|
||||
const result = await props.createHighlightForNote(noteContent)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { HighlightView } from '../../patterns/HighlightView'
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
|||
import { ArticleMutations } from '../../../lib/articleActions'
|
||||
import { isTouchScreenDevice } from '../../../lib/deviceType'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
|
||||
import 'react-sliding-pane/dist/react-sliding-pane.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
|
|
@ -39,9 +39,6 @@ type HighlightsLayerProps = {
|
|||
item: ReadableItem
|
||||
highlights: Highlight[]
|
||||
|
||||
articleId: string
|
||||
articleTitle: string
|
||||
articleAuthor: string
|
||||
isAppleAppEmbed: boolean
|
||||
highlightBarDisabled: boolean
|
||||
showHighlightsModal: boolean
|
||||
|
|
@ -105,7 +102,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const result = await createHighlight(
|
||||
{
|
||||
selection: selection,
|
||||
articleId: props.articleId,
|
||||
articleId: props.item.id,
|
||||
existingHighlights: highlights,
|
||||
color: options?.color,
|
||||
highlightStartEndOffsets: highlightLocations,
|
||||
|
|
@ -141,7 +138,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
[
|
||||
highlightLocations,
|
||||
highlights,
|
||||
props.articleId,
|
||||
props.item.id,
|
||||
props.articleMutations,
|
||||
setSelectionData,
|
||||
]
|
||||
|
|
@ -189,7 +186,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
|
||||
const didDeleteHighlight =
|
||||
await props.articleMutations.deleteHighlightMutation(
|
||||
props.articleId,
|
||||
props.item.id,
|
||||
highlightId
|
||||
)
|
||||
|
||||
|
|
@ -226,7 +223,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
updateHighlightsCallback(highlight)
|
||||
;(async () => {
|
||||
const update = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
libraryItemId: props.item.id,
|
||||
highlightId: highlight.id,
|
||||
color: color,
|
||||
})
|
||||
|
|
@ -718,7 +715,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
const annotation = event.annotation ?? ''
|
||||
|
||||
const result = await props.articleMutations.updateHighlightMutation({
|
||||
libraryItemId: props.articleId,
|
||||
libraryItemId: props.item.id,
|
||||
highlightId: focusedHighlight.id,
|
||||
annotation: event.annotation ?? '',
|
||||
})
|
||||
|
|
@ -800,9 +797,8 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
|||
{highlightModalAction?.highlightModalAction == 'addComment' && (
|
||||
<HighlightNoteModal
|
||||
highlight={highlightModalAction.highlight}
|
||||
author={props.articleAuthor}
|
||||
title={props.articleTitle}
|
||||
libraryItemId={props.articleId}
|
||||
libraryItemId={props.item.id}
|
||||
libraryItemSlug={props.item.slug}
|
||||
onUpdate={updateHighlightsCallback}
|
||||
onOpenChange={() =>
|
||||
setHighlightModalAction({ highlightModalAction: 'none' })
|
||||
|
|
|
|||
|
|
@ -5,21 +5,24 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { updateHighlightMutation } from '../../../lib/networking/mutations/updateHighlightMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { HighlightViewItem } from './HighlightViewItem'
|
||||
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
|
||||
import { TrashIcon } from '../../elements/icons/TrashIcon'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { SetHighlightLabelsModalPresenter } from './SetLabelsModalPresenter'
|
||||
import { ArticleNotes } from '../../patterns/ArticleNotes'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { formattedShortTime } from '../../../lib/dateFormatting'
|
||||
import { isDarkTheme } from '../../../lib/themeUpdater'
|
||||
import { sortHighlights } from '../../../lib/highlights/sortHighlights'
|
||||
import { useGetLibraryItemContent } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import {
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
|
||||
type NotebookContentProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -42,12 +45,14 @@ type NoteState = {
|
|||
|
||||
export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
||||
const isDark = isDarkTheme()
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
|
||||
const { articleData, mutate } = useGetArticleQuery({
|
||||
slug: props.item.slug,
|
||||
username: props.viewer.profile.username,
|
||||
includeFriendsHighlights: false,
|
||||
})
|
||||
const { data: article } = useGetLibraryItemContent(
|
||||
props.viewer.profile.username as string,
|
||||
props.item.slug as string
|
||||
)
|
||||
const [noteText, setNoteText] = useState<string>('')
|
||||
const [showConfirmDeleteHighlightId, setShowConfirmDeleteHighlightId] =
|
||||
useState<undefined | string>(undefined)
|
||||
|
|
@ -88,12 +93,16 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
noteState.current.createStarted = new Date()
|
||||
;(async () => {
|
||||
try {
|
||||
const success = await createHighlightMutation({
|
||||
id: newNoteId,
|
||||
shortId: nanoid(8),
|
||||
type: 'NOTE',
|
||||
articleId: props.item.id,
|
||||
annotation: text,
|
||||
const success = await createHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
input: {
|
||||
id: newNoteId,
|
||||
shortId: nanoid(8),
|
||||
type: 'NOTE',
|
||||
articleId: props.item.id,
|
||||
annotation: text,
|
||||
},
|
||||
})
|
||||
if (success) {
|
||||
noteState.current.note = success
|
||||
|
|
@ -112,7 +121,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
)
|
||||
|
||||
const highlights = useMemo(() => {
|
||||
const result = articleData?.article.article.highlights
|
||||
const result = article?.highlights
|
||||
const note = result?.find((h) => h.type === 'NOTE')
|
||||
if (note) {
|
||||
noteState.current.note = note
|
||||
|
|
@ -122,7 +131,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
setNoteText('')
|
||||
}
|
||||
return result
|
||||
}, [articleData])
|
||||
}, [article])
|
||||
|
||||
useEffect(() => {
|
||||
if (highlights && props.onAnnotationsChanged) {
|
||||
|
|
@ -165,7 +174,11 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
highlights
|
||||
?.filter((h) => h.type === 'NOTE')
|
||||
.forEach(async (h) => {
|
||||
const result = await deleteHighlightMutation(props.item.id, h.id)
|
||||
const result = await deleteHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
highlightId: h.id,
|
||||
})
|
||||
if (!result) {
|
||||
showErrorToast('Error deleting note')
|
||||
}
|
||||
|
|
@ -179,16 +192,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
const [lastChanged, setLastChanged] = useState<Date | undefined>(undefined)
|
||||
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const highlightsUpdated = () => {
|
||||
mutate()
|
||||
}
|
||||
document.addEventListener('highlightsUpdated', highlightsUpdated)
|
||||
return () => {
|
||||
document.removeEventListener('highlightsUpdated', highlightsUpdated)
|
||||
}
|
||||
}, [mutate])
|
||||
|
||||
return (
|
||||
<VStack
|
||||
tabIndex={-1}
|
||||
|
|
@ -257,7 +260,8 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
setSetLabelsTarget={setLabelsTarget}
|
||||
setShowConfirmDeleteHighlightId={setShowConfirmDeleteHighlightId}
|
||||
updateHighlight={() => {
|
||||
mutate()
|
||||
// nothing should be needed here anymore with new caching
|
||||
console.log('update highlight')
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -294,11 +298,11 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
onAccept={() => {
|
||||
;(async () => {
|
||||
const highlightId = showConfirmDeleteHighlightId
|
||||
const success = await deleteHighlightMutation(
|
||||
props.item.id,
|
||||
showConfirmDeleteHighlightId
|
||||
)
|
||||
mutate()
|
||||
const success = await deleteHighlight.mutateAsync({
|
||||
itemId: props.item.id,
|
||||
slug: props.item.slug,
|
||||
highlightId: showConfirmDeleteHighlightId,
|
||||
})
|
||||
if (success) {
|
||||
showSuccessToast('Highlight deleted.', {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -333,7 +337,6 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
|
|||
console.log('update highlight: ', highlight)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
mutate()
|
||||
setLabelsTarget(undefined)
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from '../../elements/Button'
|
|||
import { ExportIcon } from '../../elements/icons/ExportIcon'
|
||||
import { useCallback } from 'react'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { useGetArticleQuery } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { highlightsAsMarkdown } from '../homeFeed/HighlightItem'
|
|||
import 'react-markdown-editor-lite/lib/index.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
|
||||
type NotebookModalProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ReadableItem } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { ReadableItem } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { UserBasicData } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import 'react-sliding-pane/dist/react-sliding-pane.css'
|
||||
import { NotebookContent } from './Notebook'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import {
|
||||
ArticleAttributes,
|
||||
useUpdateItemReadStatus,
|
||||
} from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Box } from '../../elements/LayoutPrimitives'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
|
@ -7,10 +10,6 @@ import { isDarkTheme } from '../../../lib/themeUpdater'
|
|||
import PSPDFKit from 'pspdfkit'
|
||||
import { Instance, HighlightAnnotation, List, Annotation, Rect } from 'pspdfkit'
|
||||
import type { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { createHighlightMutation } from '../../../lib/networking/mutations/createHighlightMutation'
|
||||
import { deleteHighlightMutation } from '../../../lib/networking/mutations/deleteHighlightMutation'
|
||||
import { articleReadingProgressMutation } from '../../../lib/networking/mutations/articleReadingProgressMutation'
|
||||
import { mergeHighlightMutation } from '../../../lib/networking/mutations/mergeHighlightMutation'
|
||||
import { pspdfKitKey } from '../../../lib/appConfig'
|
||||
import { HighlightNoteModal } from './HighlightNoteModal'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
|
|
@ -22,6 +21,13 @@ import { NotebookHeader } from './NotebookHeader'
|
|||
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
|
||||
import { ResizableSidebar } from './ResizableSidebar'
|
||||
import { DEFAULT_HOME_PATH } from '../../../lib/navigations'
|
||||
import {
|
||||
useCreateHighlight,
|
||||
useDeleteHighlight,
|
||||
useMergeHighlight,
|
||||
useUpdateHighlight,
|
||||
} from '../../../lib/networking/highlights/useItemHighlights'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
export type PdfArticleContainerProps = {
|
||||
viewer: UserBasicData
|
||||
|
|
@ -33,12 +39,18 @@ export type PdfArticleContainerProps = {
|
|||
export default function PdfArticleContainer(
|
||||
props: PdfArticleContainerProps
|
||||
): JSX.Element {
|
||||
const router = useRouter()
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [notebookKey, setNotebookKey] = useState<string>(uuidv4())
|
||||
const [noteTarget, setNoteTarget] = useState<Highlight | undefined>(undefined)
|
||||
const [noteTargetPageIndex, setNoteTargetPageIndex] =
|
||||
useState<number | undefined>(undefined)
|
||||
const highlightsRef = useRef<Highlight[]>([])
|
||||
const createHighlight = useCreateHighlight()
|
||||
const deleteHighlight = useDeleteHighlight()
|
||||
const mergeHighlight = useMergeHighlight()
|
||||
const updateHighlight = useUpdateHighlight()
|
||||
const updateItemReadStatus = useUpdateItemReadStatus()
|
||||
|
||||
const annotationOmnivoreId = (annotation: Annotation): string | undefined => {
|
||||
if (
|
||||
|
|
@ -113,7 +125,11 @@ export default function PdfArticleContainer(
|
|||
.delete(annotation)
|
||||
.then(() => {
|
||||
if (annotationId) {
|
||||
return deleteHighlightMutation(props.article.id, annotationId)
|
||||
return deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
|
|
@ -214,8 +230,6 @@ export default function PdfArticleContainer(
|
|||
}),
|
||||
}
|
||||
|
||||
console.log('instnace config: ', config)
|
||||
|
||||
instance = await PSPDFKit.load(config)
|
||||
console.log('created PDF instance', instance)
|
||||
|
||||
|
|
@ -229,7 +243,11 @@ export default function PdfArticleContainer(
|
|||
}
|
||||
const annotationId = annotationOmnivoreId(annotation)
|
||||
if (annotationId) {
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
await deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -339,16 +357,21 @@ export default function PdfArticleContainer(
|
|||
|
||||
if (overlapping.size === 0) {
|
||||
const positionPercent = positionPercentForAnnotation(annotation)
|
||||
const result = await createHighlightMutation({
|
||||
id: id,
|
||||
shortId: shortId,
|
||||
quote: quote,
|
||||
articleId: props.article.id,
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
patch: JSON.stringify(serialized),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
|
||||
const result = await createHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
id: id,
|
||||
shortId: shortId,
|
||||
quote: quote,
|
||||
articleId: props.article.id,
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
patch: JSON.stringify(serialized),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
},
|
||||
})
|
||||
if (result) {
|
||||
highlightsRef.current.push(result)
|
||||
|
|
@ -384,20 +407,24 @@ export default function PdfArticleContainer(
|
|||
(ha) => (ha.customData?.omnivoreHighlight as Highlight).id
|
||||
)
|
||||
const positionPercent = positionPercentForAnnotation(annotation)
|
||||
const result = await mergeHighlightMutation({
|
||||
quote,
|
||||
id,
|
||||
shortId,
|
||||
patch: JSON.stringify(serialized),
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
articleId: props.article.id,
|
||||
overlapHighlightIdList: mergedIds.toArray(),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
const result = await mergeHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
quote,
|
||||
id,
|
||||
shortId,
|
||||
patch: JSON.stringify(serialized),
|
||||
prefix: surroundingText.prefix,
|
||||
suffix: surroundingText.suffix,
|
||||
articleId: props.article.id,
|
||||
overlapHighlightIdList: mergedIds.toArray(),
|
||||
highlightPositionPercent: positionPercent * 100,
|
||||
highlightPositionAnchorIndex: annotation.pageIndex,
|
||||
},
|
||||
})
|
||||
if (result) {
|
||||
highlightsRef.current.push(result)
|
||||
if (result && result.highlight) {
|
||||
highlightsRef.current.push(result.highlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -410,11 +437,15 @@ export default function PdfArticleContainer(
|
|||
100,
|
||||
Math.max(0, ((pageIndex + 1) / instance.totalPageCount) * 100)
|
||||
)
|
||||
await articleReadingProgressMutation({
|
||||
id: props.article.id,
|
||||
force: true,
|
||||
readingProgressPercent: percent,
|
||||
readingProgressAnchorIndex: pageIndex,
|
||||
await updateItemReadStatus.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
input: {
|
||||
id: props.article.id,
|
||||
force: true,
|
||||
readingProgressPercent: percent,
|
||||
readingProgressAnchorIndex: pageIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
@ -451,14 +482,14 @@ export default function PdfArticleContainer(
|
|||
case 'u':
|
||||
const navReturn = window.localStorage.getItem('nav-return')
|
||||
if (navReturn) {
|
||||
window.location.assign(navReturn)
|
||||
router.push(navReturn)
|
||||
return
|
||||
}
|
||||
const query = window.sessionStorage.getItem('q')
|
||||
if (query) {
|
||||
window.location.assign(`${DEFAULT_HOME_PATH}?${query}`)
|
||||
router.push(`${DEFAULT_HOME_PATH}?${query}`)
|
||||
} else {
|
||||
window.location.replace(DEFAULT_HOME_PATH)
|
||||
router.push(DEFAULT_HOME_PATH)
|
||||
}
|
||||
break
|
||||
case 'e':
|
||||
|
|
@ -517,7 +548,11 @@ export default function PdfArticleContainer(
|
|||
const storedId = annotationOmnivoreId(annotation)
|
||||
if (storedId == annotationId) {
|
||||
await instance.delete(annotation)
|
||||
await deleteHighlightMutation(props.article.id, annotationId)
|
||||
await deleteHighlight.mutateAsync({
|
||||
itemId: props.article.id,
|
||||
slug: props.article.slug,
|
||||
highlightId: annotationId,
|
||||
})
|
||||
|
||||
const highlightIdx = highlightsRef.current.findIndex((value) => {
|
||||
return value.id == annotationId
|
||||
|
|
@ -582,8 +617,7 @@ export default function PdfArticleContainer(
|
|||
<HighlightNoteModal
|
||||
highlight={noteTarget}
|
||||
libraryItemId={props.article.id}
|
||||
author={props.article.author ?? ''}
|
||||
title={props.article.title}
|
||||
libraryItemSlug={props.article.slug}
|
||||
onUpdate={(highlight: Highlight) => {
|
||||
const savedHighlight = highlightsRef.current.find(
|
||||
(other: Highlight) => {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@ import { Button } from '../../elements/Button'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Check, Circle, Plus, WarningCircle } from '@phosphor-icons/react'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LabelsPicker } from '../../elements/LabelsPicker'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
export interface LabelsProvider {
|
||||
labels?: Label[]
|
||||
|
|
@ -282,10 +284,10 @@ function Footer(props: FooterProps): JSX.Element {
|
|||
}
|
||||
|
||||
export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { inputValue, setInputValue, selectedLabels, setHighlightLastLabel } =
|
||||
props
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const { data: labels } = useGetLabels()
|
||||
const createLabel = useCreateLabel()
|
||||
// Move focus through the labels list on tab or arrow up/down keys
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | undefined>(0)
|
||||
|
||||
|
|
@ -321,9 +323,8 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
props.dispatchLabels({ type: 'SAVE', labels: newSelectedLabels })
|
||||
|
||||
props.clearInputState()
|
||||
revalidate()
|
||||
},
|
||||
[isSelected, props, revalidate]
|
||||
[isSelected, props]
|
||||
)
|
||||
|
||||
const filteredLabels = useMemo(() => {
|
||||
|
|
@ -342,11 +343,11 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
const createLabelFromFilterText = useCallback(
|
||||
async (text: string) => {
|
||||
const trimmedLabelName = text.trim()
|
||||
const label = await createLabelMutation(
|
||||
trimmedLabelName,
|
||||
randomLabelColorHex(),
|
||||
''
|
||||
)
|
||||
const label = await createLabel.mutateAsync({
|
||||
name: trimmedLabelName,
|
||||
color: randomLabelColorHex(),
|
||||
description: undefined,
|
||||
})
|
||||
if (label) {
|
||||
showSuccessToast(`Created label ${label.name}`, {
|
||||
position: 'bottom-right',
|
||||
|
|
@ -425,7 +426,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
}, [inputValue, setInputValue, createLabelFromFilterText])
|
||||
|
||||
const selectEnteredLabel = useCallback(() => {
|
||||
const label = labels.find(
|
||||
const label = labels?.find(
|
||||
(l: Label) => l.name.toLowerCase() == inputValue.toLowerCase()
|
||||
)
|
||||
if (!label) {
|
||||
|
|
@ -509,7 +510,7 @@ export function SetLabelsControl(props: SetLabelsControlProps): JSX.Element {
|
|||
<Footer
|
||||
filterText={inputValue}
|
||||
selectedLabels={props.selectedLabels}
|
||||
availableLabels={labels}
|
||||
availableLabels={labels ?? []}
|
||||
focused={focusedIndex === filteredLabels.length + 1}
|
||||
createEnteredLabel={createEnteredLabel}
|
||||
selectEnteredLabel={selectEnteredLabel}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,15 @@ import {
|
|||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { LabelsProvider, SetLabelsControl } from './SetLabelsControl'
|
||||
import { createLabelMutation } from '../../../lib/networking/mutations/createLabelMutation'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { randomLabelColorHex } from '../../../utils/settings-page/labels/labelColorObjects'
|
||||
import { LabelsDispatcher } from '../../../lib/hooks/useSetPageLabels'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import {
|
||||
useCreateLabel,
|
||||
useGetLabels,
|
||||
} from '../../../lib/networking/labels/useLabels'
|
||||
|
||||
type SetLabelsModalProps = {
|
||||
provider: LabelsProvider
|
||||
|
|
@ -28,7 +30,7 @@ type SetLabelsModalProps = {
|
|||
export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const { selectedLabels, dispatchLabels } = props
|
||||
const availableLabels = useGetLabelsQuery()
|
||||
const { data: availableLabels } = useGetLabels()
|
||||
const [tabCount, setTabCount] = useState(-1)
|
||||
const [tabStartValue, setTabStartValue] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
|
|
@ -37,6 +39,8 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
const errorTimeoutRef = useRef<NodeJS.Timeout | undefined>()
|
||||
const [highlightLastLabel, setHighlightLastLabel] = useState(false)
|
||||
|
||||
const createLabel = useCreateLabel()
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: string, timeout?: number) => {
|
||||
if (errorTimeoutRef.current) {
|
||||
|
|
@ -82,10 +86,11 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(newLabels: Label[], tempLabel: Label) => {
|
||||
;(async () => {
|
||||
const currentLabels = newLabels
|
||||
const newLabel = await createLabelMutation(
|
||||
tempLabel.name,
|
||||
tempLabel.color
|
||||
)
|
||||
const newLabel = await createLabel.mutateAsync({
|
||||
name: tempLabel.name,
|
||||
color: tempLabel.color,
|
||||
description: undefined,
|
||||
})
|
||||
const idx = currentLabels.findIndex((l) => l.id === tempLabel.id)
|
||||
if (newLabel) {
|
||||
showSuccessToast(`Created label ${newLabel.name}`, {
|
||||
|
|
@ -116,7 +121,7 @@ export function SetLabelsModal(props: SetLabelsModalProps): JSX.Element {
|
|||
(value: string) => {
|
||||
const current = selectedLabels ?? []
|
||||
const lowerCasedValue = value.toLowerCase()
|
||||
const existing = availableLabels.labels.find(
|
||||
const existing = availableLabels?.find(
|
||||
(l) => l.name.toLowerCase() == lowerCasedValue
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@ import { LabelsProvider } from './SetLabelsControl'
|
|||
import { SetLabelsModal } from './SetLabelsModal'
|
||||
import { useSetHighlightLabels } from '../../../lib/hooks/useSetHighlightLabels'
|
||||
import { Highlight } from '../../../lib/networking/fragments/highlightFragment'
|
||||
import { LibraryItemNode } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
|
||||
type SetPageLabelsModalPresenterProps = {
|
||||
articleId: string
|
||||
article: LabelsProvider
|
||||
libraryItem: LibraryItemNode
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function SetPageLabelsModalPresenter(
|
||||
props: SetPageLabelsModalPresenterProps
|
||||
): JSX.Element {
|
||||
const [labels, dispatchLabels] = useSetPageLabels(props.articleId)
|
||||
const [labels, dispatchLabels] = useSetPageLabels(
|
||||
props.libraryItem.id,
|
||||
props.libraryItem.slug
|
||||
)
|
||||
|
||||
const onOpenChange = useCallback(() => {
|
||||
if (props.article) {
|
||||
props.article.labels = labels.labels
|
||||
if (props.libraryItem) {
|
||||
props.libraryItem.labels = labels.labels
|
||||
}
|
||||
props.onOpenChange(true)
|
||||
}, [props, labels])
|
||||
|
|
@ -26,13 +29,13 @@ export function SetPageLabelsModalPresenter(
|
|||
useEffect(() => {
|
||||
dispatchLabels({
|
||||
type: 'RESET',
|
||||
labels: props.article.labels ?? [],
|
||||
labels: props.libraryItem.labels ?? [],
|
||||
})
|
||||
}, [props.article, dispatchLabels])
|
||||
}, [props.libraryItem, dispatchLabels])
|
||||
|
||||
return (
|
||||
<SetLabelsModal
|
||||
provider={props.article}
|
||||
provider={props.libraryItem}
|
||||
selectedLabels={labels.labels}
|
||||
dispatchLabels={dispatchLabels}
|
||||
onOpenChange={onOpenChange}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { ArticleAttributes } from '../../../lib/networking/library_items/useLibraryItems'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { HStack } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
|
|
@ -11,6 +11,7 @@ import { EditInfoIcon } from '../../elements/icons/EditInfoIcon'
|
|||
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
|
||||
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
|
||||
import { UnarchiveIcon } from '../../elements/icons/UnarchiveIcon'
|
||||
import { State } from '../../../lib/networking/fragments/articleFragment'
|
||||
|
||||
export type ArticleActionsMenuLayout = 'top' | 'side'
|
||||
|
||||
|
|
@ -94,15 +95,12 @@ export function VerticalArticleActionsMenu(
|
|||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TrashIcon size={24} color={theme.colors.thHighContrast.toString()} />
|
||||
</Button>
|
||||
|
||||
{!props.article?.isArchived ? (
|
||||
{props.article?.state !== State.ARCHIVED ? (
|
||||
<Button
|
||||
title="Archive (e)"
|
||||
style="articleActionIcon"
|
||||
|
|
@ -155,6 +153,7 @@ export function VerticalArticleActionsMenu(
|
|||
</Button>
|
||||
|
||||
<ReaderDropdownMenu
|
||||
libraryItem={props.article}
|
||||
triggerElement={
|
||||
<CircleUtilityMenuIcon
|
||||
size={24}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue