Showing preview only (9,242K chars total). Download the full file or copy to clipboard to get everything.
Repository: RSSNext/Folo
Branch: dev
Commit: ecf1ca57ac21
Files: 2869
Total size: 8.2 MB
Directory structure:
gitextract_ni4pl4rv/
├── .agents/
│ ├── settings.local.json
│ └── skills/
│ ├── desktop-release/
│ │ └── SKILL.md
│ ├── installing-mobile-preview-builds/
│ │ └── SKILL.md
│ ├── mobile-e2e/
│ │ └── SKILL.md
│ ├── mobile-release/
│ │ └── SKILL.md
│ ├── mobile-self-test/
│ │ └── SKILL.md
│ └── update-deps/
│ └── SKILL.md
├── .cursorignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ ├── i18n.yml
│ │ └── typo.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── actions/
│ │ ├── setup-version/
│ │ │ └── action.yml
│ │ └── setup-xcode/
│ │ └── action.yml
│ ├── advanced-issue-labeler.yml
│ ├── copilot-instructions.md
│ ├── dependabot.yaml
│ ├── prompts/
│ │ └── similar_issues.prompt.yml
│ ├── scripts/
│ │ └── extract-release-info.mjs
│ └── workflows/
│ ├── build-android.yml
│ ├── build-desktop.yml
│ ├── build-ios-development.yml
│ ├── build-ios.yml
│ ├── build-web.yml
│ ├── deploy-cloudflare-desktop.yml
│ ├── deploy-cloudflare-landing.yml
│ ├── deploy-cloudflare-ssr.yml
│ ├── issue-labeler.yml
│ ├── lint.yml
│ ├── pr-title-check.yml
│ ├── similar-issues.yml
│ ├── sync.yaml
│ ├── tag.yml
│ └── translator.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.mjs
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── AGENTS.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── api/
│ └── vercel_webhook.ts
├── apps/
│ ├── cli/
│ │ ├── package.json
│ │ ├── skill.md
│ │ ├── src/
│ │ │ ├── args.test.ts
│ │ │ ├── args.ts
│ │ │ ├── browser-login.test.ts
│ │ │ ├── browser-login.ts
│ │ │ ├── cli.e2e.test.ts
│ │ │ ├── client.ts
│ │ │ ├── command.ts
│ │ │ ├── commands/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── collection.ts
│ │ │ │ ├── entry.ts
│ │ │ │ ├── feed.ts
│ │ │ │ ├── list.ts
│ │ │ │ ├── opml.ts
│ │ │ │ ├── search.ts
│ │ │ │ ├── subscription.ts
│ │ │ │ ├── timeline.ts
│ │ │ │ └── unread.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ ├── output.test.ts
│ │ │ └── output.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── desktop/
│ │ ├── .env.example
│ │ ├── AGENTS.md
│ │ ├── build/
│ │ │ ├── appxmanifest-template.xml
│ │ │ ├── dev.pfx
│ │ │ ├── entitlements.mac.plist
│ │ │ ├── entitlements.mas.child.plist
│ │ │ └── entitlements.mas.plist
│ │ ├── bump.config.ts
│ │ ├── bump.hotfix.config.js
│ │ ├── changelog/
│ │ │ ├── 0.1.2.md
│ │ │ ├── 0.2.0.md
│ │ │ ├── 0.2.1.md
│ │ │ ├── 0.2.2.md
│ │ │ ├── 0.2.3.md
│ │ │ ├── 0.2.4.md
│ │ │ ├── 0.2.5.md
│ │ │ ├── 0.2.6.md
│ │ │ ├── 0.2.7.md
│ │ │ ├── 0.2.8.md
│ │ │ ├── 0.2.9.md
│ │ │ ├── 0.3.0.md
│ │ │ ├── 0.3.1.md
│ │ │ ├── 0.3.10.md
│ │ │ ├── 0.3.11.md
│ │ │ ├── 0.3.12.md
│ │ │ ├── 0.3.13.md
│ │ │ ├── 0.3.2.md
│ │ │ ├── 0.3.3.md
│ │ │ ├── 0.3.4.md
│ │ │ ├── 0.3.5.md
│ │ │ ├── 0.3.6.md
│ │ │ ├── 0.3.7.md
│ │ │ ├── 0.3.8.md
│ │ │ ├── 0.3.9.md
│ │ │ ├── 0.4.0.md
│ │ │ ├── 0.4.1.md
│ │ │ ├── 0.4.2.md
│ │ │ ├── 0.4.3.md
│ │ │ ├── 0.4.4.md
│ │ │ ├── 0.4.5.md
│ │ │ ├── 0.4.6.md
│ │ │ ├── 0.4.8.md
│ │ │ ├── 0.5.0.md
│ │ │ ├── 0.6.0.md
│ │ │ ├── 0.6.1.md
│ │ │ ├── 0.6.2.md
│ │ │ ├── 0.6.3.md
│ │ │ ├── 0.7.0.md
│ │ │ ├── 0.8.0.md
│ │ │ ├── 0.9.0.md
│ │ │ ├── 1.0.0.md
│ │ │ ├── 1.1.0.md
│ │ │ ├── 1.2.2.md
│ │ │ ├── 1.2.6.md
│ │ │ ├── 1.3.0.md
│ │ │ ├── 1.3.1.md
│ │ │ ├── 1.4.0.md
│ │ │ ├── next.md
│ │ │ └── next.template.md
│ │ ├── configs/
│ │ │ ├── vite.electron-render.config.ts
│ │ │ └── vite.render.config.ts
│ │ ├── dev-only/
│ │ │ └── dev-app-update.yml
│ │ ├── e2e/
│ │ │ ├── playwright.config.ts
│ │ │ ├── scripts/
│ │ │ │ └── capture-ui-audit.ts
│ │ │ ├── support/
│ │ │ │ ├── account.ts
│ │ │ │ ├── app.ts
│ │ │ │ ├── auth-bootstrap.ts
│ │ │ │ ├── electron.ts
│ │ │ │ └── env.ts
│ │ │ └── tests/
│ │ │ ├── electron/
│ │ │ │ └── core.spec.ts
│ │ │ └── web/
│ │ │ ├── core.spec.ts
│ │ │ └── settings-sync.spec.ts
│ │ ├── electron.vite.config.ts
│ │ ├── forge.config.cts
│ │ ├── layer/
│ │ │ ├── main/
│ │ │ │ ├── export.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── preload/
│ │ │ │ │ ├── index.d.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── src/
│ │ │ │ │ ├── @types/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── i18next.d.ts
│ │ │ │ │ │ └── resources.ts
│ │ │ │ │ ├── before-bootstrap.ts
│ │ │ │ │ ├── bootstrap.ts
│ │ │ │ │ ├── constants/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ └── system.ts
│ │ │ │ │ ├── env.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ipc/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── services/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ ├── auth.ts
│ │ │ │ │ │ ├── cli.ts
│ │ │ │ │ │ ├── debug.ts
│ │ │ │ │ │ ├── dock.ts
│ │ │ │ │ │ ├── integration.ts
│ │ │ │ │ │ ├── menu.ts
│ │ │ │ │ │ ├── reader.ts
│ │ │ │ │ │ └── setting.ts
│ │ │ │ │ ├── lib/
│ │ │ │ │ │ ├── api-client.ts
│ │ │ │ │ │ ├── auth-cookie-migration.ts
│ │ │ │ │ │ ├── cleaner.ts
│ │ │ │ │ │ ├── cli-session-sync.ts
│ │ │ │ │ │ ├── dock.ts
│ │ │ │ │ │ ├── download.ts
│ │ │ │ │ │ ├── i18n.ts
│ │ │ │ │ │ ├── proxy.test.ts
│ │ │ │ │ │ ├── proxy.ts
│ │ │ │ │ │ ├── router.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── tray.ts
│ │ │ │ │ │ ├── user.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── logger.ts
│ │ │ │ │ ├── manager/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ ├── bootstrap.ts
│ │ │ │ │ │ ├── lifecycle.ts
│ │ │ │ │ │ └── window.ts
│ │ │ │ │ ├── menu.ts
│ │ │ │ │ ├── modules/
│ │ │ │ │ │ └── language-detection/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── shims/
│ │ │ │ │ │ └── utf-8-validate.cjs
│ │ │ │ │ └── updater/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── configs.ts
│ │ │ │ │ ├── follow-update-provider.ts
│ │ │ │ │ ├── hot-updater.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── logger.ts
│ │ │ │ │ └── windows-updater.ts
│ │ │ │ ├── tsconfig.json
│ │ │ │ └── vitest.config.ts
│ │ │ └── renderer/
│ │ │ ├── debug_proxy.html
│ │ │ ├── global.d.ts
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── pwa-assets.config.ts
│ │ │ ├── setup-file.ts
│ │ │ ├── src/
│ │ │ │ ├── @types/
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── default-resource.electron.ts
│ │ │ │ │ ├── default-resource.ts
│ │ │ │ │ └── i18next.d.ts
│ │ │ │ ├── App.tsx
│ │ │ │ ├── atoms/
│ │ │ │ │ ├── ai-summary.ts
│ │ │ │ │ ├── ai-translation.ts
│ │ │ │ │ ├── app.ts
│ │ │ │ │ ├── context-menu.ts
│ │ │ │ │ ├── corner-player.ts
│ │ │ │ │ ├── debug-feature.ts
│ │ │ │ │ ├── dom.ts
│ │ │ │ │ ├── lang.ts
│ │ │ │ │ ├── network.ts
│ │ │ │ │ ├── player.ts
│ │ │ │ │ ├── popover.ts
│ │ │ │ │ ├── preview.ts
│ │ │ │ │ ├── readability.ts
│ │ │ │ │ ├── server-configs.ts
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── ai.ts
│ │ │ │ │ │ ├── general.ts
│ │ │ │ │ │ ├── integration.ts
│ │ │ │ │ │ └── ui.ts
│ │ │ │ │ ├── sidebar.ts
│ │ │ │ │ ├── source-content.tsx
│ │ │ │ │ ├── updater.ts
│ │ │ │ │ └── user.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── common/
│ │ │ │ │ │ ├── AppErrorBoundary.tsx
│ │ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ │ ├── ErrorElement.tsx
│ │ │ │ │ │ ├── ErrorTooltip.tsx
│ │ │ │ │ │ ├── ExPromise.tsx
│ │ │ │ │ │ ├── Focusable.tsx
│ │ │ │ │ │ ├── Fragment.tsx
│ │ │ │ │ │ ├── ImpressionTracker.tsx
│ │ │ │ │ │ ├── LCPEndDetector.tsx
│ │ │ │ │ │ ├── LoadMoreIndicator.tsx
│ │ │ │ │ │ ├── LoadRemixAsyncComponent.tsx
│ │ │ │ │ │ ├── Motion.tsx
│ │ │ │ │ │ ├── NotFound.tsx
│ │ │ │ │ │ ├── PoweredByFooter.tsx
│ │ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ │ ├── ReloadPrompt.tsx
│ │ │ │ │ │ ├── ShadowDOM.tsx
│ │ │ │ │ │ ├── SharePanel.tsx
│ │ │ │ │ │ └── withAppErrorBoundary.tsx
│ │ │ │ │ ├── errors/
│ │ │ │ │ │ ├── EntryNotFound.tsx
│ │ │ │ │ │ ├── FeedNotFound.tsx
│ │ │ │ │ │ ├── ModalError.tsx
│ │ │ │ │ │ ├── PageError.tsx
│ │ │ │ │ │ ├── RSSHubError.tsx
│ │ │ │ │ │ ├── enum.ts
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── mobile/
│ │ │ │ │ │ └── button.tsx
│ │ │ │ │ ├── ui/
│ │ │ │ │ │ ├── ai-summary-card/
│ │ │ │ │ │ │ ├── AISummaryCardBase.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── auto-completion/
│ │ │ │ │ │ │ ├── AutoCompletion.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── background/
│ │ │ │ │ │ │ ├── WindowUnderBlur.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── button/
│ │ │ │ │ │ │ ├── AnimatedCommandButton.tsx
│ │ │ │ │ │ │ ├── CommandActionButton.tsx
│ │ │ │ │ │ │ ├── CopyButton.tsx
│ │ │ │ │ │ │ ├── GlassButton.tsx
│ │ │ │ │ │ │ └── HeaderActionButton.tsx
│ │ │ │ │ │ ├── code-highlighter/
│ │ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── shiki/
│ │ │ │ │ │ │ ├── Shiki.tsx
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── shared.ts
│ │ │ │ │ │ │ └── shiki.module.css
│ │ │ │ │ │ ├── crop/
│ │ │ │ │ │ │ └── AvatarUploadModal.tsx
│ │ │ │ │ │ ├── datetime/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ │ └── dropdown-menu.tsx
│ │ │ │ │ │ ├── fab/
│ │ │ │ │ │ │ ├── FABContainer.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── hover-preview/
│ │ │ │ │ │ │ ├── EntryPreviewCard.tsx
│ │ │ │ │ │ │ ├── FeedPreviewCard.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── keyboard-recorder/
│ │ │ │ │ │ │ ├── KeyRecorder.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── markdown/
│ │ │ │ │ │ │ ├── HTML.tsx
│ │ │ │ │ │ │ ├── Markdown.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ ├── Toc.tsx
│ │ │ │ │ │ │ │ ├── TocItem.tsx
│ │ │ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── renderers/
│ │ │ │ │ │ │ │ ├── BlockErrorBoundary.tsx
│ │ │ │ │ │ │ │ ├── BlockImage.tsx
│ │ │ │ │ │ │ │ ├── Heading.tsx
│ │ │ │ │ │ │ │ ├── InlineImage.tsx
│ │ │ │ │ │ │ │ ├── MarkdownLink.tsx
│ │ │ │ │ │ │ │ ├── MarkdownP.tsx
│ │ │ │ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── media/
│ │ │ │ │ │ │ ├── Media.tsx
│ │ │ │ │ │ │ ├── MediaContainerWidthContext.tsx
│ │ │ │ │ │ │ ├── MediaContainerWidthProvider.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecord.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecordContext.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecordProvider.tsx
│ │ │ │ │ │ │ ├── PreviewMediaContent.tsx
│ │ │ │ │ │ │ ├── SwipeMedia.tsx
│ │ │ │ │ │ │ ├── VideoPlayer.tsx
│ │ │ │ │ │ │ ├── VolumeSlider.tsx
│ │ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ │ ├── modal/
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── close.tsx
│ │ │ │ │ │ │ ├── helper/
│ │ │ │ │ │ │ │ ├── useAsyncModal.tsx
│ │ │ │ │ │ │ │ └── useModalStackCalculationAndEffect.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── inspire/
│ │ │ │ │ │ │ │ ├── InPeekModal.tsx
│ │ │ │ │ │ │ │ └── PeekModal.tsx
│ │ │ │ │ │ │ └── stacked/
│ │ │ │ │ │ │ ├── AsyncModalContent.tsx
│ │ │ │ │ │ │ ├── atom.ts
│ │ │ │ │ │ │ ├── bus.ts
│ │ │ │ │ │ │ ├── components.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── custom-modal.tsx
│ │ │ │ │ │ │ ├── declarative-modal.tsx
│ │ │ │ │ │ │ ├── helper.tsx
│ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── internal/
│ │ │ │ │ │ │ │ ├── use-animate.ts
│ │ │ │ │ │ │ │ ├── use-drag.ts
│ │ │ │ │ │ │ │ ├── use-select.ts
│ │ │ │ │ │ │ │ └── use-subscriber.ts
│ │ │ │ │ │ │ ├── modal-stack.tsx
│ │ │ │ │ │ │ ├── modal.tsx
│ │ │ │ │ │ │ ├── overlay.tsx
│ │ │ │ │ │ │ ├── provider.tsx
│ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ ├── paper/
│ │ │ │ │ │ │ ├── Paper.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── peek-modal/
│ │ │ │ │ │ ├── EntryModalPreview.tsx
│ │ │ │ │ │ ├── EntryMoreActions.tsx
│ │ │ │ │ │ └── EntryToastPreview.tsx
│ │ │ │ │ └── ux/
│ │ │ │ │ ├── pull-to-refresh/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── transition/
│ │ │ │ │ └── icon.tsx
│ │ │ │ ├── constants/
│ │ │ │ │ ├── app.tsx
│ │ │ │ │ ├── copy.ts
│ │ │ │ │ ├── dom.ts
│ │ │ │ │ ├── env.ts
│ │ │ │ │ ├── hotkeys.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── ui.ts
│ │ │ │ ├── env.d.ts
│ │ │ │ ├── errors/
│ │ │ │ │ └── CustomSafeError.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── biz/
│ │ │ │ │ │ ├── useAsRead.ts
│ │ │ │ │ │ ├── useContextMenuActionShortCutTrigger.ts
│ │ │ │ │ │ ├── useDiscoverRSSHubRoute.tsx
│ │ │ │ │ │ ├── useEntryActions.tsx
│ │ │ │ │ │ ├── useEntryContextMenu.ts
│ │ │ │ │ │ ├── useFeature.ts
│ │ │ │ │ │ ├── useFeedActions.tsx
│ │ │ │ │ │ ├── useFollow.tsx
│ │ │ │ │ │ ├── useNavigateEntry.ts
│ │ │ │ │ │ ├── usePeekModal.tsx
│ │ │ │ │ │ ├── useProxySetting.ts
│ │ │ │ │ │ ├── useReduceMotion.ts
│ │ │ │ │ │ ├── useRenderStyle.tsx
│ │ │ │ │ │ ├── useRouteParams.ts
│ │ │ │ │ │ ├── useShowEntryDetailsColumn.ts
│ │ │ │ │ │ ├── useSubscriptionActions.tsx
│ │ │ │ │ │ ├── useTimelineList.ts
│ │ │ │ │ │ └── useTraySetting.ts
│ │ │ │ │ └── common/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── useBizQuery.ts
│ │ │ │ │ ├── useContextMenu.tsx
│ │ │ │ │ ├── useFeedSafeUrl.ts
│ │ │ │ │ ├── useI18n.ts
│ │ │ │ │ ├── useLoginModal.tsx
│ │ │ │ │ ├── usePreventOverscrollBounce.ts
│ │ │ │ │ ├── useRecaptchaToken.ts
│ │ │ │ │ ├── useRequireLogin.ts
│ │ │ │ │ └── useSyncTheme.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── initialize/
│ │ │ │ │ ├── analytics.ts
│ │ │ │ │ ├── global-shortcuts.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── migrates/
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── v/
│ │ │ │ │ │ └── v1.ts
│ │ │ │ │ ├── queue.ts
│ │ │ │ │ └── settings.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── parse-html.test.ts
│ │ │ │ │ ├── api-client.ts
│ │ │ │ │ ├── app.ts
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── avatar-upload.ts
│ │ │ │ │ ├── client-session.ts
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── clipboard.ts
│ │ │ │ │ ├── defineQuery.ts
│ │ │ │ │ ├── dev.tsx
│ │ │ │ │ ├── error-parser.ts
│ │ │ │ │ ├── export.ts
│ │ │ │ │ ├── features.tsx
│ │ │ │ │ ├── ga4.ts
│ │ │ │ │ ├── img-proxy.ts
│ │ │ │ │ ├── issues.ts
│ │ │ │ │ ├── jotai.ts
│ │ │ │ │ ├── language.ts
│ │ │ │ │ ├── load-language.ts
│ │ │ │ │ ├── log.ts
│ │ │ │ │ ├── native-menu.ts
│ │ │ │ │ ├── observe-resize.ts
│ │ │ │ │ ├── parse-html.ts
│ │ │ │ │ ├── parse-markdown.ts
│ │ │ │ │ ├── parsers.ts
│ │ │ │ │ ├── query-client.ts
│ │ │ │ │ ├── simple-text-selection.ts
│ │ │ │ │ ├── url-builder.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── main.tsx
│ │ │ │ ├── modules/
│ │ │ │ │ ├── action/
│ │ │ │ │ │ ├── action-setting.tsx
│ │ │ │ │ │ ├── constants.tsx
│ │ │ │ │ │ ├── rule-card.tsx
│ │ │ │ │ │ ├── rule-summary.ts
│ │ │ │ │ │ ├── then-section.tsx
│ │ │ │ │ │ ├── utils.ts
│ │ │ │ │ │ └── when-section.tsx
│ │ │ │ │ ├── ai-chat/
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ └── session.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── 3d-models/
│ │ │ │ │ │ │ │ ├── AISpline.ts
│ │ │ │ │ │ │ │ └── AISplineLoader.tsx
│ │ │ │ │ │ │ ├── context-bar/
│ │ │ │ │ │ │ │ ├── MentionButton.tsx
│ │ │ │ │ │ │ │ ├── blocks/
│ │ │ │ │ │ │ │ │ ├── ContextBlock.tsx
│ │ │ │ │ │ │ │ │ ├── TitleComponents.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── menus/
│ │ │ │ │ │ │ │ ├── ShortcutsMenuContent.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── displays/
│ │ │ │ │ │ │ │ ├── AIChainOfThought.tsx
│ │ │ │ │ │ │ │ ├── AIDisplayFlowPart.tsx
│ │ │ │ │ │ │ │ ├── AIReasoningPart.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── share.tsx
│ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ ├── AnalyticsMetrics.tsx
│ │ │ │ │ │ │ │ ├── CategoryTag.tsx
│ │ │ │ │ │ │ │ ├── DisplayHeader.tsx
│ │ │ │ │ │ │ │ ├── EmptyState.tsx
│ │ │ │ │ │ │ │ ├── FeedItemCard.tsx
│ │ │ │ │ │ │ │ ├── GroupedContent.tsx
│ │ │ │ │ │ │ │ ├── StatCard.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── file/
│ │ │ │ │ │ │ │ └── GlobalFileDropZone.tsx
│ │ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ │ ├── AIChatContextBar.tsx
│ │ │ │ │ │ │ │ ├── AIChatRoot.tsx
│ │ │ │ │ │ │ │ ├── AIChatSendButton.tsx
│ │ │ │ │ │ │ │ ├── AIErrorFallback.tsx
│ │ │ │ │ │ │ │ ├── AIModelIndicator.tsx
│ │ │ │ │ │ │ │ ├── AISmartSidebar.css
│ │ │ │ │ │ │ │ ├── AISmartSidebar.tsx
│ │ │ │ │ │ │ │ ├── ChatBottomPanel.tsx
│ │ │ │ │ │ │ │ ├── ChatHeader.tsx
│ │ │ │ │ │ │ │ ├── ChatHistoryDropdown.tsx
│ │ │ │ │ │ │ │ ├── ChatInput.tsx
│ │ │ │ │ │ │ │ ├── ChatInterface.tsx
│ │ │ │ │ │ │ │ ├── ChatMessageContainer.tsx
│ │ │ │ │ │ │ │ ├── ChatMoreDropdown.tsx
│ │ │ │ │ │ │ │ ├── ChatShortcutsRow.tsx
│ │ │ │ │ │ │ │ ├── ChatTitle.tsx
│ │ │ │ │ │ │ │ ├── Messages.tsx
│ │ │ │ │ │ │ │ ├── RateLimitNotice.tsx
│ │ │ │ │ │ │ │ ├── ScrollToBottomButton.tsx
│ │ │ │ │ │ │ │ ├── TaskReportDropdown.tsx
│ │ │ │ │ │ │ │ ├── WelcomeScreen.tsx
│ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ ├── ChatSessionComponents.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── useChatSessionHandlers.tsx
│ │ │ │ │ │ │ │ └── utils.ts
│ │ │ │ │ │ │ ├── message/
│ │ │ │ │ │ │ │ ├── AIChatMessage.tsx
│ │ │ │ │ │ │ │ ├── AIDataBlockPart.tsx
│ │ │ │ │ │ │ │ ├── AIMarkdownMessage.tsx
│ │ │ │ │ │ │ │ ├── AIMessageIdContext.tsx
│ │ │ │ │ │ │ │ ├── AIMessageParts.tsx
│ │ │ │ │ │ │ │ ├── BlockTitleComponents.tsx
│ │ │ │ │ │ │ │ ├── EditableMessage.tsx
│ │ │ │ │ │ │ │ ├── ErrorMessage.tsx
│ │ │ │ │ │ │ │ ├── ImageThumbnail.tsx
│ │ │ │ │ │ │ │ ├── TokenUsagePill.tsx
│ │ │ │ │ │ │ │ ├── ToolInvocationComponent.tsx
│ │ │ │ │ │ │ │ ├── UserChatMessage.tsx
│ │ │ │ │ │ │ │ ├── UserMessageParts.tsx
│ │ │ │ │ │ │ │ ├── UserRichTextMessage.tsx
│ │ │ │ │ │ │ │ ├── ai-block-constants.ts
│ │ │ │ │ │ │ │ ├── animated/
│ │ │ │ │ │ │ │ │ ├── AnimatedMarkdown.tsx
│ │ │ │ │ │ │ │ │ ├── TokenizedText.tsx
│ │ │ │ │ │ │ │ │ └── constants.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── parse-incomplete-markdown.ts
│ │ │ │ │ │ │ │ └── useContextBlockPresentation.tsx
│ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ └── common-states.tsx
│ │ │ │ │ │ │ ├── ui/
│ │ │ │ │ │ │ │ ├── AIShortcutButton.tsx
│ │ │ │ │ │ │ │ ├── ShortcutTooltip.tsx
│ │ │ │ │ │ │ │ └── UploadProgress.tsx
│ │ │ │ │ │ │ └── welcome/
│ │ │ │ │ │ │ ├── DefaultWelcomeContent.tsx
│ │ │ │ │ │ │ ├── EntrySummaryCard.tsx
│ │ │ │ │ │ │ ├── EntryWelcomeContent.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── editor/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── plugins/
│ │ │ │ │ │ │ ├── file-upload/
│ │ │ │ │ │ │ │ ├── FileAttachmentNode.tsx
│ │ │ │ │ │ │ │ ├── FileUploadPlugin.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ └── FileDropZone.tsx
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── useFileAttachmentBlockSync.ts
│ │ │ │ │ │ │ │ │ └── useFileUploadIntegration.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ └── file-handling.ts
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── mention/
│ │ │ │ │ │ │ │ ├── MentionNode.tsx
│ │ │ │ │ │ │ │ ├── MentionPlugin.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── MentionComponent.tsx
│ │ │ │ │ │ │ │ │ ├── MentionDropdown.tsx
│ │ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ │ └── MentionTypeIcon.tsx
│ │ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── dateMentionConfig.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionParsers.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionSearch.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionUtils.ts
│ │ │ │ │ │ │ │ │ ├── useMentionKeyboard.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSearch.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSearchService.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSelection.ts
│ │ │ │ │ │ │ │ │ └── useMentionTrigger.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ ├── mentionTextValue.ts
│ │ │ │ │ │ │ │ ├── parseNaturalLanguageDate.ts
│ │ │ │ │ │ │ │ ├── textReplacement.ts
│ │ │ │ │ │ │ │ └── triggerDetection.ts
│ │ │ │ │ │ │ ├── selection/
│ │ │ │ │ │ │ │ ├── SelectedTextNode.tsx
│ │ │ │ │ │ │ │ ├── SelectedTextNodeComponent.tsx
│ │ │ │ │ │ │ │ ├── SelectedTextPlugin.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── insertSelectedTextNode.ts
│ │ │ │ │ │ │ │ └── selectedTextBridge.ts
│ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── MentionLikePill.tsx
│ │ │ │ │ │ │ │ │ ├── TypeaheadDropdown.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── useListKeyboardNavigation.ts
│ │ │ │ │ │ │ │ │ ├── useTextTrigger.ts
│ │ │ │ │ │ │ │ │ └── useTypeaheadSelection.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ └── positioning.ts
│ │ │ │ │ │ │ └── shortcut/
│ │ │ │ │ │ │ ├── ShortcutNode.tsx
│ │ │ │ │ │ │ ├── ShortcutPlugin.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ ├── ShortcutComponent.tsx
│ │ │ │ │ │ │ │ └── ShortcutDropdown.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ ├── useShortcutKeyboard.ts
│ │ │ │ │ │ │ │ ├── useShortcutSearch.ts
│ │ │ │ │ │ │ │ ├── useShortcutSearchService.ts
│ │ │ │ │ │ │ │ ├── useShortcutSelection.ts
│ │ │ │ │ │ │ │ └── useShortcutTrigger.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── positioning.ts
│ │ │ │ │ │ │ ├── shortcutTextValue.ts
│ │ │ │ │ │ │ ├── textReplacement.ts
│ │ │ │ │ │ │ └── triggerDetection.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── useAIConfiguration.ts
│ │ │ │ │ │ │ ├── useAIModel.ts
│ │ │ │ │ │ │ ├── useAIShortcut.ts
│ │ │ │ │ │ │ ├── useAttachScrollBeyond.tsx
│ │ │ │ │ │ │ ├── useAutoScroll.tsx
│ │ │ │ │ │ │ ├── useAutoTimelineSummaryShortcut.ts
│ │ │ │ │ │ │ ├── useChatHistory.ts
│ │ │ │ │ │ │ ├── useDisplayBlocks.ts
│ │ │ │ │ │ │ ├── useFeedEntrySearchService.ts
│ │ │ │ │ │ │ ├── useFileUpload.ts
│ │ │ │ │ │ │ ├── useLoadMessages.ts
│ │ │ │ │ │ │ ├── useMainEntryId.ts
│ │ │ │ │ │ │ ├── useSendAIShortcut.ts
│ │ │ │ │ │ │ └── useTimelineSummaryAutoContext.ts
│ │ │ │ │ │ ├── services/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── store/
│ │ │ │ │ │ │ ├── AIChatContext.ts
│ │ │ │ │ │ │ ├── chat-core/
│ │ │ │ │ │ │ │ ├── chat-actions.ts
│ │ │ │ │ │ │ │ ├── chat-instance.ts
│ │ │ │ │ │ │ │ ├── chat-state.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ ├── event-system/
│ │ │ │ │ │ │ │ ├── event-emitter.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── slices/
│ │ │ │ │ │ │ │ ├── block.slice.ts
│ │ │ │ │ │ │ │ ├── chat.slice.ts
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ │ ├── transport.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── types/
│ │ │ │ │ │ │ └── ChatSession.ts
│ │ │ │ │ │ └── utils/
│ │ │ │ │ │ ├── error.ts
│ │ │ │ │ │ ├── extractor.ts
│ │ │ │ │ │ ├── file-processing.ts
│ │ │ │ │ │ ├── file-validation.ts
│ │ │ │ │ │ ├── mentionDate.ts
│ │ │ │ │ │ ├── rate-limit.ts
│ │ │ │ │ │ ├── shortcut.ts
│ │ │ │ │ │ └── titleGeneration.ts
│ │ │ │ │ ├── ai-chat-session/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── query.ts
│ │ │ │ │ │ ├── service.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── ai-onboarding/
│ │ │ │ │ │ ├── ai-chat-pane.tsx
│ │ │ │ │ │ ├── ai-onboarding-modal-content.tsx
│ │ │ │ │ │ ├── feeds-selection-list.tsx
│ │ │ │ │ │ ├── modal.tsx
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── ai-task/
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── ai-item-actions.tsx
│ │ │ │ │ │ │ ├── ai-task-modal.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── notify-channels-config.tsx
│ │ │ │ │ │ │ ├── schedule-config.tsx
│ │ │ │ │ │ │ ├── task-item.tsx
│ │ │ │ │ │ │ └── task-list.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── query.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── app/
│ │ │ │ │ │ ├── EnvironmentIndicator.tsx
│ │ │ │ │ │ ├── NetworkStatusIndicator.tsx
│ │ │ │ │ │ └── Titlebar.tsx
│ │ │ │ │ ├── app-layout/
│ │ │ │ │ │ ├── LAYOUT_ARCHITECTURE.md
│ │ │ │ │ │ ├── MainDestopLayout.tsx
│ │ │ │ │ │ ├── ai/
│ │ │ │ │ │ │ ├── AIChatFixedPanel.tsx
│ │ │ │ │ │ │ ├── AIChatFloatingPanel.tsx
│ │ │ │ │ │ │ └── AISplineButton.tsx
│ │ │ │ │ │ ├── ai-enhanced-timeline/
│ │ │ │ │ │ │ ├── AIEnhancedTimelineLayout.tsx
│ │ │ │ │ │ │ ├── MobileTimelineLayout.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ │ └── EntryContentPlaceholder.tsx
│ │ │ │ │ │ ├── subscription-column/
│ │ │ │ │ │ │ ├── SubscriptionColumn.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── PodcastButton.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── subview/
│ │ │ │ │ │ ├── SubviewLayout.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── app-tip/
│ │ │ │ │ │ ├── AICopilotMedia.tsx
│ │ │ │ │ │ ├── AppTipDialog.tsx
│ │ │ │ │ │ ├── AppTipMediaPreview.tsx
│ │ │ │ │ │ ├── AppTipModalContent.tsx
│ │ │ │ │ │ ├── OverviewMedia.tsx
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── useNewUserGuideState.ts
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ ├── Form.tsx
│ │ │ │ │ │ ├── LoginModalContent.tsx
│ │ │ │ │ │ └── TokenModal.tsx
│ │ │ │ │ ├── claim/
│ │ │ │ │ │ ├── feed-claim-modal.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── command/
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── command-button.test-d.ts
│ │ │ │ │ │ ├── command-button.tsx
│ │ │ │ │ │ ├── command-manager.ts
│ │ │ │ │ │ ├── commands/
│ │ │ │ │ │ │ ├── entry-render.tsx
│ │ │ │ │ │ │ ├── entry.tsx
│ │ │ │ │ │ │ ├── global.tsx
│ │ │ │ │ │ │ ├── id.ts
│ │ │ │ │ │ │ ├── integration.tsx
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ │ ├── settings.tsx
│ │ │ │ │ │ │ ├── subscription.tsx
│ │ │ │ │ │ │ ├── timeline.tsx
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── use-command-binding.ts
│ │ │ │ │ │ │ ├── use-command.test-d.ts
│ │ │ │ │ │ │ ├── use-command.ts
│ │ │ │ │ │ │ ├── use-register-command.test-d.ts
│ │ │ │ │ │ │ ├── use-register-command.ts
│ │ │ │ │ │ │ ├── use-register-hotkey.test-d.ts
│ │ │ │ │ │ │ └── use-register-hotkey.ts
│ │ │ │ │ │ ├── mutation-command-ids.ts
│ │ │ │ │ │ ├── registry/
│ │ │ │ │ │ │ ├── command.test-d.ts
│ │ │ │ │ │ │ ├── command.ts
│ │ │ │ │ │ │ └── registry.ts
│ │ │ │ │ │ ├── shortcuts/
│ │ │ │ │ │ │ └── SettingShortcuts.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── customize-toolbar/
│ │ │ │ │ │ ├── constant.ts
│ │ │ │ │ │ ├── dnd.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── modal.tsx
│ │ │ │ │ ├── debug/
│ │ │ │ │ │ └── registry.ts
│ │ │ │ │ ├── discover/
│ │ │ │ │ │ ├── DiscoverFeedCard.tsx
│ │ │ │ │ │ ├── DiscoverFeedForm.tsx
│ │ │ │ │ │ ├── DiscoverForm.tsx
│ │ │ │ │ │ ├── DiscoverImport.tsx
│ │ │ │ │ │ ├── DiscoverInboxList.tsx
│ │ │ │ │ │ ├── DiscoverTransform.tsx
│ │ │ │ │ │ ├── DiscoverUser.tsx
│ │ │ │ │ │ ├── DiscoveryContent.tsx
│ │ │ │ │ │ ├── FeedForm.tsx
│ │ │ │ │ │ ├── FeedSummary.tsx
│ │ │ │ │ │ ├── Inbox/
│ │ │ │ │ │ │ ├── ConfirmDestroyModalContent.tsx
│ │ │ │ │ │ │ ├── InboxActions.tsx
│ │ │ │ │ │ │ ├── InboxEmail.tsx
│ │ │ │ │ │ │ ├── InboxSecret.tsx
│ │ │ │ │ │ │ ├── InboxTable.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── InboxForm.tsx
│ │ │ │ │ │ ├── ListForm.tsx
│ │ │ │ │ │ ├── OpmlAbstractGraphic.tsx
│ │ │ │ │ │ ├── OpmlSelectionModal.tsx
│ │ │ │ │ │ ├── RecommendationContent.tsx
│ │ │ │ │ │ ├── TrendingFeedCard.tsx
│ │ │ │ │ │ ├── UnifiedDiscoverForm.tsx
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ └── discover.ts
│ │ │ │ │ │ ├── example-data.json
│ │ │ │ │ │ ├── recommendations.tsx
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── download/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── editor/
│ │ │ │ │ │ └── css-editor.tsx
│ │ │ │ │ ├── entry-column/
│ │ │ │ │ │ ├── EntryColumnShortcutHandler.tsx
│ │ │ │ │ │ ├── EntryItemSkeleton.tsx
│ │ │ │ │ │ ├── Items/
│ │ │ │ │ │ │ ├── all-item.tsx
│ │ │ │ │ │ │ ├── article-item.tsx
│ │ │ │ │ │ │ ├── audio-item.tsx
│ │ │ │ │ │ │ ├── getItemComponentByView.ts
│ │ │ │ │ │ │ ├── getSkeletonItemComponentByView.ts
│ │ │ │ │ │ │ ├── list-item.tsx
│ │ │ │ │ │ │ ├── media-gallery.tsx
│ │ │ │ │ │ │ ├── notification-item.tsx
│ │ │ │ │ │ │ ├── picture-item-skeleton.tsx
│ │ │ │ │ │ │ ├── picture-item-stateless.tsx
│ │ │ │ │ │ │ ├── picture-item.tsx
│ │ │ │ │ │ │ ├── picture-masonry.tsx
│ │ │ │ │ │ │ ├── social-media-item.tsx
│ │ │ │ │ │ │ └── video-item.tsx
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ ├── ai-timeline.ts
│ │ │ │ │ │ │ └── social-media-content-width.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── DateItem.tsx
│ │ │ │ │ │ │ ├── FooterMarkItem.tsx
│ │ │ │ │ │ │ ├── VirtualRowItem.tsx
│ │ │ │ │ │ │ ├── ai-timeline-loading/
│ │ │ │ │ │ │ │ ├── AITimelineLoadingOverlay.css
│ │ │ │ │ │ │ │ └── AITimelineLoadingOverlay.tsx
│ │ │ │ │ │ │ ├── entry-column-wrapper/
│ │ │ │ │ │ │ │ ├── EntryColumnWrapper.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ └── mark-all-button.tsx
│ │ │ │ │ │ ├── context/
│ │ │ │ │ │ │ └── EntriesContext.tsx
│ │ │ │ │ │ ├── grid.tsx
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── useAttachScrollBeyond.tsx
│ │ │ │ │ │ │ ├── useEntriesByView.ts
│ │ │ │ │ │ │ ├── useEntryIdListSnap.ts
│ │ │ │ │ │ │ ├── useEntryMarkReadHandler.tsx
│ │ │ │ │ │ │ ├── useEntryVirtualization.ts
│ │ │ │ │ │ │ ├── useIsPreviewFeed.ts
│ │ │ │ │ │ │ ├── useLocalEntries.ts
│ │ │ │ │ │ │ ├── useMarkAll.ts
│ │ │ │ │ │ │ ├── useNavigateFirstEntry.tsx
│ │ │ │ │ │ │ └── useWheelGestureClose.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── item-stateless.tsx
│ │ │ │ │ │ ├── item.tsx
│ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ ├── AppendTaildingDivider.tsx
│ │ │ │ │ │ │ ├── EntryItemWrapper.tsx
│ │ │ │ │ │ │ ├── EntryListHeader.tsx
│ │ │ │ │ │ │ └── buttons/
│ │ │ │ │ │ │ └── SwitchToMasonryButton.tsx
│ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ ├── star-icon.tsx
│ │ │ │ │ │ ├── store/
│ │ │ │ │ │ │ └── EntryColumnContext.ts
│ │ │ │ │ │ ├── styles.ts
│ │ │ │ │ │ ├── templates/
│ │ │ │ │ │ │ ├── grid-item-template.tsx
│ │ │ │ │ │ │ └── list-item-template.tsx
│ │ │ │ │ │ ├── translation.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ ├── EntryContent.tsx
│ │ │ │ │ │ ├── EntryContentForPreview.tsx
│ │ │ │ │ │ ├── actions/
│ │ │ │ │ │ │ ├── header-actions.tsx
│ │ │ │ │ │ │ ├── more-actions.tsx
│ │ │ │ │ │ │ └── picture-gallery.tsx
│ │ │ │ │ │ ├── atoms.tsx
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── AISummary.tsx
│ │ │ │ │ │ │ ├── ApplyEntryActions.tsx
│ │ │ │ │ │ │ ├── EntryAttachments.tsx
│ │ │ │ │ │ │ ├── EntryPlaceholderLogo.tsx
│ │ │ │ │ │ │ ├── EntryTimelineSidebar.tsx
│ │ │ │ │ │ │ ├── EntryTitle.tsx
│ │ │ │ │ │ │ ├── ImageGalleryContent.tsx
│ │ │ │ │ │ │ ├── SourceContentView.tsx
│ │ │ │ │ │ │ ├── WarnGoToExternalLink.tsx
│ │ │ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ │ │ ├── EntryCommandShortcutRegister.tsx
│ │ │ │ │ │ │ │ ├── EntryContentFallback.tsx
│ │ │ │ │ │ │ │ ├── EntryContentLoading.tsx
│ │ │ │ │ │ │ │ ├── EntryNoContent.tsx
│ │ │ │ │ │ │ │ ├── EntryRenderError.tsx
│ │ │ │ │ │ │ │ ├── EntryScrollingAndNavigationHandler.tsx
│ │ │ │ │ │ │ │ ├── EntryTitleMetaHandler.tsx
│ │ │ │ │ │ │ │ ├── ReadabilityNotice.tsx
│ │ │ │ │ │ │ │ ├── accessories/
│ │ │ │ │ │ │ │ │ ├── ContainerToc.tsx
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ ├── entry-header/
│ │ │ │ │ │ │ │ ├── AIEntryHeader.tsx
│ │ │ │ │ │ │ │ ├── EntryHeader.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── internal/
│ │ │ │ │ │ │ │ │ ├── EntryHeaderActionsContainer.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderBreadcrumb.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderMeta.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderReadHistory.tsx
│ │ │ │ │ │ │ │ │ └── context.tsx
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ ├── entry-read-history/
│ │ │ │ │ │ │ │ ├── EntryReadHistory.tsx
│ │ │ │ │ │ │ │ ├── EntryUser.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ │ ├── ArticleLayout.tsx
│ │ │ │ │ │ │ │ ├── MediaLayout.tsx
│ │ │ │ │ │ │ │ ├── PicturesLayout.tsx
│ │ │ │ │ │ │ │ ├── SocialMediaLayout.tsx
│ │ │ │ │ │ │ │ ├── VideosLayout.tsx
│ │ │ │ │ │ │ │ ├── factory.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ │ ├── AudioPlayer.tsx
│ │ │ │ │ │ │ │ │ ├── AuthorHeader.tsx
│ │ │ │ │ │ │ │ │ ├── ContentBody.tsx
│ │ │ │ │ │ │ │ │ ├── MediaTranscript.tsx
│ │ │ │ │ │ │ │ │ ├── TranscriptToggle.tsx
│ │ │ │ │ │ │ │ │ ├── VideoPlayer.tsx
│ │ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ │ └── useTranscription.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ └── selection/
│ │ │ │ │ │ │ ├── GlassButton.tsx
│ │ │ │ │ │ │ ├── SharePosterModal.tsx
│ │ │ │ │ │ │ └── TextSelectionToolbar.tsx
│ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ └── navigation-hints.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ └── useEntryNavigationHints.ts
│ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ ├── feed-certification.tsx
│ │ │ │ │ │ ├── feed-icon.tsx
│ │ │ │ │ │ ├── feed-summary.tsx
│ │ │ │ │ │ ├── feed-title.tsx
│ │ │ │ │ │ └── view-select-content.tsx
│ │ │ │ │ ├── integration/
│ │ │ │ │ │ ├── CustomIntegrationPreview.tsx
│ │ │ │ │ │ ├── CustomIntegrationValidator.tsx
│ │ │ │ │ │ ├── PlaceholderHelp.tsx
│ │ │ │ │ │ ├── URLSchemePreview.tsx
│ │ │ │ │ │ ├── custom-integration-manager.ts
│ │ │ │ │ │ ├── fetch-adapter.ts
│ │ │ │ │ │ └── url-scheme-handler.ts
│ │ │ │ │ ├── modal/
│ │ │ │ │ │ ├── ConfirmDestroyModalContent.tsx
│ │ │ │ │ │ ├── ShortcutModalContent.tsx
│ │ │ │ │ │ └── hooks/
│ │ │ │ │ │ ├── useConfirmUnsubscribeSubscriptionModal.tsx
│ │ │ │ │ │ └── useShortcutsModal.tsx
│ │ │ │ │ ├── new-user-guide/
│ │ │ │ │ │ ├── ai-chat-pane.tsx
│ │ │ │ │ │ ├── discover-import-step.tsx
│ │ │ │ │ │ ├── feeds-selection-list.tsx
│ │ │ │ │ │ ├── pre-finish.tsx
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── panel/
│ │ │ │ │ │ ├── cmdf.tsx
│ │ │ │ │ │ ├── cmdk.module.css
│ │ │ │ │ │ ├── cmdk.tsx
│ │ │ │ │ │ └── cmdn.tsx
│ │ │ │ │ ├── plan/
│ │ │ │ │ │ ├── UpgradePlanModalContent.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── player/
│ │ │ │ │ │ ├── corner-player.tsx
│ │ │ │ │ │ └── entry-tts.ts
│ │ │ │ │ ├── power/
│ │ │ │ │ │ ├── my-wallet-section/
│ │ │ │ │ │ │ ├── create-wallet.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── withdraw.tsx
│ │ │ │ │ │ └── transaction-section/
│ │ │ │ │ │ ├── TransactionsSection.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── tx-table/
│ │ │ │ │ │ ├── TxTable.tsx
│ │ │ │ │ │ ├── components.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── profile/
│ │ │ │ │ │ ├── account-management.tsx
│ │ │ │ │ │ ├── email-management.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── profile-setting-form.tsx
│ │ │ │ │ │ ├── two-factor.tsx
│ │ │ │ │ │ ├── update-password-form.tsx
│ │ │ │ │ │ ├── user-profile-modal/
│ │ │ │ │ │ │ ├── UserProfileModalContent.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── shared.tsx
│ │ │ │ │ │ └── user-profile-modal.constants.ts
│ │ │ │ │ ├── renderer/
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ └── TimeStamp.tsx
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ ├── html.tsx
│ │ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── review-prompt/
│ │ │ │ │ │ ├── ReviewPromptModalContent.tsx
│ │ │ │ │ │ ├── debug.ts
│ │ │ │ │ │ ├── provider.tsx
│ │ │ │ │ │ ├── use-review-prompt-state.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── rsshub/
│ │ │ │ │ │ ├── add-modal-content.tsx
│ │ │ │ │ │ ├── delete-modal-content.tsx
│ │ │ │ │ │ └── set-modal-content.tsx
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ ├── control.tsx
│ │ │ │ │ │ ├── helper/
│ │ │ │ │ │ │ ├── EnhancedIndicator.tsx
│ │ │ │ │ │ │ ├── SyncIndicator.tsx
│ │ │ │ │ │ │ ├── builder.ts
│ │ │ │ │ │ │ ├── setting-builder.tsx
│ │ │ │ │ │ │ ├── sync-queue.ts
│ │ │ │ │ │ │ └── withSettingEnable.tsx
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── use-setting-ctx.ts
│ │ │ │ │ │ │ └── useWrapEnhancedSettingItem.ts
│ │ │ │ │ │ ├── modal/
│ │ │ │ │ │ │ ├── SettingModalContent.tsx
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── use-setting-modal-hack.ts
│ │ │ │ │ │ │ └── useSettingModal.ts
│ │ │ │ │ │ ├── section.tsx
│ │ │ │ │ │ ├── sections/
│ │ │ │ │ │ │ └── fonts.tsx
│ │ │ │ │ │ ├── settings-glob.ts
│ │ │ │ │ │ ├── tabs/
│ │ │ │ │ │ │ ├── about.tsx
│ │ │ │ │ │ │ ├── ai/
│ │ │ │ │ │ │ │ ├── PanelStyleSection.tsx
│ │ │ │ │ │ │ │ ├── PersonalizePromptSection.tsx
│ │ │ │ │ │ │ │ ├── TimelinePromptSection.tsx
│ │ │ │ │ │ │ │ ├── byok/
│ │ │ │ │ │ │ │ │ ├── ByokProviderItem.tsx
│ │ │ │ │ │ │ │ │ ├── ByokProviderModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── ByokSection.tsx
│ │ │ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── mcp/
│ │ │ │ │ │ │ │ │ ├── MCPPresetCard.tsx
│ │ │ │ │ │ │ │ │ ├── MCPPresetSelectionModal.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServiceItem.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServiceModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServicesSection.tsx
│ │ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ │ ├── shortcuts/
│ │ │ │ │ │ │ │ │ ├── AIShortcutsSection.tsx
│ │ │ │ │ │ │ │ │ ├── ShortcutItem.tsx
│ │ │ │ │ │ │ │ │ ├── ShortcutModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── tasks/
│ │ │ │ │ │ │ │ │ ├── TaskSchedulingSection.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ └── usage/
│ │ │ │ │ │ │ │ ├── UsageAnalysisSection.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── DetailedUsageModal.tsx
│ │ │ │ │ │ │ │ │ ├── EfficiencyTab.tsx
│ │ │ │ │ │ │ │ │ ├── HistoryTab.tsx
│ │ │ │ │ │ │ │ │ ├── OverviewTab.tsx
│ │ │ │ │ │ │ │ │ ├── PatternsTab.tsx
│ │ │ │ │ │ │ │ │ ├── UsageProgressRing.tsx
│ │ │ │ │ │ │ │ │ ├── UsageWarningBanner.tsx
│ │ │ │ │ │ │ │ │ ├── charts/
│ │ │ │ │ │ │ │ │ │ ├── BarList.tsx
│ │ │ │ │ │ │ │ │ │ ├── Sparkline.tsx
│ │ │ │ │ │ │ │ │ │ ├── TinyBars.tsx
│ │ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils.ts
│ │ │ │ │ │ │ ├── ai.tsx
│ │ │ │ │ │ │ ├── appearance.tsx
│ │ │ │ │ │ │ ├── cli.tsx
│ │ │ │ │ │ │ ├── data-control.tsx
│ │ │ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ │ ├── integration/
│ │ │ │ │ │ │ │ ├── CustomIntegrationModal.tsx
│ │ │ │ │ │ │ │ ├── CustomIntegrationSection.tsx
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── lists/
│ │ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── modals.tsx
│ │ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ │ ├── plan.tsx
│ │ │ │ │ │ │ └── shortcut.tsx
│ │ │ │ │ │ ├── title.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ └── ViewSelectorRadioGroup.tsx
│ │ │ │ │ ├── subscription-column/
│ │ │ │ │ │ ├── CategoryRemoveDialogContent.tsx
│ │ │ │ │ │ ├── CategoryUnsubscribeDialogContent.tsx
│ │ │ │ │ │ ├── FeedCategory.tsx
│ │ │ │ │ │ ├── FeedItem.tsx
│ │ │ │ │ │ ├── RenameCategoryForm.tsx
│ │ │ │ │ │ ├── SimpleDiscoverModal.tsx
│ │ │ │ │ │ ├── SortedFeedItems.tsx
│ │ │ │ │ │ ├── SubscriptionColumnHeader.tsx
│ │ │ │ │ │ ├── SubscriptionTabButton.tsx
│ │ │ │ │ │ ├── TimelineTabsSettingsModal.tsx
│ │ │ │ │ │ ├── UnreadNumber.tsx
│ │ │ │ │ │ ├── atom.ts
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── hook.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── sort-by/
│ │ │ │ │ │ │ ├── SortByAlphabeticalList.tsx
│ │ │ │ │ │ │ ├── SortByUnreadList.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ ├── styles.ts
│ │ │ │ │ │ └── subscription-list/
│ │ │ │ │ │ ├── EmptyFeedList.tsx
│ │ │ │ │ │ ├── ListHeader.tsx
│ │ │ │ │ │ ├── SortButton.tsx
│ │ │ │ │ │ ├── StarredItem.tsx
│ │ │ │ │ │ ├── SubscriptionList.tsx
│ │ │ │ │ │ ├── SubscriptionListGuard.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── trending/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── update-notice/
│ │ │ │ │ │ └── UpdateNotice.tsx
│ │ │ │ │ ├── upgrade/
│ │ │ │ │ │ ├── container.tsx
│ │ │ │ │ │ ├── lazy/
│ │ │ │ │ │ │ ├── index.electron.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── user/
│ │ │ │ │ │ ├── LoginButton.tsx
│ │ │ │ │ │ ├── ProfileButton.tsx
│ │ │ │ │ │ ├── UserAvatar.tsx
│ │ │ │ │ │ ├── UserGallery.tsx
│ │ │ │ │ │ ├── UserProBadge.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ └── wallet/
│ │ │ │ │ └── balance.tsx
│ │ │ │ ├── pages/
│ │ │ │ │ ├── (main)/
│ │ │ │ │ │ ├── (layer)/
│ │ │ │ │ │ │ ├── (ai)/
│ │ │ │ │ │ │ │ └── ai/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── (subview)/
│ │ │ │ │ │ │ │ ├── action/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── discover/
│ │ │ │ │ │ │ │ │ ├── category/
│ │ │ │ │ │ │ │ │ │ └── [category].tsx
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ │ ├── power/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ └── rsshub/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── timeline/
│ │ │ │ │ │ │ └── [timelineId]/
│ │ │ │ │ │ │ ├── [feedId]/
│ │ │ │ │ │ │ │ ├── [entryId]/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ │ ├── index.sync.tsx
│ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ └── settings/
│ │ │ │ │ ├── (settings)/
│ │ │ │ │ │ ├── about.tsx
│ │ │ │ │ │ ├── ai.tsx
│ │ │ │ │ │ ├── appearance.tsx
│ │ │ │ │ │ ├── cli.tsx
│ │ │ │ │ │ ├── data-control.tsx
│ │ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ ├── integration.tsx
│ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ ├── plan.tsx
│ │ │ │ │ │ ├── profile.tsx
│ │ │ │ │ │ └── shortcuts.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ ├── app-grid-layout-container-provider.tsx
│ │ │ │ │ ├── context-menu-provider.tsx
│ │ │ │ │ ├── extension-expose-provider.tsx
│ │ │ │ │ ├── external-jump-in-provider.tsx
│ │ │ │ │ ├── global-focusable-provider.tsx
│ │ │ │ │ ├── global-hotkeys-provider.tsx
│ │ │ │ │ ├── hotkey-provider.tsx
│ │ │ │ │ ├── i18n-provider.tsx
│ │ │ │ │ ├── inject-styles-provider.tsx
│ │ │ │ │ ├── invalidate-query-provider.tsx
│ │ │ │ │ ├── lazy/
│ │ │ │ │ │ ├── index.electron.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── main-view-hotkeys-provider.tsx
│ │ │ │ │ ├── popover-provider.tsx
│ │ │ │ │ ├── root-providers.tsx
│ │ │ │ │ ├── server-configs-provider.tsx
│ │ │ │ │ ├── setting-sync.tsx
│ │ │ │ │ ├── user-provider.tsx
│ │ │ │ │ └── wrapped-element-provider.tsx
│ │ │ │ ├── push-notification.ts
│ │ │ │ ├── queries/
│ │ │ │ │ ├── _.ts
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── discover.ts
│ │ │ │ │ ├── entries.ts
│ │ │ │ │ ├── feed.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── messaging.ts
│ │ │ │ │ ├── rsshub.ts
│ │ │ │ │ ├── server-configs.ts
│ │ │ │ │ ├── settings.ts
│ │ │ │ │ ├── types.d.ts
│ │ │ │ │ ├── users.ts
│ │ │ │ │ └── wallet.tsx
│ │ │ │ ├── router.tsx
│ │ │ │ ├── router.web.tsx
│ │ │ │ ├── store/
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── db.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── clear.ts
│ │ │ │ │ ├── helper.test.ts
│ │ │ │ │ └── helper.ts
│ │ │ │ ├── styles/
│ │ │ │ │ ├── additional.css
│ │ │ │ │ ├── base.css
│ │ │ │ │ ├── cursor.css
│ │ │ │ │ ├── font.css
│ │ │ │ │ ├── main.css
│ │ │ │ │ └── scrollbar.css
│ │ │ │ ├── sw.ts
│ │ │ │ ├── wdyr.ts
│ │ │ │ └── workers/
│ │ │ │ └── sw/
│ │ │ │ ├── index.ts
│ │ │ │ └── pusher.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vitest.config.ts
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ └── vite/
│ │ │ ├── ast.ts
│ │ │ ├── cleanup.ts
│ │ │ ├── compress.ts
│ │ │ ├── deps.ts
│ │ │ ├── generate-main-hash.ts
│ │ │ ├── hmr.ts
│ │ │ ├── html-inject.ts
│ │ │ ├── i18n-hmr.ts
│ │ │ ├── locales-json.ts
│ │ │ ├── locales.ts
│ │ │ ├── manifest.ts
│ │ │ ├── specific-import.ts
│ │ │ └── utils/
│ │ │ └── i18n-completeness.ts
│ │ ├── postcss.config.cjs
│ │ ├── resources/
│ │ │ ├── app-update.yml
│ │ │ ├── icon-staging.icns
│ │ │ └── icon.icns
│ │ ├── scripts/
│ │ │ ├── apply-changelog.ts
│ │ │ ├── generate-appx-manifest.ts
│ │ │ ├── merge-yml.ts
│ │ │ └── update-windows-yml.ts
│ │ ├── static/
│ │ │ └── dmg-icon.icns
│ │ ├── tailwind.config.ts
│ │ ├── vite.config.electron-render.ts
│ │ ├── vite.config.ts
│ │ └── wrangler.jsonc
│ ├── landing/
│ │ ├── .prettierrc.mjs
│ │ ├── components.json
│ │ ├── eslint.config.mjs
│ │ ├── global.d.ts
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ └── eslint-recursive-sort.mjs
│ │ ├── postcss.config.mjs
│ │ ├── public/
│ │ │ ├── discover-sources.json
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── ClientInit.tsx
│ │ │ │ ├── InitInClient.ts
│ │ │ │ ├── [locale]/
│ │ │ │ │ ├── download/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── error.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── pricing/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── privacy-policy/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── terms-of-service/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── apple-app-site-association/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── discover-sources/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── globals.css
│ │ │ │ ├── init.ts
│ │ │ │ ├── layout.tsx
│ │ │ │ └── robots.ts
│ │ │ ├── atoms/
│ │ │ │ ├── css-media.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-interactive.ts
│ │ │ │ └── viewport.ts
│ │ │ ├── components/
│ │ │ │ ├── brand/
│ │ │ │ │ ├── Folo.tsx
│ │ │ │ │ └── Logo.tsx
│ │ │ │ ├── common/
│ │ │ │ │ ├── ClientOnly.tsx
│ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ ├── GithubTrending.tsx
│ │ │ │ │ ├── HydrationEndDetector.tsx
│ │ │ │ │ ├── Lazyload.tsx
│ │ │ │ │ ├── LightRays.tsx
│ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ ├── QueryHydrate.tsx
│ │ │ │ │ └── ScrollTop.tsx
│ │ │ │ ├── hoc/
│ │ │ │ │ └── with-no-ssr.tsx
│ │ │ │ ├── layout/
│ │ │ │ │ ├── container/
│ │ │ │ │ │ └── Normal.tsx
│ │ │ │ │ ├── content/
│ │ │ │ │ │ ├── Content.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── footer/
│ │ │ │ │ │ └── Footer.tsx
│ │ │ │ │ └── root/
│ │ │ │ │ └── Root.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── 3d-models/
│ │ │ │ │ │ ├── AISpline.tsx
│ │ │ │ │ │ └── AISplineLoader.tsx
│ │ │ │ │ ├── accordion/
│ │ │ │ │ │ └── Accordion.tsx
│ │ │ │ │ ├── border-beam.tsx
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── Button.tsx
│ │ │ │ │ │ ├── MotionButton.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ ├── Checkbox.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── collapse/
│ │ │ │ │ │ ├── CollapseCss.tsx
│ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dialog/
│ │ │ │ │ │ ├── Dialog.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── Divider.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ └── DropdownMenu.tsx
│ │ │ │ │ ├── effects/
│ │ │ │ │ │ ├── GridGuides.tsx
│ │ │ │ │ │ ├── ParticlesAura.tsx
│ │ │ │ │ │ └── TiltCard.tsx
│ │ │ │ │ ├── glass/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── highlighter.tsx
│ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ ├── HoverCard.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── input/
│ │ │ │ │ │ ├── Input.tsx
│ │ │ │ │ │ ├── TextArea.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── json-highlighter/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── label/
│ │ │ │ │ │ └── Label.tsx
│ │ │ │ │ ├── light-rays.tsx
│ │ │ │ │ ├── loading/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── magic-card.tsx
│ │ │ │ │ ├── markdown/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── modal/
│ │ │ │ │ │ ├── ModalContainer.tsx
│ │ │ │ │ │ ├── ModalManager.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── panel/
│ │ │ │ │ │ └── PanelSplitter.tsx
│ │ │ │ │ ├── portal/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── provider.tsx
│ │ │ │ │ ├── prompts/
│ │ │ │ │ │ ├── BasePrompt.tsx
│ │ │ │ │ │ ├── InputPrompt.tsx
│ │ │ │ │ │ ├── Prompt.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── radio/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── relative-time/
│ │ │ │ │ │ ├── RelativeTime.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── scroll-areas/
│ │ │ │ │ │ ├── ScrollArea.tsx
│ │ │ │ │ │ ├── ctx.ts
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── segment-tab/
│ │ │ │ │ │ ├── SegmentTab.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── select/
│ │ │ │ │ │ ├── ComboboxSelect.tsx
│ │ │ │ │ │ ├── MultiSelect.tsx
│ │ │ │ │ │ ├── Select.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── sheet/
│ │ │ │ │ │ ├── Sheet.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── skeleton/
│ │ │ │ │ │ ├── Skeleton.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── switch/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── theme-switcher/
│ │ │ │ │ │ ├── ThemeSwitcher.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── tooltip/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── transition/
│ │ │ │ │ │ ├── BottomToUpSoftScaleTransitionView.tsx
│ │ │ │ │ │ ├── BottomToUpTransitionView.tsx
│ │ │ │ │ │ ├── FadeInOutTransitionView.tsx
│ │ │ │ │ │ ├── IconTransiton.tsx
│ │ │ │ │ │ ├── LeftToRightTransitionView.tsx
│ │ │ │ │ │ ├── RightToLeftTransitionView.tsx
│ │ │ │ │ │ ├── ScaleTransitionView.tsx
│ │ │ │ │ │ ├── TextUpTransitionView.tsx
│ │ │ │ │ │ ├── factor.tsx
│ │ │ │ │ │ └── typings.ts
│ │ │ │ │ └── viewport/
│ │ │ │ │ ├── OnlyDesktop.tsx
│ │ │ │ │ ├── OnlyMobile.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── widgets/
│ │ │ │ ├── download/
│ │ │ │ │ ├── DownloadHero.tsx
│ │ │ │ │ └── PlatformDownloads.tsx
│ │ │ │ ├── landing/
│ │ │ │ │ ├── Audience.tsx
│ │ │ │ │ ├── BuiltOpen.tsx
│ │ │ │ │ ├── Features.tsx
│ │ │ │ │ ├── Header.tsx
│ │ │ │ │ ├── Hero.tsx
│ │ │ │ │ ├── PromptDemo.tsx
│ │ │ │ │ ├── RepoStats.tsx
│ │ │ │ │ ├── SocialProof.tsx
│ │ │ │ │ ├── TrustedBy.tsx
│ │ │ │ │ ├── ViewsShowcase.tsx
│ │ │ │ │ └── WindowChrome.tsx
│ │ │ │ ├── pricing/
│ │ │ │ │ └── PricingPlans.tsx
│ │ │ │ └── simulators/
│ │ │ │ ├── EntryChatPanel.tsx
│ │ │ │ ├── EntryPage.tsx
│ │ │ │ ├── ListDemo.tsx
│ │ │ │ ├── TimelineChatDemo.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── EntryPageOverlay.tsx
│ │ │ │ │ ├── ai/
│ │ │ │ │ │ ├── AIChainOfThought.tsx
│ │ │ │ │ │ ├── AIMarkdownMessage.tsx
│ │ │ │ │ │ ├── AIReasoningPart.tsx
│ │ │ │ │ │ ├── ToolInvocationComponent.tsx
│ │ │ │ │ │ ├── animated/
│ │ │ │ │ │ │ ├── AnimatedMarkdown.tsx
│ │ │ │ │ │ │ ├── TokenizedText.tsx
│ │ │ │ │ │ │ └── constants.ts
│ │ │ │ │ │ ├── mocks.ts
│ │ │ │ │ │ ├── parse-incomplete-markdown.ts
│ │ │ │ │ │ ├── reasoning-mock.json
│ │ │ │ │ │ └── shiny-text/
│ │ │ │ │ │ ├── ShinyText.tsx
│ │ │ │ │ │ └── index.module.css
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── AiMessageContextBar.tsx
│ │ │ │ │ ├── AiMockMessage.tsx
│ │ │ │ │ ├── AiUserMessage.tsx
│ │ │ │ │ ├── ListChatPlayer.tsx
│ │ │ │ │ ├── MarkdownMessage.tsx
│ │ │ │ │ └── stream.ts
│ │ │ │ └── mocks.tsx
│ │ │ ├── constants/
│ │ │ │ ├── download.ts
│ │ │ │ ├── env.ts
│ │ │ │ ├── site.ts
│ │ │ │ └── spring.ts
│ │ │ ├── hooks/
│ │ │ │ ├── biz/
│ │ │ │ │ └── use-github-star.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── use-before-mounted.ts
│ │ │ │ │ ├── use-click-away.ts
│ │ │ │ │ ├── use-debounce-value.ts
│ │ │ │ │ ├── use-event-callback.ts
│ │ │ │ │ ├── use-input-composition.ts
│ │ │ │ │ ├── use-is-active.ts
│ │ │ │ │ ├── use-is-client.ts
│ │ │ │ │ ├── use-is-dark.ts
│ │ │ │ │ ├── use-is-mounted.ts
│ │ │ │ │ ├── use-is-unmounted.ts
│ │ │ │ │ ├── use-previous.ts
│ │ │ │ │ ├── use-ref-value.ts
│ │ │ │ │ ├── use-safe-setState.ts
│ │ │ │ │ ├── use-state-ref.ts
│ │ │ │ │ ├── use-sync-effect.ts
│ │ │ │ │ └── useMeasure.ts
│ │ │ │ └── shared/
│ │ │ │ └── use-mask-scrollarea.ts
│ │ │ ├── i18n/
│ │ │ │ ├── request.ts
│ │ │ │ └── routing.ts
│ │ │ ├── legal/
│ │ │ │ ├── privacy.md
│ │ │ │ └── tos.md
│ │ │ ├── lib/
│ │ │ │ ├── apple-app-site-association.ts
│ │ │ │ ├── cn.ts
│ │ │ │ ├── color.ts
│ │ │ │ ├── cookie.ts
│ │ │ │ ├── datetime.ts
│ │ │ │ ├── dom.ts
│ │ │ │ ├── env.ts
│ │ │ │ ├── fonts.ts
│ │ │ │ ├── helper.ts
│ │ │ │ ├── jotai.ts
│ │ │ │ ├── landing-data.ts
│ │ │ │ ├── noop.ts
│ │ │ │ ├── platform.ts
│ │ │ │ ├── pricing-data.ts
│ │ │ │ ├── query-client.server.ts
│ │ │ │ ├── scroller.ts
│ │ │ │ ├── sleep.ts
│ │ │ │ ├── spring.ts
│ │ │ │ └── store.ts
│ │ │ ├── messages/
│ │ │ │ ├── en.json
│ │ │ │ ├── jp.json
│ │ │ │ └── zh.json
│ │ │ ├── providers/
│ │ │ │ ├── root/
│ │ │ │ │ ├── debug-provider.tsx
│ │ │ │ │ ├── event-provider.tsx
│ │ │ │ │ ├── framer-lazy-feature.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── jotai-provider.tsx
│ │ │ │ │ ├── page-scroll-info-provider.tsx
│ │ │ │ │ ├── react-query-provider.tsx
│ │ │ │ │ └── sonner.tsx
│ │ │ │ └── shared/
│ │ │ │ ├── LayoutRightSideProvider.tsx
│ │ │ │ └── WrappedElementProvider.tsx
│ │ │ ├── proxy.ts
│ │ │ └── styles/
│ │ │ ├── globals.css
│ │ │ └── pastel-theme-oklch.css
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── worker/
│ │ │ └── index.js
│ │ └── wrangler.jsonc
│ ├── mobile/
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .watchmanconfig
│ │ ├── AGENTS.md
│ │ ├── README.md
│ │ ├── app.config.ts
│ │ ├── assets/
│ │ │ └── font/
│ │ │ └── sn-pro/
│ │ │ ├── SNPro-Black.otf
│ │ │ ├── SNPro-BlackItalic.otf
│ │ │ ├── SNPro-Bold.otf
│ │ │ ├── SNPro-BoldItalic.otf
│ │ │ ├── SNPro-Book.otf
│ │ │ ├── SNPro-BookItalic.otf
│ │ │ ├── SNPro-Heavy.otf
│ │ │ ├── SNPro-HeavyItalic.otf
│ │ │ ├── SNPro-Light.otf
│ │ │ ├── SNPro-LightItalic.otf
│ │ │ ├── SNPro-Medium.otf
│ │ │ ├── SNPro-MediumItalic.otf
│ │ │ ├── SNPro-Regular.otf
│ │ │ ├── SNPro-RegularItalic.otf
│ │ │ ├── SNPro-Semibold.otf
│ │ │ ├── SNPro-SemiboldItalic.otf
│ │ │ ├── SNPro-Thin.otf
│ │ │ └── SNPro-ThinItalic.otf
│ │ ├── babel.config.js
│ │ ├── build/
│ │ │ ├── GoogleService-Info.plist
│ │ │ └── google-services.json
│ │ ├── bump.config.ts
│ │ ├── changelog/
│ │ │ ├── 0.1.3.md
│ │ │ ├── 0.1.4.md
│ │ │ ├── 0.1.5.md
│ │ │ ├── 0.1.6.md
│ │ │ ├── 0.1.7.md
│ │ │ ├── 0.1.8.md
│ │ │ ├── 0.1.9.md
│ │ │ ├── 0.2.0.md
│ │ │ ├── 0.2.1.md
│ │ │ ├── 0.2.10.md
│ │ │ ├── 0.2.2.md
│ │ │ ├── 0.2.3.md
│ │ │ ├── 0.2.4.md
│ │ │ ├── 0.2.5.md
│ │ │ ├── 0.2.6.md
│ │ │ ├── 0.2.8.md
│ │ │ ├── 0.3.0.md
│ │ │ ├── 0.4.0.md
│ │ │ ├── next.md
│ │ │ └── next.template.md
│ │ ├── e2e/
│ │ │ ├── README.md
│ │ │ ├── flows/
│ │ │ │ ├── ios/
│ │ │ │ │ ├── auth.yaml
│ │ │ │ │ ├── content.yaml
│ │ │ │ │ ├── core.yaml
│ │ │ │ │ ├── dismiss-overlays.yaml
│ │ │ │ │ ├── ensure-onboarding-unfollowed.yaml
│ │ │ │ │ ├── follow-onboarding.yaml
│ │ │ │ │ ├── login.yaml
│ │ │ │ │ ├── register.yaml
│ │ │ │ │ ├── sign-out.yaml
│ │ │ │ │ ├── timeline-entry.yaml
│ │ │ │ │ └── unfollow-onboarding.yaml
│ │ │ │ └── shared/
│ │ │ │ ├── core.yaml
│ │ │ │ ├── dismiss-ios-system-modal.yaml
│ │ │ │ ├── ensure-onboarding-unfollowed.yaml
│ │ │ │ ├── follow-onboarding.yaml
│ │ │ │ ├── login.yaml
│ │ │ │ ├── open-auth.yaml
│ │ │ │ ├── register.yaml
│ │ │ │ ├── sign-out.yaml
│ │ │ │ ├── timeline-entry.yaml
│ │ │ │ └── unfollow-onboarding.yaml
│ │ │ └── run-maestro.sh
│ │ ├── eas.json
│ │ ├── global.d.ts
│ │ ├── ios/
│ │ │ ├── .gitignore
│ │ │ ├── .xcode.env
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── black_board_2_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── black_board_2_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── home_5_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── home_5_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── search_3_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── search_3_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── settings_1_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── settings_1_cute_re.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Folo/
│ │ │ │ ├── AppDelegate.swift
│ │ │ │ ├── Folo-Bridging-Header.h
│ │ │ │ ├── Folo.entitlements
│ │ │ │ ├── Images.xcassets/
│ │ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ │ └── Contents.json
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── SplashScreenBackground.colorset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Info.plist
│ │ │ │ ├── PrivacyInfo.xcprivacy
│ │ │ │ ├── SplashScreen.storyboard
│ │ │ │ └── Supporting/
│ │ │ │ └── Expo.plist
│ │ │ ├── Folo - Follow everything.storekit
│ │ │ ├── Folo.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── Folo.xcscheme
│ │ │ ├── Folo.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── Podfile
│ │ │ └── Podfile.properties.json
│ │ ├── metro.config.js
│ │ ├── native/
│ │ │ ├── .eslintrc.js
│ │ │ ├── .gitignore
│ │ │ ├── .npmignore
│ │ │ ├── README.md
│ │ │ ├── expo-module.config.json
│ │ │ ├── ios/
│ │ │ │ ├── Controllers/
│ │ │ │ │ ├── ModalWebViewController.swift
│ │ │ │ │ ├── RNSViewController.swift
│ │ │ │ │ └── WebViewController.swift
│ │ │ │ ├── Extensions/
│ │ │ │ │ ├── UIColor+Hex.swift
│ │ │ │ │ ├── UIImage+asActivityItemSource.swift
│ │ │ │ │ ├── UIImage.swift
│ │ │ │ │ └── UIWindow.swift
│ │ │ │ ├── FollowNative.podspec
│ │ │ │ ├── Models/
│ │ │ │ │ ├── ProfileData.swift
│ │ │ │ │ └── UserData.swift
│ │ │ │ ├── Modules/
│ │ │ │ │ ├── AppleIntelligenceGlowEffect/
│ │ │ │ │ │ ├── AppleIntelligenceGlowEffectModule.swift
│ │ │ │ │ │ ├── IntelligenceAnimationController.swift
│ │ │ │ │ │ └── IntelligenceAnimationView.swift
│ │ │ │ │ ├── Helper/
│ │ │ │ │ │ ├── Helper+Image.swift
│ │ │ │ │ │ └── HelperModule.swift
│ │ │ │ │ ├── ItemPressable/
│ │ │ │ │ │ └── ItemPressableModule.swift
│ │ │ │ │ ├── PagerView/
│ │ │ │ │ │ ├── EnhancePageViewModule.swift
│ │ │ │ │ │ ├── EnhancePagerController.swift
│ │ │ │ │ │ └── EnhancePagerViewModule.swift
│ │ │ │ │ ├── SharedWebView/
│ │ │ │ │ │ ├── FOWebView.swift
│ │ │ │ │ │ ├── FollowImageURLSchemeHandler.swift
│ │ │ │ │ │ ├── Injected/
│ │ │ │ │ │ │ ├── at_end.js
│ │ │ │ │ │ │ └── at_start.js
│ │ │ │ │ │ ├── SharedWebView+BridgeData.swift
│ │ │ │ │ │ ├── SharedWebView.swift
│ │ │ │ │ │ ├── SharedWebViewModule.swift
│ │ │ │ │ │ ├── WebViewManager.swift
│ │ │ │ │ │ └── WebViewState.swift
│ │ │ │ │ ├── StoreKitTestHelper/
│ │ │ │ │ │ └── StoreKitTestHelperModule.swift
│ │ │ │ │ ├── TabBar/
│ │ │ │ │ │ ├── TabBarBottomAccessoryModule.swift
│ │ │ │ │ │ ├── TabBarModule.swift
│ │ │ │ │ │ ├── TabBarPortalModule.swift
│ │ │ │ │ │ ├── TabBarRootView.swift
│ │ │ │ │ │ ├── TabScreenModule.swift
│ │ │ │ │ │ └── TabScreenView.swift
│ │ │ │ │ └── Toaster/
│ │ │ │ │ ├── Toast.swift
│ │ │ │ │ └── ToasterModule.swift
│ │ │ │ ├── Packages/
│ │ │ │ │ ├── ImageViewer_swift/
│ │ │ │ │ │ ├── ImageCarouselViewController.swift
│ │ │ │ │ │ ├── ImageCarouselViewControllerProtocol.swift
│ │ │ │ │ │ ├── ImageItem.swift
│ │ │ │ │ │ ├── ImageLoader.swift
│ │ │ │ │ │ ├── ImageViewerController.swift
│ │ │ │ │ │ ├── ImageViewerOption.swift
│ │ │ │ │ │ ├── ImageViewerTransitionPresentationManager.swift
│ │ │ │ │ │ ├── ImageViewer_swift.h
│ │ │ │ │ │ ├── LISENCE
│ │ │ │ │ │ ├── SimpleImageDatasource.swift
│ │ │ │ │ │ ├── UIImageView_Extensions.swift
│ │ │ │ │ │ ├── UINavigationBar_Extensions.swift
│ │ │ │ │ │ └── UIView_Extensions.swift
│ │ │ │ │ └── SPIndicator/
│ │ │ │ │ └── LICENSE
│ │ │ │ └── Utils/
│ │ │ │ └── Utils.swift
│ │ │ └── package.json
│ │ ├── nativewind-env.d.ts
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ ├── android-trust-user-certs.js
│ │ │ ├── network_security_config.xml
│ │ │ ├── with-android-jdk-21.js
│ │ │ ├── with-android-manifest-plugin.js
│ │ │ ├── with-follow-app-delegate.js
│ │ │ ├── with-follow-assets.js
│ │ │ └── with-gradle-jvm-heap-size-increase.js
│ │ ├── postcss.config.js
│ │ ├── scripts/
│ │ │ ├── apply-changelog.ts
│ │ │ ├── e2e-prod-ios-auth-bootstrap.ts
│ │ │ └── expo-update.ts
│ │ ├── shim-env.d.ts
│ │ ├── src/
│ │ │ ├── @types/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── default-resource.ts
│ │ │ │ └── i18next.d.ts
│ │ │ ├── App.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── app.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ └── useDeviceType.ts
│ │ │ │ ├── server-configs.ts
│ │ │ │ └── settings/
│ │ │ │ ├── data.ts
│ │ │ │ ├── general.ts
│ │ │ │ ├── internal/
│ │ │ │ │ └── helper.ts
│ │ │ │ └── ui.ts
│ │ │ ├── components/
│ │ │ │ ├── common/
│ │ │ │ │ ├── AnimatedComponents.tsx
│ │ │ │ │ ├── Balance.tsx
│ │ │ │ │ ├── BlurEffect.tsx
│ │ │ │ │ ├── CopyButton.tsx
│ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ ├── FullWindowOverlay.ios.tsx
│ │ │ │ │ ├── FullWindowOverlay.tsx
│ │ │ │ │ ├── Link.tsx
│ │ │ │ │ ├── NoLoginInfo.tsx
│ │ │ │ │ ├── RefreshControl.tsx
│ │ │ │ │ ├── RotateableLoading.tsx
│ │ │ │ │ ├── SubmitButton.tsx
│ │ │ │ │ ├── SwipeableItem.tsx
│ │ │ │ │ └── ThemedBlurView.tsx
│ │ │ │ ├── errors/
│ │ │ │ │ ├── GlobalErrorScreen.tsx
│ │ │ │ │ ├── ListErrorView.tsx
│ │ │ │ │ └── ScreenErrorScreen.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── OouiUserAnonymous.tsx
│ │ │ │ │ └── PhUsersBold.tsx
│ │ │ │ ├── layouts/
│ │ │ │ │ ├── contexts/
│ │ │ │ │ │ └── ModalScrollViewContext.ts
│ │ │ │ │ ├── header/
│ │ │ │ │ │ ├── FakeNativeHeaderTitle.tsx
│ │ │ │ │ │ ├── HeaderElements.tsx
│ │ │ │ │ │ ├── NavigationHeader.tsx
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── tabbar/
│ │ │ │ │ │ ├── BottomTabHeightProvider.tsx
│ │ │ │ │ │ ├── BottomTabProvider.tsx
│ │ │ │ │ │ ├── BottomTabs.tsx
│ │ │ │ │ │ ├── ReactNativeTab.ios.tsx
│ │ │ │ │ │ ├── ReactNativeTab.tsx
│ │ │ │ │ │ ├── Tabbar.tsx
│ │ │ │ │ │ ├── contexts/
│ │ │ │ │ │ │ ├── BottomTabBarBackgroundContext.tsx
│ │ │ │ │ │ │ ├── BottomTabBarHeightContext.tsx
│ │ │ │ │ │ │ └── BottomTabBarVisibleContext.tsx
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── views/
│ │ │ │ │ ├── NavigationHeaderContext.tsx
│ │ │ │ │ └── SafeNavigationScrollView.tsx
│ │ │ │ ├── native/
│ │ │ │ │ ├── PagerView/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── specs.ts
│ │ │ │ │ └── webview/
│ │ │ │ │ ├── DebugPanel.tsx
│ │ │ │ │ ├── EntryContentWebView.tsx
│ │ │ │ │ ├── atom.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.android.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── injected-js.ts
│ │ │ │ │ ├── native-webview.android.tsx
│ │ │ │ │ ├── native-webview.tsx
│ │ │ │ │ └── webview-manager.ts
│ │ │ │ └── ui/
│ │ │ │ ├── accordion/
│ │ │ │ │ └── AccordionItem.tsx
│ │ │ │ ├── action-bar/
│ │ │ │ │ └── ActionBarItem.tsx
│ │ │ │ ├── avatar/
│ │ │ │ │ └── UserAvatar.tsx
│ │ │ │ ├── button/
│ │ │ │ │ └── UIBarButton.tsx
│ │ │ │ ├── carousel/
│ │ │ │ │ └── MediaCarousel.tsx
│ │ │ │ ├── context-menu/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── datetime/
│ │ │ │ │ └── RelativeDateTime.tsx
│ │ │ │ ├── form/
│ │ │ │ │ ├── FormProvider.tsx
│ │ │ │ │ ├── Label.tsx
│ │ │ │ │ ├── PickerIos.tsx
│ │ │ │ │ ├── Select.android.tsx
│ │ │ │ │ ├── Select.tsx
│ │ │ │ │ ├── Slider.tsx
│ │ │ │ │ ├── Switch.tsx
│ │ │ │ │ └── TextField.tsx
│ │ │ │ ├── grid/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── grouped/
│ │ │ │ │ ├── GroupedInsetListCardItemStyle.tsx
│ │ │ │ │ ├── GroupedList.tsx
│ │ │ │ │ └── constants.ts
│ │ │ │ ├── icon/
│ │ │ │ │ ├── fallback-icon.tsx
│ │ │ │ │ └── feed-icon.tsx
│ │ │ │ ├── image/
│ │ │ │ │ ├── Image.tsx
│ │ │ │ │ ├── ImageContextMenu.tsx
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── lightbox/
│ │ │ │ │ ├── ImageViewing/
│ │ │ │ │ │ ├── @types/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── ImageDefaultHeader.tsx
│ │ │ │ │ │ │ └── ImageItem/
│ │ │ │ │ │ │ ├── ImageItem.android.tsx
│ │ │ │ │ │ │ ├── ImageItem.ios.tsx
│ │ │ │ │ │ │ └── ImageItem.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── transforms.ts
│ │ │ │ │ ├── Lightbox.tsx
│ │ │ │ │ └── lightboxState.tsx
│ │ │ │ ├── loading/
│ │ │ │ │ └── PlatformActivityIndicator.tsx
│ │ │ │ ├── logo/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── modal/
│ │ │ │ │ ├── BottomModal.tsx
│ │ │ │ │ └── imperative-modal/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── modal.tsx
│ │ │ │ │ └── templates.tsx
│ │ │ │ ├── overlay/
│ │ │ │ │ └── Overlay.tsx
│ │ │ │ ├── pressable/
│ │ │ │ │ ├── IosItemPressable.ios.tsx
│ │ │ │ │ ├── IosItemPressable.tsx
│ │ │ │ │ ├── ItemPressable.ios.tsx
│ │ │ │ │ ├── ItemPressable.tsx
│ │ │ │ │ ├── NativePressable.ios.tsx
│ │ │ │ │ ├── NativePressable.tsx
│ │ │ │ │ ├── NativePressable.types.tsx
│ │ │ │ │ └── enum.ts
│ │ │ │ ├── qrcode/
│ │ │ │ │ ├── LICENSE
│ │ │ │ │ ├── QRCode.tsx
│ │ │ │ │ ├── SVGPieces.tsx
│ │ │ │ │ ├── SVGRadialGradient.tsx
│ │ │ │ │ ├── adapter.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── useQRCodeData.ts
│ │ │ │ ├── slider/
│ │ │ │ │ ├── Slider.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── switch/
│ │ │ │ │ └── Switch.tsx
│ │ │ │ ├── tabview/
│ │ │ │ │ ├── TabBar.tsx
│ │ │ │ │ ├── TabView.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── toast/
│ │ │ │ │ ├── CenteredToast.tsx
│ │ │ │ │ ├── ToastContainer.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ ├── manager.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── typography/
│ │ │ │ │ ├── HtmlWeb.tsx
│ │ │ │ │ ├── MarkdownNative.tsx
│ │ │ │ │ ├── MonoText.tsx
│ │ │ │ │ └── Text.tsx
│ │ │ │ └── video/
│ │ │ │ ├── PlayerAction.tsx
│ │ │ │ └── VideoPlayer.tsx
│ │ │ ├── constants/
│ │ │ │ ├── native-images.ts
│ │ │ │ ├── spring.ts
│ │ │ │ ├── ui.ts
│ │ │ │ └── views.tsx
│ │ │ ├── database/
│ │ │ │ └── index.ts
│ │ │ ├── global.css
│ │ │ ├── hooks/
│ │ │ │ ├── useBackHandler.ts
│ │ │ │ ├── useDefaultHeaderHeight.ts
│ │ │ │ ├── useIntentHandler.ts
│ │ │ │ ├── useLoadingCallback.tsx
│ │ │ │ ├── useMessaging.ts
│ │ │ │ ├── useOnboarding.ts
│ │ │ │ ├── useUnreadCountBadge.ts
│ │ │ │ └── useWebViewNavigation.tsx
│ │ │ ├── icons/
│ │ │ │ ├── AZ_sort_ascending_letters_cute_re.tsx
│ │ │ │ ├── AZ_sort_descending_letters_cute_re.tsx
│ │ │ │ ├── VIP_2_cute_fi.tsx
│ │ │ │ ├── VIP_2_cute_re.tsx
│ │ │ │ ├── add_cute_fi.tsx
│ │ │ │ ├── add_cute_re.tsx
│ │ │ │ ├── ai_cute_fi.tsx
│ │ │ │ ├── ai_cute_re.tsx
│ │ │ │ ├── alert_cute_fi.tsx
│ │ │ │ ├── align_justify_cute_re.tsx
│ │ │ │ ├── align_left_cute_re.tsx
│ │ │ │ ├── announcement_cute_fi.tsx
│ │ │ │ ├── apple_cute_fi.tsx
│ │ │ │ ├── arrow_left_cute_re.tsx
│ │ │ │ ├── arrow_right_circle_cute_fi.tsx
│ │ │ │ ├── arrow_right_up_cute_re.tsx
│ │ │ │ ├── arrow_up_circle_cute_fi.tsx
│ │ │ │ ├── at_cute_re.tsx
│ │ │ │ ├── attachment_cute_re.tsx
│ │ │ │ ├── back_2_cute_re.tsx
│ │ │ │ ├── black_board_2_cute_fi.tsx
│ │ │ │ ├── black_board_2_cute_re.tsx
│ │ │ │ ├── book_6_cute_re.tsx
│ │ │ │ ├── bookmark_cute_re.tsx
│ │ │ │ ├── bubble_cute_fi.tsx
│ │ │ │ ├── bug_cute_re.tsx
│ │ │ │ ├── calendar_time_add_cute_re.tsx
│ │ │ │ ├── celebrate_cute_re.tsx
│ │ │ │ ├── certificate_cute_fi.tsx
│ │ │ │ ├── certificate_cute_re.tsx
│ │ │ │ ├── check_circle_cute_re.tsx
│ │ │ │ ├── check_circle_filled.tsx
│ │ │ │ ├── check_cute_re.tsx
│ │ │ │ ├── check_filled.tsx
│ │ │ │ ├── check_line.tsx
│ │ │ │ ├── classify_2_cute_re.tsx
│ │ │ │ ├── close_circle_fill.tsx
│ │ │ │ ├── close_cute_re.tsx
│ │ │ │ ├── comment_2_cute_re.tsx
│ │ │ │ ├── comment_cute_fi.tsx
│ │ │ │ ├── comment_cute_li.tsx
│ │ │ │ ├── comment_cute_re.tsx
│ │ │ │ ├── compass_3_cute_re.tsx
│ │ │ │ ├── compass_cute_fi.tsx
│ │ │ │ ├── copy_2_cute_re.tsx
│ │ │ │ ├── copy_cute_re.tsx
│ │ │ │ ├── cursor_3_cute_re.tsx
│ │ │ │ ├── danmaku_cute_fi.tsx
│ │ │ │ ├── database.tsx
│ │ │ │ ├── delete_2_cute_re.tsx
│ │ │ │ ├── department_cute_re.tsx
│ │ │ │ ├── discord_cute_fi.tsx
│ │ │ │ ├── docment_cute_fi.tsx
│ │ │ │ ├── docment_cute_re.tsx
│ │ │ │ ├── documents_cute_re.tsx
│ │ │ │ ├── download_2_cute_fi.tsx
│ │ │ │ ├── download_2_cute_re.tsx
│ │ │ │ ├── edit_cute_re.tsx
│ │ │ │ ├── emoji_2_cute_re.tsx
│ │ │ │ ├── exit_cute_fi.tsx
│ │ │ │ ├── exit_cute_re.tsx
│ │ │ │ ├── external_link_cute_re.tsx
│ │ │ │ ├── eye_2_cute_re.tsx
│ │ │ │ ├── eye_close_cute_re.tsx
│ │ │ │ ├── facebook_cute_fi.tsx
│ │ │ │ ├── facebook_cute_re.tsx
│ │ │ │ ├── fast_forward_cute_re.tsx
│ │ │ │ ├── file_import_cute_re.tsx
│ │ │ │ ├── file_upload_cute_re.tsx
│ │ │ │ ├── filter_cute_re.tsx
│ │ │ │ ├── finger_press_cute_re.tsx
│ │ │ │ ├── fire_cute_fi.tsx
│ │ │ │ ├── fire_cute_re.tsx
│ │ │ │ ├── flag_1_cute_fi.tsx
│ │ │ │ ├── folder_open_cute_re.tsx
│ │ │ │ ├── forward_2_cute_re.tsx
│ │ │ │ ├── fullscreen_2_cute_re.tsx
│ │ │ │ ├── fullscreen_cute_re.tsx
│ │ │ │ ├── fullscreen_exit_cute_re.tsx
│ │ │ │ ├── ghost_cute_re.tsx
│ │ │ │ ├── gift_cute_re.tsx
│ │ │ │ ├── github_2_cute_fi.tsx
│ │ │ │ ├── github_cute_fi.tsx
│ │ │ │ ├── google_cute_fi.tsx
│ │ │ │ ├── grid_2_cute_re.tsx
│ │ │ │ ├── grid_cute_re.tsx
│ │ │ │ ├── hammer_cute_re.tsx
│ │ │ │ ├── heart_cute_fi.tsx
│ │ │ │ ├── history_cute_re.tsx
│ │ │ │ ├── home_5_cute_fi.tsx
│ │ │ │ ├── home_5_cute_re.tsx
│ │ │ │ ├── hotkey_cute_re.tsx
│ │ │ │ ├── inbox_cute_fi.tsx
│ │ │ │ ├── inbox_cute_re.tsx
│ │ │ │ ├── info_circle_fill.tsx
│ │ │ │ ├── information_cute_re.tsx
│ │ │ │ ├── instagram_cute_fi.tsx
│ │ │ │ ├── key_2_cute_re.tsx
│ │ │ │ ├── layout_4_cute_re.tsx
│ │ │ │ ├── layout_leftbar_close_cute_re.tsx
│ │ │ │ ├── layout_leftbar_open_cute_re.tsx
│ │ │ │ ├── left_cute_fi.tsx
│ │ │ │ ├── left_small_sharp.tsx
│ │ │ │ ├── line_cute_re.tsx
│ │ │ │ ├── link_cute_re.tsx
│ │ │ │ ├── list_check_2_cute_re.tsx
│ │ │ │ ├── list_check_3_cute_re.tsx
│ │ │ │ ├── list_check_cute_re.tsx
│ │ │ │ ├── list_collapse_cute_fi.tsx
│ │ │ │ ├── list_collapse_cute_re.tsx
│ │ │ │ ├── list_expansion_cute_fi.tsx
│ │ │ │ ├── list_expansion_cute_re.tsx
│ │ │ │ ├── loading_3_cute_li.tsx
│ │ │ │ ├── loading_3_cute_re.tsx
│ │ │ │ ├── love_cute_fi.tsx
│ │ │ │ ├── love_cute_re.tsx
│ │ │ │ ├── magic_2_cute_fi.tsx
│ │ │ │ ├── magic_2_cute_re.tsx
│ │ │ │ ├── mail_cute_re.tsx
│ │ │ │ ├── mic_cute_fi.tsx
│ │ │ │ ├── mic_cute_re.tsx
│ │ │ │ ├── mind_map_cute_re.tsx
│ │ │ │ ├── mingcute_down_line.tsx
│ │ │ │ ├── mingcute_left_line.tsx
│ │ │ │ ├── mingcute_right_line.tsx
│ │ │ │ ├── more_1_cute_re.tsx
│ │ │ │ ├── music_2_cute_fi.tsx
│ │ │ │ ├── notification_cute_re.tsx
│ │ │ │ ├── numbers_09_sort_ascending_cute_re.tsx
│ │ │ │ ├── numbers_09_sort_descending_cute_re.tsx
│ │ │ │ ├── numbers_90_sort_ascending_cute_re.tsx
│ │ │ │ ├── numbers_90_sort_descending_cute_re.tsx
│ │ │ │ ├── palette_cute_fi.tsx
│ │ │ │ ├── palette_cute_re.tsx
│ │ │ │ ├── paper_cute_fi.tsx
│ │ │ │ ├── paste_cute_re.tsx
│ │ │ │ ├── pause_cute_fi.tsx
│ │ │ │ ├── pause_cute_re.tsx
│ │ │ │ ├── pdf_cute_re.tsx
│ │ │ │ ├── photo_album_cute_fi.tsx
│ │ │ │ ├── photo_album_cute_re.tsx
│ │ │ │ ├── pic_cute_fi.tsx
│ │ │ │ ├── pic_cute_re.tsx
│ │ │ │ ├── play_cute_fi.tsx
│ │ │ │ ├── play_cute_re.tsx
│ │ │ │ ├── plugin_2_cute_re.tsx
│ │ │ │ ├── polygon_cute_re.tsx
│ │ │ │ ├── power.tsx
│ │ │ │ ├── power_mono.tsx
│ │ │ │ ├── power_outline.tsx
│ │ │ │ ├── question_cute_re.tsx
│ │ │ │ ├── quill_pen_cute_re.tsx
│ │ │ │ ├── rada_cute_fi.tsx
│ │ │ │ ├── rada_cute_re.tsx
│ │ │ │ ├── refresh_2_cute_re.tsx
│ │ │ │ ├── rewind_backward_15_cute_re.tsx
│ │ │ │ ├── rewind_forward_30_cute_re.tsx
│ │ │ │ ├── right_cute_fi.tsx
│ │ │ │ ├── right_cute_li.tsx
│ │ │ │ ├── right_cute_re.tsx
│ │ │ │ ├── right_small_sharp.tsx
│ │ │ │ ├── rocket_cute_fi.tsx
│ │ │ │ ├── rocket_cute_re.tsx
│ │ │ │ ├── round_cute_fi.tsx
│ │ │ │ ├── round_cute_re.tsx
│ │ │ │ ├── rss_2_cute_fi.tsx
│ │ │ │ ├── rss_cute_fi.tsx
│ │ │ │ ├── sad_cute_re.tsx
│ │ │ │ ├── safe_alert_cute_re.tsx
│ │ │ │ ├── safe_lock_filled.tsx
│ │ │ │ ├── safety_certificate_cute_re.tsx
│ │ │ │ ├── save_cute_re.tsx
│ │ │ │ ├── search_2_cute_re.tsx
│ │ │ │ ├── search_3_cute_fi.tsx
│ │ │ │ ├── search_3_cute_re.tsx
│ │ │ │ ├── search_cute_re.tsx
│ │ │ │ ├── send_plane_cute_fi.tsx
│ │ │ │ ├── send_plane_cute_re.tsx
│ │ │ │ ├── settings_1_cute_fi.tsx
│ │ │ │ ├── settings_1_cute_re.tsx
│ │ │ │ ├── settings_7_cute_re.tsx
│ │ │ │ ├── share_forward_cute_re.tsx
│ │ │ │ ├── shuffle_2_cute_re.tsx
│ │ │ │ ├── social_x_cute_li.tsx
│ │ │ │ ├── social_x_cute_re.tsx
│ │ │ │ ├── sort_ascending_cute_re.tsx
│ │ │ │ ├── sort_descending_cute_re.tsx
│ │ │ │ ├── star_cute_fi.tsx
│ │ │ │ ├── star_cute_re.tsx
│ │ │ │ ├── stop_circle_cute_fi.tsx
│ │ │ │ ├── telegram_cute_fi.tsx
│ │ │ │ ├── telegram_cute_re.tsx
│ │ │ │ ├── thought_cute_fi.tsx
│ │ │ │ ├── time_cute_re.tsx
│ │ │ │ ├── tool_cute_re.tsx
│ │ │ │ ├── train_cute_fi.tsx
│ │ │ │ ├── translate_2_ai_cute_re.tsx
│ │ │ │ ├── translate_2_cute_re.tsx
│ │ │ │ ├── trending_up_cute_re.tsx
│ │ │ │ ├── trophy_cute_fi.tsx
│ │ │ │ ├── trophy_cute_re.tsx
│ │ │ │ ├── twitter_cute_fi.tsx
│ │ │ │ ├── up_cute_re.tsx
│ │ │ │ ├── user_3_cute_fi.tsx
│ │ │ │ ├── user_3_cute_re.tsx
│ │ │ │ ├── user_4_cute_fi.tsx
│ │ │ │ ├── user_4_cute_re.tsx
│ │ │ │ ├── user_add_2_cute_fi.tsx
│ │ │ │ ├── user_heart_cute_fi.tsx
│ │ │ │ ├── user_heart_cute_re.tsx
│ │ │ │ ├── user_setting_cute_fi.tsx
│ │ │ │ ├── user_setting_cute_re.tsx
│ │ │ │ ├── video_cute_fi.tsx
│ │ │ │ ├── video_cute_re.tsx
│ │ │ │ ├── voice_cute_re.tsx
│ │ │ │ ├── volume_cute_re.tsx
│ │ │ │ ├── volume_mute_cute_re.tsx
│ │ │ │ ├── volume_off_cute_re.tsx
│ │ │ │ ├── wallet_2_cute_fi.tsx
│ │ │ │ ├── warning_cute_re.tsx
│ │ │ │ ├── web_cute_re.tsx
│ │ │ │ ├── webhook_cute_re.tsx
│ │ │ │ ├── weibo_cute_re.tsx
│ │ │ │ ├── wifi_off_cute_re.tsx
│ │ │ │ ├── world_2_cute_fi.tsx
│ │ │ │ ├── world_2_cute_re.tsx
│ │ │ │ └── youtube_cute_fi.tsx
│ │ │ ├── initialize/
│ │ │ │ ├── analytics.ts
│ │ │ │ ├── app-check.ts
│ │ │ │ ├── background.ts
│ │ │ │ ├── dayjs.ts
│ │ │ │ ├── device.ts
│ │ │ │ ├── hydrate.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── migration.ts
│ │ │ │ └── player.ts
│ │ │ ├── interfaces/
│ │ │ │ └── settings/
│ │ │ │ └── data.ts
│ │ │ ├── lib/
│ │ │ │ ├── api-client.ts
│ │ │ │ ├── auth-cookie-migration.ts
│ │ │ │ ├── auth.ts
│ │ │ │ ├── client-session.ts
│ │ │ │ ├── dialog-state.ts
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── e2e-config.ts
│ │ │ │ ├── error-parser.ts
│ │ │ │ ├── event-bus.ts
│ │ │ │ ├── ga4.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── image.ts
│ │ │ │ ├── img-proxy.ts
│ │ │ │ ├── jotai.ts
│ │ │ │ ├── kv.ts
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── native/
│ │ │ │ │ ├── index.ios.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── picker.ts
│ │ │ │ │ └── user-agent.ts
│ │ │ │ ├── navigation/
│ │ │ │ │ ├── AttachNavigationScrollViewContext.tsx
│ │ │ │ │ ├── ChainNavigationContext.tsx
│ │ │ │ │ ├── GroupedNavigationRouteContext.ts
│ │ │ │ │ ├── Navigation.ts
│ │ │ │ │ ├── NavigationInstanceContext.ts
│ │ │ │ │ ├── NavigationLink.tsx
│ │ │ │ │ ├── ScreenItemContext.ts
│ │ │ │ │ ├── ScreenNameContext.tsx
│ │ │ │ │ ├── ScreenOptionsContext.ts
│ │ │ │ │ ├── StackNavigation.tsx
│ │ │ │ │ ├── StackScreenHeaderPortal.tsx
│ │ │ │ │ ├── WrappedScreenItem.tsx
│ │ │ │ │ ├── __internal/
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── biz/
│ │ │ │ │ │ └── Destination.ts
│ │ │ │ │ ├── bottom-tab/
│ │ │ │ │ │ ├── BottomTabContext.tsx
│ │ │ │ │ │ ├── TabBarPortal.tsx
│ │ │ │ │ │ ├── TabRoot.tsx
│ │ │ │ │ │ ├── TabScreen.tsx
│ │ │ │ │ │ ├── TabScreenContext.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── native.ios.tsx
│ │ │ │ │ │ ├── native.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── debug/
│ │ │ │ │ │ └── DebugButtonGroup.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── readme.md
│ │ │ │ │ ├── sitemap/
│ │ │ │ │ │ └── registry.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── onboarding.ts
│ │ │ │ ├── parse-api-error.ts
│ │ │ │ ├── payment.ts
│ │ │ │ ├── permission.ts
│ │ │ │ ├── platform.ts
│ │ │ │ ├── player.ts
│ │ │ │ ├── proxy-env.ts
│ │ │ │ ├── query-client.ts
│ │ │ │ ├── responsive.ts
│ │ │ │ ├── secure-store.ts
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── token.ts
│ │ │ │ ├── url-builder.ts
│ │ │ │ └── volume.ts
│ │ │ ├── main.tsx
│ │ │ ├── modules/
│ │ │ │ ├── ai/
│ │ │ │ │ └── summary.tsx
│ │ │ │ ├── context-menu/
│ │ │ │ │ ├── entry.tsx
│ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ ├── inbox.tsx
│ │ │ │ │ ├── lists.tsx
│ │ │ │ │ └── video.tsx
│ │ │ │ ├── debug/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── dialogs/
│ │ │ │ │ ├── ConfirmPasswordDialog.tsx
│ │ │ │ │ ├── ConfirmTOTPCodeDialog.tsx
│ │ │ │ │ ├── MarkAllAsReadDialog.tsx
│ │ │ │ │ └── UpgradeRequiredDialog.tsx
│ │ │ │ ├── discover/
│ │ │ │ │ ├── Category.tsx
│ │ │ │ │ ├── Content.tsx
│ │ │ │ │ ├── DiscoverContent.tsx
│ │ │ │ │ ├── FeedSummary.tsx
│ │ │ │ │ ├── RecommendationListItem.tsx
│ │ │ │ │ ├── Recommendations.tsx
│ │ │ │ │ ├── SearchContent.tsx
│ │ │ │ │ ├── SearchTabBar.tsx
│ │ │ │ │ ├── Trending.tsx
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ ├── search-tabs/
│ │ │ │ │ │ ├── SearchFeed.tsx
│ │ │ │ │ │ ├── SearchFeedCard.tsx
│ │ │ │ │ │ ├── SearchList.tsx
│ │ │ │ │ │ ├── __base.tsx
│ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ └── search.tsx
│ │ │ │ ├── entry-content/
│ │ │ │ │ ├── EntryAISummary.tsx
│ │ │ │ │ ├── EntryContentHeaderRightActions.tsx
│ │ │ │ │ ├── EntryGridFooter.tsx
│ │ │ │ │ ├── EntryNavigationHeader.tsx
│ │ │ │ │ ├── EntryReadHistory.tsx
│ │ │ │ │ ├── EntryTitle.tsx
│ │ │ │ │ ├── ctx.ts
│ │ │ │ │ └── pull-up-navigation/
│ │ │ │ │ ├── PullUpIndicatorAndroid.tsx
│ │ │ │ │ ├── PullUpIndicatorIos.tsx
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── use-pull-up-navigation.android.tsx
│ │ │ │ │ └── use-pull-up-navigation.tsx
│ │ │ │ ├── entry-list/
│ │ │ │ │ ├── EntryListContentArticle.tsx
│ │ │ │ │ ├── EntryListContentPicture.tsx
│ │ │ │ │ ├── EntryListContentSocial.tsx
│ │ │ │ │ ├── EntryListContentVideo.tsx
│ │ │ │ │ ├── EntryListContext.tsx
│ │ │ │ │ ├── EntryListEmpty.tsx
│ │ │ │ │ ├── EntryListFooter.tsx
│ │ │ │ │ ├── EntryListSelector.tsx
│ │ │ │ │ ├── ItemSeparator.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── templates/
│ │ │ │ │ │ ├── EntryNormalItem.tsx
│ │ │ │ │ │ ├── EntryPictureItem.tsx
│ │ │ │ │ │ ├── EntrySocialItem.tsx
│ │ │ │ │ │ ├── EntryTranslation.tsx
│ │ │ │ │ │ └── EntryVideoItem.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── feed/
│ │ │ │ │ ├── FollowFeed.tsx
│ │ │ │ │ └── view-selector.tsx
│ │ │ │ ├── list/
│ │ │ │ │ └── FollowList.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── email.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── social.tsx
│ │ │ │ ├── onboarding/
│ │ │ │ │ ├── feeds-english.json
│ │ │ │ │ ├── feeds.json
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ └── use-reading-behavior.ts
│ │ │ │ │ ├── preset.ts
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ ├── step-finished.tsx
│ │ │ │ │ ├── step-interests.tsx
│ │ │ │ │ ├── step-preferences.tsx
│ │ │ │ │ └── step-welcome.tsx
│ │ │ │ ├── player/
│ │ │ │ │ ├── GlassPlayerTabBar.tsx
│ │ │ │ │ ├── PlayerTabBar.tsx
│ │ │ │ │ ├── context.ts
│ │ │ │ │ ├── control.tsx
│ │ │ │ │ └── hooks.ts
│ │ │ │ ├── review-prompt/
│ │ │ │ │ ├── debug.ts
│ │ │ │ │ ├── provider.tsx
│ │ │ │ │ ├── use-review-prompt-state.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── rsshub/
│ │ │ │ │ └── preview-url.tsx
│ │ │ │ ├── screen/
│ │ │ │ │ ├── PagerList.ios.tsx
│ │ │ │ │ ├── PagerList.tsx
│ │ │ │ │ ├── PagerListContext.ts
│ │ │ │ │ ├── TimelineSelectorList.tsx
│ │ │ │ │ ├── TimelineSelectorProvider.tsx
│ │ │ │ │ ├── TimelineViewSelector.tsx
│ │ │ │ │ ├── TimelineViewSelectorContextMenu.tsx
│ │ │ │ │ ├── action.tsx
│ │ │ │ │ ├── atoms.ts
│ │ │ │ │ └── hooks/
│ │ │ │ │ └── useHeaderHeight.tsx
│ │ │ │ ├── settings/
│ │ │ │ │ ├── SettingsList.tsx
│ │ │ │ │ ├── UserHeaderBanner.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ └── OTPWindow.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── useShareSubscription.tsx
│ │ │ │ │ │ └── useTOTPModalWrapper.tsx
│ │ │ │ │ ├── routes/
│ │ │ │ │ │ ├── 2FASetting.tsx
│ │ │ │ │ │ ├── About.tsx
│ │ │ │ │ │ ├── Account.tsx
│ │ │ │ │ │ ├── Achievement.tsx
│ │ │ │ │ │ ├── Actions.tsx
│ │ │ │ │ │ ├── Appearance.tsx
│ │ │ │ │ │ ├── Data.tsx
│ │ │ │ │ │ ├── EditCondition.tsx
│ │ │ │ │ │ ├── EditProfile.tsx
│ │ │ │ │ │ ├── EditRewriteRules.tsx
│ │ │ │ │ │ ├── EditRule.tsx
│ │ │ │ │ │ ├── EditWebhooks.tsx
│ │ │ │ │ │ ├── Feeds.tsx
│ │ │ │ │ │ ├── General.tsx
│ │ │ │ │ │ ├── Lists.tsx
│ │ │ │ │ │ ├── ManageList.tsx
│ │ │ │ │ │ ├── Notifications.tsx
│ │ │ │ │ │ ├── Plan.tsx
│ │ │ │ │ │ ├── Privacy.tsx
│ │ │ │ │ │ ├── ResetPassword.tsx
│ │ │ │ │ │ └── navigateToPlanScreen.ts
│ │ │ │ │ ├── sync-queue.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ └── subscription/
│ │ │ │ ├── CategoryGrouped.tsx
│ │ │ │ ├── ItemSeparator.tsx
│ │ │ │ ├── SubscriptionLists.tsx
│ │ │ │ ├── UnGroupedList.tsx
│ │ │ │ ├── atoms.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── ctx.ts
│ │ │ │ ├── header-actions.tsx
│ │ │ │ └── items/
│ │ │ │ ├── InboxItem.tsx
│ │ │ │ ├── ListSubscriptionItem.tsx
│ │ │ │ ├── SubscriptionItem.tsx
│ │ │ │ ├── UnreadCount.tsx
│ │ │ │ └── types.tsx
│ │ │ ├── polyfill/
│ │ │ │ ├── index.ts
│ │ │ │ └── promise-with-resolvers.ts
│ │ │ ├── providers/
│ │ │ │ ├── AppleIAPProvider.tsx
│ │ │ │ ├── FontScalingProvider.tsx
│ │ │ │ ├── ServerConfigsLoader.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── migration.tsx
│ │ │ ├── screens/
│ │ │ │ ├── (headless)/
│ │ │ │ │ ├── (debug)/
│ │ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ │ └── text.tsx
│ │ │ │ │ └── DebugScreen.tsx
│ │ │ │ ├── (modal)/
│ │ │ │ │ ├── DiscoverSettingsScreen.tsx
│ │ │ │ │ ├── EditEmailScreen.tsx
│ │ │ │ │ ├── FollowScreen.tsx
│ │ │ │ │ ├── ForgetPasswordScreen.tsx
│ │ │ │ │ ├── ListScreen.tsx
│ │ │ │ │ ├── LoginScreen.tsx
│ │ │ │ │ ├── ProfileScreen.tsx
│ │ │ │ │ ├── RsshubFormScreen.tsx
│ │ │ │ │ ├── TwoFactorAuthScreen.tsx
│ │ │ │ │ └── onboarding/
│ │ │ │ │ ├── EditProfileScreen.tsx
│ │ │ │ │ └── SelectReadingModeScreen.tsx
│ │ │ │ ├── (stack)/
│ │ │ │ │ ├── (tabs)/
│ │ │ │ │ │ ├── discover.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── settings.tsx
│ │ │ │ │ │ └── subscriptions.tsx
│ │ │ │ │ ├── entries/
│ │ │ │ │ │ └── [entryId]/
│ │ │ │ │ │ └── EntryDetailScreen.tsx
│ │ │ │ │ ├── feeds/
│ │ │ │ │ │ └── [feedId]/
│ │ │ │ │ │ └── FeedScreen.tsx
│ │ │ │ │ └── recommendation/
│ │ │ │ │ └── RecommendationCategoryScreen.tsx
│ │ │ │ ├── +native-intent.tsx
│ │ │ │ ├── OnboardingScreen.tsx
│ │ │ │ └── PlayerScreen.tsx
│ │ │ ├── sitemap.tsx
│ │ │ ├── spec/
│ │ │ │ └── typography.ts
│ │ │ ├── store/
│ │ │ │ └── image/
│ │ │ │ ├── hooks.ts
│ │ │ │ └── store.ts
│ │ │ └── theme/
│ │ │ ├── colors.ts
│ │ │ ├── utils.ts
│ │ │ └── web.ts
│ │ ├── tailwind.config.ts
│ │ ├── tailwind.dom.config.ts
│ │ ├── tsconfig.json
│ │ └── web-app/
│ │ ├── html-renderer/
│ │ │ ├── global.d.ts
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── postcss.config.cjs
│ │ │ ├── src/
│ │ │ │ ├── App.tsx
│ │ │ │ ├── HTML.tsx
│ │ │ │ ├── atoms/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ └── WrappedElementProvider.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── __internal/
│ │ │ │ │ │ ├── calculateDimensions.tsx
│ │ │ │ │ │ └── ctx.ts
│ │ │ │ │ ├── heading.tsx
│ │ │ │ │ ├── image.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── link.tsx
│ │ │ │ │ ├── math.tsx
│ │ │ │ │ ├── p.tsx
│ │ │ │ │ └── shiki/
│ │ │ │ │ ├── Shiki.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ └── shiki.module.css
│ │ │ │ ├── index.css
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers/
│ │ │ │ │ └── webview-bridge.ts
│ │ │ │ ├── parser.tsx
│ │ │ │ ├── test.txt
│ │ │ │ └── utils.ts
│ │ │ ├── tailwind.config.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── types/
│ │ │ │ └── index.ts
│ │ │ └── vite.config.mts
│ │ └── package.json
│ └── ssr/
│ ├── .env.example
│ ├── api/
│ │ └── index.ts
│ ├── client/
│ │ ├── @types/
│ │ │ ├── constants.ts
│ │ │ ├── default-resource.ts
│ │ │ └── i18next.d.ts
│ │ ├── App.tsx
│ │ ├── atoms/
│ │ │ ├── server-configs.ts
│ │ │ ├── settings/
│ │ │ │ ├── general.ts
│ │ │ │ └── helper.ts
│ │ │ └── user.ts
│ │ ├── components/
│ │ │ ├── common/
│ │ │ │ ├── 404.tsx
│ │ │ │ └── PoweredByFooter.tsx
│ │ │ ├── items/
│ │ │ │ ├── grid.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── normal.tsx
│ │ │ │ └── picture.tsx
│ │ │ ├── layout/
│ │ │ │ └── header/
│ │ │ │ └── index.tsx
│ │ │ └── ui/
│ │ │ ├── feed-certification.tsx
│ │ │ ├── feed-icon.tsx
│ │ │ ├── image.tsx
│ │ │ └── user-avatar.tsx
│ │ ├── configs.ts
│ │ ├── global.d.ts
│ │ ├── hooks/
│ │ │ └── useRecaptchaToken.ts
│ │ ├── i18n.ts
│ │ ├── index.tsx
│ │ ├── initialize/
│ │ │ ├── helper.ts
│ │ │ ├── index.ts
│ │ │ └── sentry.ts
│ │ ├── lib/
│ │ │ ├── api-fetch.ts
│ │ │ ├── auth.ts
│ │ │ ├── helper.ts
│ │ │ ├── query-client.ts
│ │ │ ├── store.ts
│ │ │ └── url-builder.ts
│ │ ├── modules/
│ │ │ └── login/
│ │ │ └── index.tsx
│ │ ├── pages/
│ │ │ ├── (login)/
│ │ │ │ ├── forget-password.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ ├── register.tsx
│ │ │ │ └── reset-password.tsx
│ │ │ ├── (main)/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── share/
│ │ │ │ ├── feeds/
│ │ │ │ │ └── [id]/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ ├── lists/
│ │ │ │ │ └── [id]/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ └── users/
│ │ │ │ └── [id]/
│ │ │ │ ├── index.tsx
│ │ │ │ └── metadata.ts
│ │ │ └── layout.tsx
│ │ ├── providers/
│ │ │ ├── root-providers.tsx
│ │ │ ├── server-configs-provider.tsx
│ │ │ └── user-provider.tsx
│ │ ├── query/
│ │ │ ├── auth.ts
│ │ │ ├── entries.ts
│ │ │ ├── feed.ts
│ │ │ ├── list.ts
│ │ │ └── users.ts
│ │ ├── router.tsx
│ │ └── styles/
│ │ └── index.css
│ ├── global.ts
│ ├── helper/
│ │ └── meta-map.ts
│ ├── index.html
│ ├── index.ts
│ ├── note.md
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── public/
│ │ └── manifest.json
│ ├── scripts/
│ │ ├── check-fonts.ts
│ │ ├── cleanup-vercel-build.ts
│ │ ├── generate-font-data.ts
│ │ ├── patch-worker-build.ts
│ │ ├── prepare-vercel-build.ts
│ │ ├── skip-ssr-app-vercel-build.sh
│ │ └── upload-fonts-to-r2.ts
│ ├── src/
│ │ ├── global.d.ts
│ │ ├── lib/
│ │ │ ├── api-client.ts
│ │ │ ├── dev-vite.ts
│ │ │ ├── load-env.ts
│ │ │ ├── load-env.worker.ts
│ │ │ ├── not-found.ts
│ │ │ ├── og/
│ │ │ │ ├── fonts.ts
│ │ │ │ ├── fonts.worker.ts
│ │ │ │ ├── render-to-image.ts
│ │ │ │ ├── render-to-image.worker.ts
│ │ │ │ └── resvg-wasm-shim.ts
│ │ │ ├── seo.ts
│ │ │ └── worker-request-context.ts
│ │ ├── meta-handler.map.ts
│ │ ├── meta-handler.ts
│ │ └── router/
│ │ ├── global.ts
│ │ └── og/
│ │ ├── __base.tsx
│ │ ├── feed.tsx
│ │ ├── index.ts
│ │ ├── list.tsx
│ │ └── user.tsx
│ ├── tailwind.config.ts
│ ├── tsconfig.json
│ ├── tsdown.config.ts
│ ├── tsdown.worker.config.ts
│ ├── vercel.json
│ ├── vite.config.mts
│ ├── worker-app.ts
│ ├── worker-entry.ts
│ └── wrangler.jsonc
├── changelogithub.config.ts
├── conductor.json
├── eslint.config.mjs
├── locales/
│ ├── ai/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── app/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── common/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── errors/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── external/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── lang/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── mobile/
│ │ └── default/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── native/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── settings/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ └── shortcuts/
│ ├── en.json
│ ├── fr-FR.json
│ ├── ja.json
│ ├── zh-CN.json
│ └── zh-TW.json
├── package.json
├── packages/
│ ├── configs/
│ │ ├── package.json
│ │ ├── tailwindcss/
│ │ │ ├── ratio-mixing-plugin.js
│ │ │ ├── tailwind-extend.css
│ │ │ ├── tw-css-plugin.js
│ │ │ └── web.ts
│ │ └── tsconfig.extend.json
│ ├── internal/
│ │ ├── AGENTS.md
│ │ ├── atoms/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── atoms/
│ │ │ │ │ └── user.ts
│ │ │ │ └── helper/
│ │ │ │ └── setting.ts
│ │ │ └── tsconfig.json
│ │ ├── components/
│ │ │ ├── assets/
│ │ │ │ ├── colors-media.css
│ │ │ │ ├── colors.css
│ │ │ │ ├── font.css
│ │ │ │ ├── index.css
│ │ │ │ └── tailwind.css
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── atoms/
│ │ │ │ │ ├── mouse.ts
│ │ │ │ │ ├── route.ts
│ │ │ │ │ └── viewport.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── Focusable/
│ │ │ │ │ │ ├── Focusable.tsx
│ │ │ │ │ │ ├── GlobalFocusableProvider.tsx
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── Fragment.ts
│ │ │ │ │ ├── MemoedDangerousHTMLStyle.tsx
│ │ │ │ │ ├── MotionProvider.tsx
│ │ │ │ │ └── ReparentPortal.tsx
│ │ │ │ ├── constants/
│ │ │ │ │ └── spring.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── useMedia.ts
│ │ │ │ │ ├── useMobile.ts
│ │ │ │ │ ├── useMouse.ts
│ │ │ │ │ └── useViewport.ts
│ │ │ │ ├── icons/
│ │ │ │ │ ├── Database.tsx
│ │ │ │ │ ├── Meditation.tsx
│ │ │ │ │ ├── MynauiInboxArchive.tsx
│ │ │ │ │ ├── OouiUserAnonymous.tsx
│ │ │ │ │ ├── PhCloudCheck.tsx
│ │ │ │ │ ├── PhCloudWarning.tsx
│ │ │ │ │ ├── PhCloudX.tsx
│ │ │ │ │ ├── Progress.tsx
│ │ │ │ │ ├── empty.tsx
│ │ │ │ │ ├── follow.tsx
│ │ │ │ │ ├── folo.tsx
│ │ │ │ │ ├── infinify.tsx
│ │ │ │ │ ├── logo.tsx
│ │ │ │ │ ├── nft.tsx
│ │ │ │ │ ├── resize.tsx
│ │ │ │ │ ├── user.tsx
│ │ │ │ │ └── users.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ ├── event-provider.tsx
│ │ │ │ │ └── stable-router-provider.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── auto-resize-height/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── avatar/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── avatar-group/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── action-button.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── interface.ts
│ │ │ │ │ │ └── variants.tsx
│ │ │ │ │ ├── card/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── collapse/
│ │ │ │ │ │ ├── Collapse.tsx
│ │ │ │ │ │ ├── CollapseCss.tsx
│ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── context-menu/
│ │ │ │ │ │ ├── context-menu.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── datetime/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.tsx
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── Divider.tsx
│ │ │ │ │ │ ├── PanelSplitter.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── drop-zone/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── effect/
│ │ │ │ │ │ └── MagneticHoverEffect.tsx
│ │ │ │ │ ├── form/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── icon/
│ │ │ │ │ │ └── SiteIcon.tsx
│ │ │ │ │ ├── input/
│ │ │ │ │ │ ├── DateTimePicker.tsx
│ │ │ │ │ │ ├── Input.tsx
│ │ │ │ │ │ ├── InputV2.tsx
│ │ │ │ │ │ ├── OTP.tsx
│ │ │ │ │ │ ├── TextArea.tsx
│ │ │ │ │ │ ├── TextAreaWrapper.tsx
│ │ │ │ │ │ ├── TimeSelect.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── json-highlighter/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── katex/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── lazy.tsx
│ │ │ │ │ ├── kbd/
│ │ │ │ │ │ └── Kbd.tsx
│ │ │ │ │ ├── key-value-editor/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── label/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── lexical-rich-editor/
│ │ │ │ │ │ ├── LexicalRichEditor.tsx
│ │ │ │ │ │ ├── LexicalRichEditorTextArea.tsx
│ │ │ │ │ │ ├── editor.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── nodes.ts
│ │ │ │ │ │ ├── plugins/
│ │ │ │ │ │ │ ├── code-highlighting/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── exit-code/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── keyboard/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── string-length-change/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── triple-backtick-toggle/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── theme.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── link/
│ │ │ │ │ │ ├── LinkWithTooltip.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── loading/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── markdown/
│ │ │ │ │ │ └── html.tsx
│ │ │ │ │ ├── marquee/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── masonry/
│ │ │ │ │ │ ├── contexts.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── navigation-menu/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── style.ts
│ │ │ │ │ ├── platform-icon/
│ │ │ │ │ │ ├── collections/
│ │ │ │ │ │ │ ├── cubox.tsx
│ │ │ │ │ │ │ ├── eagle.tsx
│ │ │ │ │ │ │ ├── instapaper.tsx
│ │ │ │ │ │ │ ├── obsidian.tsx
│ │ │ │ │ │ │ ├── outline.tsx
│ │ │ │ │ │ │ ├── readeck.tsx
│ │ │ │ │ │ │ ├── readwise.tsx
│ │ │ │ │ │ │ ├── rss3.tsx
│ │ │ │ │ │ │ ├── rsshub.tsx
│ │ │ │ │ │ │ └── zotero.tsx
│ │ │ │ │ │ ├── icons.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.tsx
│ │ │ │ │ ├── popover/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── portal/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── provider.tsx
│ │ │ │ │ ├── progress/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── progressive-blur/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── radio-group/
│ │ │ │ │ │ ├── RadioCard.tsx
│ │ │ │ │ │ ├── RadioGroup.tsx
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── motion.tsx
│ │ │ │ │ ├── scroll-area/
│ │ │ │ │ │ ├── ScrollArea.tsx
│ │ │ │ │ │ ├── ctx.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.module.css
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── segment/
│ │ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── select/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── responsive.tsx
│ │ │ │ │ ├── sheet/
│ │ │ │ │ │ ├── Sheet.tsx
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── shiny-text/
│ │ │ │ │ │ ├── ShinyText.tsx
│ │ │ │ │ │ └── index.module.css
│ │ │ │ │ ├── shrinking-focus-border/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── skeleton/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── slider/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── switch/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── table/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── variants.tsx
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── toast/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── tooltip/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── typography/
│ │ │ │ │ │ ├── EllipsisWithTooltip.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── z-index/
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── dayjs.ts
│ │ │ │ ├── icon.ts
│ │ │ │ ├── parse-markdown.tsx
│ │ │ │ └── selector.tsx
│ │ │ └── tsconfig.json
│ │ ├── constants/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── app.ts
│ │ │ │ ├── auth-providers.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── rsshub.ts
│ │ │ │ ├── social.ts
│ │ │ │ └── tabs.tsx
│ │ │ └── tsconfig.json
│ │ ├── database/
│ │ │ ├── drizzle.config.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── DatabaseSource.js
│ │ │ │ ├── ResourceLock.ts
│ │ │ │ ├── constant.ts
│ │ │ │ ├── db.desktop.ts
│ │ │ │ ├── db.rn.ts
│ │ │ │ ├── db.ts
│ │ │ │ ├── drizzle/
│ │ │ │ │ ├── 0000_harsh_shiva.sql
│ │ │ │ │ ├── 0001_bored_hobgoblin.sql
│ │ │ │ │ ├── 0002_smart_power_man.sql
│ │ │ │ │ ├── 0003_known_roland_deschain.sql
│ │ │ │ │ ├── 0004_majestic_thunderbolt_ross.sql
│ │ │ │ │ ├── 0005_tense_sleepwalker.sql
│ │ │ │ │ ├── 0006_exotic_kid_colt.sql
│ │ │ │ │ ├── 0007_curvy_tarantula.sql
│ │ │ │ │ ├── 0008_last_the_santerians.sql
│ │ │ │ │ ├── 0009_lucky_power_man.sql
│ │ │ │ │ ├── 0010_legal_ben_grimm.sql
│ │ │ │ │ ├── 0011_mysterious_stark_industries.sql
│ │ │ │ │ ├── 0012_magenta_thing.sql
│ │ │ │ │ ├── 0013_chunky_stephen_strange.sql
│ │ │ │ │ ├── 0014_chemical_shocker.sql
│ │ │ │ │ ├── 0015_colorful_warbird.sql
│ │ │ │ │ ├── 0016_curious_carnage.sql
│ │ │ │ │ ├── 0017_talented_captain_cross.sql
│ │ │ │ │ ├── 0018_dashing_the_fury.sql
│ │ │ │ │ ├── 0019_wonderful_shape.sql
│ │ │ │ │ ├── 0020_little_marauders.sql
│ │ │ │ │ ├── 0021_wakeful_onslaught.sql
│ │ │ │ │ ├── 0022_tiny_northstar.sql
│ │ │ │ │ ├── 0023_pink_namor.sql
│ │ │ │ │ ├── 0024_spooky_alex_power.sql
│ │ │ │ │ ├── 0025_colorful_valkyrie.sql
│ │ │ │ │ ├── 0026_numerous_slyde.sql
│ │ │ │ │ ├── 0027_nostalgic_human_torch.sql
│ │ │ │ │ ├── 0028_chief_cyclops.sql
│ │ │ │ │ ├── 0029_flaky_gorgon.sql
│ │ │ │ │ ├── 0030_common_gabe_jones.sql
│ │ │ │ │ ├── 0031_kind_ikaris.sql
│ │ │ │ │ ├── 0032_orange_prima.sql
│ │ │ │ │ ├── 0033_shiny_sebastian_shaw.sql
│ │ │ │ │ ├── 0034_curly_darkstar.sql
│ │ │ │ │ ├── 0035_last_valeria_richards.sql
│ │ │ │ │ ├── 0036_entry_tag_summary.sql
│ │ │ │ │ ├── 0037_bored_the_leader.sql
│ │ │ │ │ ├── meta/
│ │ │ │ │ │ ├── 0000_snapshot.json
│ │ │ │ │ │ ├── 0001_snapshot.json
│ │ │ │ │ │ ├── 0002_snapshot.json
│ │ │ │ │ │ ├── 0003_snapshot.json
│ │ │ │ │ │ ├── 0004_snapshot.json
│ │ │ │ │ │ ├── 0005_snapshot.json
│ │ │ │ │ │ ├── 0006_snapshot.json
│ │ │ │ │ │ ├── 0007_snapshot.json
│ │ │ │ │ │ ├── 0008_snapshot.json
│ │ │ │ │ │ ├── 0009_snapshot.json
│ │ │ │ │ │ ├── 0010_snapshot.json
│ │ │ │ │ │ ├── 0011_snapshot.json
│ │ │ │ │ │ ├── 0012_snapshot.json
│ │ │ │ │ │ ├── 0013_snapshot.json
│ │ │ │ │ │ ├── 0014_snapshot.json
│ │ │ │ │ │ ├── 0015_snapshot.json
│ │ │ │ │ │ ├── 0016_snapshot.json
│ │ │ │ │ │ ├── 0017_snapshot.json
│ │ │ │ │ │ ├── 0018_snapshot.json
│ │ │ │ │ │ ├── 0019_snapshot.json
│ │ │ │ │ │ ├── 0020_snapshot.json
│ │ │ │ │ │ ├── 0021_snapshot.json
│ │ │ │ │ │ ├── 0022_snapshot.json
│ │ │ │ │ │ ├── 0023_snapshot.json
│ │ │ │ │ │ ├── 0024_snapshot.json
│ │ │ │ │ │ ├── 0025_snapshot.json
│ │ │ │ │ │ ├── 0026_snapshot.json
│ │ │ │ │ │ ├── 0027_snapshot.json
│ │ │ │ │ │ ├── 0028_snapshot.json
│ │ │ │ │ │ ├── 0029_snapshot.json
│ │ │ │ │ │ ├── 0030_snapshot.json
│ │ │ │ │ │ ├── 0031_snapshot.json
│ │ │ │ │ │ ├── 0032_snapshot.json
│ │ │ │ │ │ ├── 0033_snapshot.json
│ │ │ │ │ │ ├── 0034_snapshot.json
│ │ │ │ │ │ ├── 0035_snapshot.json
│ │ │ │ │ │ ├── 0036_snapshot.json
│ │ │ │ │ │ ├── 0037_snapshot.json
│ │ │ │ │ │ └── _journal.json
│ │ │ │ │ └── migrations.js
│ │ │ │ ├── migrator.ts
│ │ │ │ ├── schemas/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── services/
│ │ │ │ │ ├── collection.ts
│ │ │ │ │ ├── entry.ts
│ │ │ │ │ ├── feed.ts
│ │ │ │ │ ├── image.ts
│ │ │ │ │ ├── inbox.ts
│ │ │ │ │ ├── internal/
│ │ │ │ │ │ ├── base.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── list.ts
│ │ │ │ │ ├── subscription.ts
│ │ │ │ │ ├── summary.ts
│ │ │ │ │ ├── translation.ts
│ │ │ │ │ ├── unread.ts
│ │ │ │ │ └── user.ts
│ │ │ │ └── types.ts
│ │ │ └── tsconfig.json
│ │ ├── hooks/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── factory/
│ │ │ │ │ └── createHTMLMediaHook.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── internal/
│ │ │ │ │ └── for-theme.ts
│ │ │ │ ├── optimistic/
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── strategies.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── useOptimisticMutation.ts
│ │ │ │ ├── useAnyPointDown.ts
│ │ │ │ ├── useControlled.ts
│ │ │ │ ├── useCountDown.ts
│ │ │ │ ├── useDark.ts
│ │ │ │ ├── useElementWidth.ts
│ │ │ │ ├── useInputComposition.ts
│ │ │ │ ├── useInterval.ts
│ │ │ │ ├── useIsOnline.ts
│ │ │ │ ├── useLongPress.ts
│ │ │ │ ├── useMeasure.ts
│ │ │ │ ├── useOnce.ts
│ │ │ │ ├── usePageVisibility.ts
│ │ │ │ ├── usePrevious.ts
│ │ │ │ ├── useRefValue.ts
│ │ │ │ ├── useSetState.ts
│ │ │ │ ├── useSmoothScroll.ts
│ │ │ │ ├── useSyncTheme.ts
│ │ │ │ ├── useTitle.ts
│ │ │ │ ├── useTriangleMenu.ts
│ │ │ │ ├── useTypescriptHappyCallback.ts
│ │ │ │ └── useVideo.ts
│ │ │ └── tsconfig.json
│ │ ├── logger/
│ │ │ ├── electron.ts
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ └── web.ts
│ │ ├── models/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ └── rsshub.ts
│ │ │ └── tsconfig.json
│ │ ├── shared/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── bridge.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── electron.ts
│ │ │ │ ├── env.common.ts
│ │ │ │ ├── env.desktop.ts
│ │ │ │ ├── env.rn.ts
│ │ │ │ ├── env.ssr.ts
│ │ │ │ ├── event.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── language.ts
│ │ │ │ ├── queue.ts
│ │ │ │ ├── review-prompt.test.ts
│ │ │ │ ├── review-prompt.ts
│ │ │ │ └── settings/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── defaults.ts
│ │ │ │ ├── hook.ts
│ │ │ │ └── interface.ts
│ │ │ └── tsconfig.json
│ │ ├── store/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── @types/
│ │ │ │ │ ├── default-resource.ts
│ │ │ │ │ └── i18next.d.ts
│ │ │ │ ├── constants/
│ │ │ │ │ ├── app.ts
│ │ │ │ │ └── onboarding.ts
│ │ │ │ ├── context.ts
│ │ │ │ ├── hydrate.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ └── stream.ts
│ │ │ │ ├── modules/
│ │ │ │ │ ├── action/
│ │ │ │ │ │ ├── constant.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── collection/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── entry/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── inbox/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── list/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── subscription/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── summary/
│ │ │ │ │ │ ├── enum.ts
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── translation/
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── unread/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ └── user/
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── getters.ts
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── store.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── morph/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── db-store.ts
│ │ │ │ │ └── store-db.ts
│ │ │ │ ├── reset.ts
│ │ │ │ └── types.ts
│ │ │ └── tsconfig.json
│ │ ├── tracker/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── adapters/
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── firebase.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── posthog.ts
│ │ │ │ │ └── proxy.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── manager.ts
│ │ │ │ ├── track-manager.ts
│ │ │ │ ├── tracker-points.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils.ts
│ │ │ └── tsconfig.json
│ │ ├── types/
│ │ │ ├── global.d.ts
│ │ │ ├── package.json
│ │ │ ├── react-global.d.ts
│ │ │ └── vite-env.d.ts
│ │ └── utils/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── attribution.ts
│ │ │ ├── bind-this.ts
│ │ │ ├── chain.ts
│ │ │ ├── cjk.ts
│ │ │ ├── color.ts
│ │ │ ├── data-structure/
│ │ │ │ ├── index.ts
│ │ │ │ └── set.ts
│ │ │ ├── dom.ts
│ │ │ ├── duration.ts
│ │ │ ├── environment.ts
│ │ │ ├── event-bus.rn.ts
│ │ │ ├── event-bus.ts
│ │ │ ├── headers.ts
│ │ │ ├── html.ts
│ │ │ ├── img-proxy.ts
│ │ │ ├── index.ts
│ │ │ ├── jotai.ts
│ │ │ ├── json-codec.ts
│ │ │ ├── language.ts
│ │ │ ├── link-parser.ts
│ │ │ ├── lru-cache.test.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── noop.ts
│ │ │ ├── ns.ts
│ │ │ ├── path-parser.test.ts
│ │ │ ├── path-parser.ts
│ │ │ ├── react.ts
│ │ │ ├── resize.ts
│ │ │ ├── scroller.ts
│ │ │ ├── url-builder.ts
│ │ │ ├── url-for-video.ts
│ │ │ ├── utils.spec.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── readability/
│ ├── bump.config.ts
│ ├── package.json
│ ├── src/
│ │ └── index.ts
│ ├── tsconfig.json
│ └── tsdown.config.ts
├── patches/
│ ├── @microflash__remark-callout-directives.patch
│ ├── @mozilla__readability@0.6.0.patch
│ ├── @pengx17__electron-forge-maker-appimage.patch
│ ├── daisyui@4.12.24.patch
│ ├── re-resizable@6.11.2.patch
│ ├── react-native-sheet-transitions.patch
│ ├── react-native-track-player@4.1.1.patch
│ └── workbox-precaching.patch
├── plugins/
│ ├── eslint/
│ │ ├── eslint-check-i18n-json.js
│ │ ├── eslint-no-debug.js
│ │ ├── eslint-package-json.js
│ │ └── eslint-recursive-sort.js
│ └── utils.js
├── pnpm-workspace.yaml
├── scripts/
│ ├── copy-translation.ts
│ ├── increment-build-id.sh
│ ├── lib.ts
│ ├── mitproxy.py
│ ├── run-proxy.sh
│ ├── skip-main-app-vercel-build.sh
│ ├── svg-to-rn.ts
│ └── update-icon.ts
├── tsconfig.json
├── tsslint.config.ts
├── turbo.json
├── vercel.json
├── vitest.workspace.js
├── vitest.workspace.ts
└── wiki/
└── contribute-i18n.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .agents/settings.local.json
================================================
{
"permissions": {
"allow": [
"Bash(gh pr view:*)",
"Bash(pnpm bump:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(ls:*)",
"Bash(npx:*)",
"Bash(git stash:*)",
"Bash(git show:*)",
"Bash(git -C /Users/diygod/Code/Projects/Folo status --short)",
"Bash(git -C /Users/diygod/Code/Projects/Folo add apps/mobile/changelog/next.md)",
"Bash(git -C /Users/diygod/Code/Projects/Folo commit:*)",
"Bash(git checkout:*)",
"Bash(git fetch:*)",
"Bash(git merge:*)",
"Bash(git rm:*)",
"Bash(git push:*)",
"Bash(gh api:*)",
"Bash(pnpm exec vv:*)"
]
}
}
================================================
FILE: .agents/skills/desktop-release/SKILL.md
================================================
---
name: desktop-release
description: Perform a regular desktop release from the dev branch. Gathers commits since last release, updates changelog, evaluates mainHash changes, bumps version, and creates release PR.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
---
# Desktop Regular Release
Perform a regular desktop release. This skill handles the full release workflow from the `dev` branch.
## Pre-flight checks
1. Confirm the current branch is `dev`. If not, abort with a warning.
2. Run `git pull --rebase` in the repo root to ensure the local branch is up to date.
3. Read `apps/desktop/package.json` to get the current `version` and `mainHash`.
## Step 1: Gather changes since last release
1. Find the last release tag:
```bash
git tag --sort=-creatordate | grep '^desktop/v' | head -1
```
2. Get all commits since that tag on the current branch:
```bash
git log <last-tag>..HEAD --oneline --no-merges
```
3. Categorize commits into:
- **Shiny new things** (feat: commits, new features)
- **Improvements** (refactor:, perf:, chore: improvements, dependency updates)
- **No longer broken** (fix: commits, bug fixes)
- **Thanks** (identify external contributor GitHub usernames from commits)
## Step 2: Update changelog
1. Read `apps/desktop/changelog/next.md`.
2. Present the categorized changes to the user and draft the changelog content.
3. Wait for user confirmation or edits before writing.
4. Write the final content to `apps/desktop/changelog/next.md`, following the template format:
```markdown
# What's new in vNEXT_VERSION
## Shiny new things
- description of new feature
## Improvements
- description of improvement
## No longer broken
- description of fix
## Thanks
Special thanks to volunteer contributors @username for their valuable contributions
```
5. Keep `NEXT_VERSION` as the placeholder - it will be replaced by `apply-changelog.ts` during bump.
## Step 3: Commit changelog updates before bump
`nbump` requires a clean working tree. Commit changelog edits before running bump.
1. Stage the changelog update:
```bash
git add apps/desktop/changelog/next.md
```
2. Commit it on `dev`:
```bash
git commit -m "docs(desktop): prepare release changelog"
```
3. If there are no changes to commit, continue without creating an extra commit.
## Step 4: Evaluate mainHash
This is critical for determining whether users need a full app update or can use the lightweight renderer hot update.
1. Check what files changed in `apps/desktop/layer/main/` since the last release tag:
```bash
git diff <last-tag>..HEAD --name-only -- apps/desktop/layer/main/
```
2. Also check changes to `apps/desktop/package.json` fields other than version/mainHash (since package.json is included in the hash calculation):
```bash
git diff <last-tag>..HEAD -- apps/desktop/package.json
```
**Decision logic:**
- If there are **NO changes** in `layer/main/` and no meaningful `package.json` changes (only version/mainHash/changelog-related), then mainHash should NOT be updated. Users will get a fast renderer-only hot update.
- If there are **trivial changes** in `layer/main/` (typo fixes, comment changes, logging tweaks) that don't affect runtime behavior, recommend NOT updating mainHash. Present the changes to the user and ask for confirmation.
- If there are **meaningful changes** in `layer/main/` (new features, bug fixes, dependency changes, API changes), mainHash MUST be updated. Users will need a full app update.
Present your analysis to the user with:
- List of changed files in `layer/main/`
- A summary of what changed
- Your recommendation (update or skip mainHash)
- Ask for explicit confirmation
## Step 5: Save old mainHash and execute bump
1. Save the current mainHash from `apps/desktop/package.json` for later comparison.
2. Verify working tree is clean before bump:
```bash
git status --short
```
3. Change directory to `apps/desktop/` and run the bump:
```bash
cd apps/desktop && pnpm bump
```
4. This command will:
- Pull latest changes
- Apply changelog (rename next.md to {version}.md, create new next.md)
- Recalculate mainHash and write to package.json
- Format package.json
- Bump minor version
- Commit with message `release(desktop): release v{NEW_VERSION}`
- Create branch `release/desktop/{NEW_VERSION}`
- Push branch and create PR to `main`
## Step 6: Restore mainHash if skipping update
If Step 4 decided mainHash should NOT be updated, restore the old value now. The bump has already committed, pushed, and created the PR on a new release branch, so we amend the commit and force push. This is safe because the release branch was just created.
1. Change back to the repo root first (Step 5 left the working directory at `apps/desktop/`):
```bash
cd ../..
```
2. Ensure you are on the `release/desktop/{NEW_VERSION}` branch (bump should have switched to it).
3. Replace the recalculated mainHash with the saved old value in `apps/desktop/package.json`.
4. Stage and amend the release commit:
```bash
git add apps/desktop/package.json && git commit --amend --no-edit
```
5. Force push the release branch:
```bash
git push --force origin release/desktop/{NEW_VERSION}
```
If Step 4 decided mainHash SHOULD be updated, skip this step entirely — the bump already wrote the correct new value.
## Step 7: Verify
1. Confirm the PR was created successfully by checking the output.
2. Report the new version number and PR URL to the user.
3. Summarize:
- New version: v{NEW_VERSION}
- mainHash updated: yes/no (and why)
- Changelog highlights
- PR URL
## Reference
- Bump config: `apps/desktop/bump.config.ts`
- Changelog dir: `apps/desktop/changelog/`
- Changelog template: `apps/desktop/changelog/next.template.md`
- mainHash generator: `apps/desktop/plugins/vite/generate-main-hash.ts`
- Hot updater logic: `apps/desktop/layer/main/src/updater/hot-updater.ts`
- CI build workflow: `.github/workflows/build-desktop.yml`
- Tag workflow: `.github/workflows/tag.yml`
================================================
FILE: .agents/skills/installing-mobile-preview-builds/SKILL.md
================================================
---
name: installing-mobile-preview-builds
description: Builds and installs the iOS preview build for apps/mobile using EAS local build and devicectl. Use when the user asks to install a preview/internal iOS build on a connected iPhone for production-like testing.
disable-model-invocation: true
allowed-tools: Bash, Read, Glob, Grep
argument-hint: "[device-udid-or-name(optional)]"
---
# Install Mobile Preview Build (iOS)
Use this skill to create a fresh local `preview` iOS build and install it on a connected iPhone.
## Inputs
- Optional `$ARGUMENTS`: device identifier (UDID or exact device name).
- If no argument is provided, auto-select the first paired iPhone from `xcrun devicectl list devices`.
## Workflow
1. Validate repo and tooling.
- Run from repo root and ensure `apps/mobile` exists.
- Verify `pnpm`, `xcrun`, `xcodebuild`, and `eas-cli` are available.
- Verify EAS login:
```bash
cd apps/mobile
pnpm dlx eas-cli whoami
```
2. Resolve target device.
- List paired devices:
```bash
xcrun devicectl list devices
```
- Choose device in this order:
- `$ARGUMENTS` if provided and matches exactly one device.
- Otherwise, first paired iPhone.
3. Trigger local `preview` iOS build.
```bash
mkdir -p .context/preview-install
cd apps/mobile
pnpm dlx eas-cli build -p ios --profile preview --non-interactive --local --output=./build-preview.ipa
cd ../..
cp apps/mobile/build-preview.ipa .context/preview-install/folo-preview.ipa
```
4. Install to device locally.
```bash
unzip -q -o .context/preview-install/folo-preview.ipa -d .context/preview-install/unpacked
APP_PATH=$(find .context/preview-install/unpacked/Payload -maxdepth 1 -name '*.app' -type d | head -n 1)
xcrun devicectl device install app --device "<device-id>" "$APP_PATH"
```
5. Try launching app.
```bash
xcrun devicectl device process launch --device "<device-id>" is.follow --activate
```
- If launch fails due to locked device, instruct the user to unlock iPhone and open `Folo` manually.
## Failure Handling
- If local build fails, report:
- build mode (`local`)
- failing command
- key error message from command output
- If app config fails with `Assets source directory not found ... /out/rn-web`, prebuild assets then retry once:
```bash
pnpm --filter @follow/rn-micro-web-app build --outDir out/rn-web/html-renderer
```
## Output Format
Always return:
1. Build mode (`local`) and final status.
2. Local IPA path.
3. Target device identifier.
4. Install result (`installed` or `failed`) and launch result.
5. Next action for the user if manual action is required.
================================================
FILE: .agents/skills/mobile-e2e/SKILL.md
================================================
---
name: mobile-e2e
description: Run apps/mobile Maestro end-to-end tests in this repo. Use when an agent needs to validate mobile auth flows on iOS Simulator or Android Emulator. Current maintained coverage is register, sign out, and sign in.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
---
# Mobile E2E
Run the mobile Maestro tests for `apps/mobile`.
## Files that matter
- Runner: `apps/mobile/e2e/run-maestro.sh`
- iOS auth flow: `apps/mobile/e2e/flows/ios/auth.yaml`
- Android auth flow: `apps/mobile/e2e/flows/android/core.yaml`
- Shared auth flows: `apps/mobile/e2e/flows/shared/*.yaml`
- Artifacts: `apps/mobile/e2e/artifacts/`
## Always do first
From repo root:
```bash
cd apps/mobile
pnpm run e2e:doctor
pnpm run typecheck
```
## iOS
Use a simulator `.app` build, not an Expo development client.
### Preferred simulator
Prefer the **latest installed iOS runtime** and a **latest-generation iPhone simulator**.
When multiple simulators are available, bias toward the newest iPhone model on the newest installed iOS version.
### Boot simulator
```bash
xcrun simctl boot <IOS_UDID>
xcrun simctl bootstatus <IOS_UDID> -b
open -a Simulator --args -CurrentDeviceUDID <IOS_UDID>
```
### App bundle
`run-maestro.sh` can resolve the app bundle from one of these sources:
- `MAESTRO_IOS_APP_PATH`
- a local `build-*.tar.gz` in `apps/mobile`
- an existing `DerivedData/.../Release-iphonesimulator/Folo.app`
If none of those exist, build one first.
### Build simulator app when missing
If `Folo.app` is not available yet:
```bash
cd apps/mobile/ios
pod install
xcodebuild -workspace Folo.xcworkspace \
-scheme Folo \
-configuration Release \
-sdk iphonesimulator \
-destination 'id=<IOS_UDID>' \
build
```
### Apple Silicon simulator optimization
When running on an Apple Silicon Mac and building only for the simulator used in the current run, prefer compiling only the active `arm64` simulator architecture:
```bash
xcodebuild ... \
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64
```
Use this optimization only for local self-test / e2e simulator builds tied to the current machine. Do not use it when you need a universal simulator app for other machines or when running on Intel Macs.
Expected output pattern:
```bash
~/Library/Developer/Xcode/DerivedData/.../Build/Products/Release-iphonesimulator/Folo.app
```
### Run iOS auth flow
```bash
cd apps/mobile
MAESTRO_IOS_DEVICE_ID=<IOS_UDID> \
MAESTRO_IOS_APP_PATH=<PATH_TO_Folo.app> \
pnpm run e2e:ios
```
## Android
Use a **release APK**, not an Expo development build.
### Java
Use Android Studio bundled JBR:
```bash
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export PATH="$JAVA_HOME/bin:$PATH"
```
### Android SDK
```bash
export ANDROID_HOME="$HOME/Library/Android/sdk"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
```
If `apps/mobile/android/local.properties` is missing, create it with:
```bash
echo "sdk.dir=$HOME/Library/Android/sdk" > apps/mobile/android/local.properties
```
### Build release APK
If `apps/mobile/android` does not exist locally, generate it first with Expo prebuild / run-android tooling.
Then build the release APK:
```bash
cd apps/mobile/android
./gradlew app:assembleRelease --console=plain
```
Expected APK path:
```bash
apps/mobile/android/app/build/outputs/apk/release/app-release.apk
```
### Install to emulator
```bash
adb -s emulator-5554 install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
```
### Run Android auth flow
Start a booted emulator first, then:
```bash
cd apps/mobile
pnpm run e2e:android
```
## Result checks
Successful auth validation means:
- register flow finishes
- sign-out reaches `login-screen`
- login flow makes `login-screen` disappear
## Debugging output
Inspect these folders after a run:
```bash
apps/mobile/e2e/artifacts/ios/
apps/mobile/e2e/artifacts/android/
```
For a one-off focused run, invoke Maestro directly against a single flow and a custom debug directory.
================================================
FILE: .agents/skills/mobile-release/SKILL.md
================================================
---
name: mobile-release
description: Perform a regular mobile release from the dev branch. Gathers commits since last release, updates changelog, bumps version, updates iOS Info.plist, and creates release PR to mobile-main.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
---
# Mobile Regular Release
Perform a regular mobile release. This skill handles the full release workflow from the `dev` branch.
## Pre-flight checks
1. Confirm the current branch is `dev`. If not, abort with a warning.
2. Run `git pull --rebase` in the repo root to ensure the local branch is up to date.
3. Read `apps/mobile/package.json` to get the current `version`.
## Step 1: Gather changes since last release
1. Find the last release tag (both old `mobile@` and new `mobile/v` prefixes exist):
```bash
git tag --sort=-creatordate | grep -E '^mobile[@/]' | head -1
```
2. If no tag found, find the last release commit by matching only the subject line:
```bash
git log --format="%H %s" | grep "^[a-f0-9]* release(mobile): release v" | head -1 | awk '{print $1}'
```
3. Get all commits since the last release on the current branch:
```bash
git log <last-tag-or-commit>..HEAD --oneline --no-merges
```
4. Categorize commits into:
- **Shiny new things** (feat: commits, new features)
- **Improvements** (refactor:, perf:, chore: improvements, dependency updates)
- **No longer broken** (fix: commits, bug fixes)
- **Thanks** (identify external contributor GitHub usernames from commits)
## Step 2: Update changelog
1. Read `apps/mobile/changelog/next.md`.
2. Present the categorized changes to the user and draft the changelog content.
3. Wait for user confirmation or edits before writing.
4. Write the final content to `apps/mobile/changelog/next.md`, following the template format:
```markdown
# What's New in vNEXT_VERSION
## Shiny new things
- description of new feature
## Improvements
- description of improvement
## No longer broken
- description of fix
## Thanks
Special thanks to volunteer contributors @username for their valuable contributions
```
5. Keep `NEXT_VERSION` as the placeholder - it will be replaced by `apply-changelog.ts` during bump.
## Step 3: Commit changelog updates before bump
`nbump` requires a clean working tree. Commit changelog edits before running bump.
1. Stage the changelog update:
```bash
git add apps/mobile/changelog/next.md
```
2. Commit it on `dev`:
```bash
git commit -m "docs(mobile): prepare release changelog"
```
3. If there are no changes to commit, continue without creating an extra commit.
## Step 4: Execute bump
1. Verify working tree is clean before bump:
```bash
git status --short
```
2. Change directory to `apps/mobile/` and run the bump:
```bash
cd apps/mobile && pnpm bump
```
3. This is an interactive `nbump` command that prompts for version selection. It will:
- Pull latest changes
- Apply changelog (rename next.md to {version}.md, create new next.md from template)
- Format package.json with eslint + prettier
- Bump version in `package.json`
- Update `ios/Folo/Info.plist`:
- Set `CFBundleShortVersionString` to the new version
- Increment `CFBundleVersion` (build number) by 1
- Commit with message `release(mobile): release v{NEW_VERSION}`
- Create branch `release/mobile/{NEW_VERSION}`
- Push branch and create PR to `mobile-main`
## Step 5: Verify
1. Confirm the PR was created successfully by checking the output.
2. Report the new version number and PR URL to the user.
3. Summarize:
- New version: v{NEW_VERSION}
- Changelog highlights
- PR URL
## Post-release (manual steps, inform user)
After the release PR is merged to `mobile-main`:
1. **Trigger production builds** via GitHub Actions `workflow_dispatch`:
- Go to "Build iOS" workflow, select `mobile-main` branch, profile = `production`
- Go to "Build Android" workflow, select `mobile-main` branch, profile = `production`
2. Production builds auto-submit to App Store (via `eas submit`) and Google Play (as draft).
3. After submission, go to App Store Connect and Google Play Console to complete the review/release process.
## Reference
- Bump config: `apps/mobile/bump.config.ts`
- Changelog dir: `apps/mobile/changelog/`
- Changelog template: `apps/mobile/changelog/next.template.md`
- Apply changelog script: `apps/mobile/scripts/apply-changelog.ts`
- EAS config: `apps/mobile/eas.json`
- App config: `apps/mobile/app.config.ts`
- iOS Info.plist: `apps/mobile/ios/Folo/Info.plist`
- CI build iOS: `.github/workflows/build-ios.yml`
- CI build Android: `.github/workflows/build-android.yml`
================================================
FILE: .agents/skills/mobile-self-test/SKILL.md
================================================
---
name: mobile-self-test
description: Self-test a mobile feature change or bug fix after implementation in `apps/mobile`. Use this whenever the user asks to verify a mobile change, run simulator acceptance, smoke-test a mobile PR, or provide screenshot proof for a mobile fix. This skill decides between prod vs local API mode, starts the local follow-server when needed, builds a release app, uses Maestro only to bootstrap registration for non-auth work, then switches to screenshot-driven visual validation and returns screenshot evidence.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
---
# Mobile Self Test
Validate a mobile change after implementation.
This skill extends `../mobile-e2e/SKILL.md`. Read that skill first for the baseline doctor checks, iOS simulator boot rules, Java/Android SDK setup, and Maestro artifact conventions. Then apply the extra rules in this skill.
## Files that matter
- Reference skill: `../mobile-e2e/SKILL.md`
- Runner: `apps/mobile/e2e/run-maestro.sh`
- iOS register flow: `apps/mobile/e2e/flows/ios/register.yaml`
- Android register flow: `apps/mobile/e2e/flows/android/register.yaml`
- Shared auth flows: `apps/mobile/e2e/flows/shared/*.yaml`
- Expo config: `apps/mobile/app.config.ts`
- Build profiles: `apps/mobile/eas.json`
- Mobile artifacts: `apps/mobile/e2e/artifacts/`
- Local server repo: `/Users/diygod/Code/Projects/follow-server`
## Default assumptions
- Prefer **iOS simulator** unless the user explicitly asks for Android or the change is Android-specific.
- Default to **prod API mode** when the user did not specify a mode.
- Default to **local API mode** when the task also involves local server changes, backend debugging, or modified files in `/Users/diygod/Code/Projects/follow-server`.
- Keep `EXPO_PUBLIC_E2E_LANGUAGE=en` unless the user explicitly wants another language. The existing Maestro flows assume English UI.
## Simulator and emulator isolation
This section overrides the shared device-selection guidance from `../mobile-e2e/SKILL.md`.
Self-test runs must be isolated because other agents may be using simulators or emulators on the same machine.
- Always create a dedicated temporary simulator or emulator for the current run.
- Never reuse `booted`, an already-running simulator, or a generic Android serial such as `emulator-5554`.
- Record the temporary device name and identifier immediately after creation, then use only that stored identifier for build, install, launch, screenshots, and Maestro.
- Register cleanup before booting the device so the temporary simulator or emulator is deleted even if the test fails midway.
- If cleanup fails, report the leftover device name and identifier in the final response.
## Decide API mode first
Use this decision order:
1. If the user explicitly asks for `prod` or `local`, obey that.
2. Otherwise, if the task depends on local backend changes or local server behavior, use `local`.
3. Otherwise, use `prod`.
Map the chosen mode into the release build:
- `prod` mode: `EXPO_PUBLIC_E2E_ENV_PROFILE=prod`
- `local` mode: `EXPO_PUBLIC_E2E_ENV_PROFILE=local`
Do not silently reuse a build from the other mode. Rebuild the release app when switching between `prod` and `local`.
## Always do first
From repo root:
```bash
cd apps/mobile
pnpm run e2e:doctor
pnpm run typecheck
```
If these fail, stop and report the blocker before attempting simulator work.
## Local server mode
`local` mode requires the local server to be available at `http://localhost:3000`.
Before starting anything, check whether it is already running. Do not start a duplicate server.
```bash
FOLLOW_SERVER_LOG=/tmp/follow-server-dev-core.log
if pgrep -af "pnpm dev:core" >/dev/null 2>&1 || lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then
echo "follow-server already running"
else
(
cd /Users/diygod/Code/Projects/follow-server
nohup pnpm dev:core >"$FOLLOW_SERVER_LOG" 2>&1 &
)
fi
for _ in $(seq 1 60); do
nc -z 127.0.0.1 3000 >/dev/null 2>&1 && break
sleep 2
done
nc -z 127.0.0.1 3000 >/dev/null 2>&1
```
If the task depends on other local surfaces such as `http://localhost:2233`, call that out explicitly instead of pretending the mobile test fully covers it.
## Release build profiles for self-test
Use release-style builds so the test matches user-facing behavior.
- iOS simulator builds: `PROFILE=e2e-ios-simulator`
- Android emulator builds: `PROFILE=e2e-android`
Always pair those with the chosen API mode and language:
```bash
export EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local>
export EXPO_PUBLIC_E2E_LANGUAGE=en
```
## iOS workflow
Do not attach to an existing simulator from `../mobile-e2e/SKILL.md`. Create a dedicated temporary simulator for this run and keep using only its UDID.
### Create a dedicated temporary simulator
Pick the latest available iOS runtime and a recent iPhone device type, then create a temporary simulator.
```bash
IOS_SIM_NAME="CodexSelfTest-$(date +%Y%m%d-%H%M%S)"
IOS_RUNTIME_ID="<latest available iOS runtime identifier from `xcrun simctl list runtimes`>"
IOS_DEVICE_TYPE_ID="<recent iPhone device type identifier from `xcrun simctl list devicetypes`>"
IOS_UDID="$(xcrun simctl create "$IOS_SIM_NAME" "$IOS_DEVICE_TYPE_ID" "$IOS_RUNTIME_ID")"
cleanup_ios_simulator() {
xcrun simctl shutdown "$IOS_UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$IOS_UDID" >/dev/null 2>&1 || true
}
trap cleanup_ios_simulator EXIT
```
Do not switch to another simulator after `IOS_UDID` is created.
### Boot the dedicated simulator
```bash
xcrun simctl boot "$IOS_UDID"
xcrun simctl bootstatus "$IOS_UDID" -b
open -a Simulator --args -CurrentDeviceUDID "$IOS_UDID"
```
If other simulators are already booted, leave them alone and continue using only `IOS_UDID`.
### Build release simulator app
```bash
cd apps/mobile/ios
pod install
PROFILE=e2e-ios-simulator \
EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local> \
EXPO_PUBLIC_E2E_LANGUAGE=en \
xcodebuild -workspace Folo.xcworkspace \
-scheme Folo \
-configuration Release \
-sdk iphonesimulator \
-destination "id=$IOS_UDID" \
clean build
```
On Apple Silicon Macs, when the build is only for the dedicated simulator created for the current self-test run, prefer compiling only the active `arm64` simulator architecture:
```bash
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64
```
Do not use that optimization when you need a universal simulator bundle for other machines or when the host Mac is Intel.
Expected output pattern:
```bash
~/Library/Developer/Xcode/DerivedData/.../Build/Products/Release-iphonesimulator/Folo.app
```
### Install app on simulator
```bash
xcrun simctl install "$IOS_UDID" <PATH_TO_Folo.app>
xcrun simctl launch "$IOS_UDID" is.follow
```
## Android workflow
Reuse the Java and Android SDK setup from `../mobile-e2e/SKILL.md`.
Do not attach to a shared emulator. Create a dedicated temporary AVD for this run and keep using only its recorded serial.
### Create a dedicated temporary AVD
Create a fresh AVD backed by an installed phone system image.
```bash
ANDROID_AVD_NAME="codex-self-test-$(date +%Y%m%d-%H%M%S)"
ANDROID_AVD_PACKAGE="<installed Android system image package>"
ANDROID_AVD_DEVICE="<phone hardware profile>"
avdmanager create avd -n "$ANDROID_AVD_NAME" -k "$ANDROID_AVD_PACKAGE" -d "$ANDROID_AVD_DEVICE" --force
ANDROID_EMULATOR_PORT=""
for port in 5554 5556 5558 5560 5562 5564; do
if ! lsof -nP -iTCP:$port >/dev/null 2>&1 && ! lsof -nP -iTCP:$((port + 1)) >/dev/null 2>&1; then
ANDROID_EMULATOR_PORT="$port"
break
fi
done
[ -n "$ANDROID_EMULATOR_PORT" ] || {
echo "No free Android emulator port found"
exit 1
}
ANDROID_DEVICE_ID="emulator-$ANDROID_EMULATOR_PORT"
cleanup_android_emulator() {
adb -s "$ANDROID_DEVICE_ID" emu kill >/dev/null 2>&1 || true
avdmanager delete avd -n "$ANDROID_AVD_NAME" >/dev/null 2>&1 || true
}
trap cleanup_android_emulator EXIT
```
### Boot the dedicated emulator
```bash
emulator @"$ANDROID_AVD_NAME" -port "$ANDROID_EMULATOR_PORT" -no-snapshot -wipe-data &
adb -s "$ANDROID_DEVICE_ID" wait-for-device
```
If other emulators are already booted, ignore them and continue using only `ANDROID_DEVICE_ID`.
If `apps/mobile/android` does not exist locally, generate it first.
```bash
cd apps/mobile
pnpm expo prebuild android
```
### Build release APK
```bash
cd apps/mobile/android
PROFILE=e2e-android \
EXPO_PUBLIC_E2E_ENV_PROFILE=<prod-or-local> \
EXPO_PUBLIC_E2E_LANGUAGE=en \
./gradlew clean app:assembleRelease --console=plain
```
Expected APK path:
```bash
apps/mobile/android/app/build/outputs/apk/release/app-release.apk
```
### Install app on emulator
```bash
adb -s "$ANDROID_DEVICE_ID" install -r apps/mobile/android/app/build/outputs/apk/release/app-release.apk
adb -s "$ANDROID_DEVICE_ID" shell monkey -p is.follow -c android.intent.category.LAUNCHER 1
```
## Cleanup is mandatory
Delete the temporary simulator or emulator created for the run before returning control to the user.
### iOS cleanup
```bash
xcrun simctl shutdown "$IOS_UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$IOS_UDID" >/dev/null 2>&1 || true
```
### Android cleanup
```bash
adb -s "$ANDROID_DEVICE_ID" emu kill >/dev/null 2>&1 || true
avdmanager delete avd -n "$ANDROID_AVD_NAME" >/dev/null 2>&1 || true
```
Do not leave temporary devices behind for other agents.
## Choose the auth strategy
This is the core difference from `mobile-e2e`.
### A. Change is **not** related to login or registration
Use the existing automated **registration** flow first to bootstrap a clean logged-in account, then do the real verification visually.
Examples:
- timeline behavior
- subscription management
- onboarding content after auth
- settings pages unrelated to sign-in state
- player, reader, share, discover, profile editing
Generate a unique test account before running the flow:
```bash
export E2E_PASSWORD='Password123!'
export E2E_EMAIL="folo-self-test-$(date +%Y%m%d%H%M%S)@example.com"
```
For non-auth iOS self-tests, bootstrap auth through the standard iOS runner mode after the app has been installed and launched once:
```bash
cd apps/mobile
pnpm run e2e:ios:bootstrap
```
This bootstrap path is the default for `prod` and `local` self-tests. Only skip it when the feature under test is login, registration, sign-out, session restoration, or another auth-specific flow that must be validated visually end-to-end.
#### iOS registration bootstrap
```bash
cd apps/mobile
maestro test --format junit --platform ios --device "$IOS_UDID" \
--debug-output e2e/artifacts/ios/register-bootstrap \
-e E2E_EMAIL="$E2E_EMAIL" \
-e E2E_PASSWORD="$E2E_PASSWORD" \
e2e/flows/ios/register.yaml
```
#### Android registration bootstrap
```bash
cd apps/mobile
maestro test --format junit --platform android --device "$ANDROID_DEVICE_ID" \
--debug-output e2e/artifacts/android/register-bootstrap \
-e E2E_EMAIL="$E2E_EMAIL" \
-e E2E_PASSWORD="$E2E_PASSWORD" \
e2e/flows/android/register.yaml
```
After registration succeeds, continue with screenshot-driven visual testing.
### B. Change **is** related to login, registration, logout, session handling, auth validation, or onboarding gates
Do **not** rely on the existing Maestro auth flows for the actual verification. Use a fully visual/manual run instead so the changed UX itself is what gets tested.
Examples:
- register screen changes
- login screen changes
- credential validation changes
- auth toggle changes
- logout behavior
- auth/session restoration
- onboarding shown or hidden based on auth state
For auth-related work:
- create the test account manually through the UI if needed
- use screenshots after every critical step
- verify success and error states visually
- keep a clean record of the exact screen sequence shown to the user
## Screenshot-driven visual testing
Once the app is in the right state, drive the rest of the validation with the visual toolchain available in the current environment. Screenshots are the source of truth for acceptance.
Create a timestamped artifact folder first:
```bash
REPO_ROOT="$(git rev-parse --show-toplevel)"
ARTIFACT_DIR="$REPO_ROOT/apps/mobile/e2e/artifacts/manual/$(date +%Y%m%d-%H%M%S)-<platform>-<prod-or-local>"
mkdir -p "$ARTIFACT_DIR"
```
Capture screenshots after each meaningful checkpoint.
### iOS screenshot command
```bash
xcrun simctl io "$IOS_UDID" screenshot "$ARTIFACT_DIR/<name>.png"
```
### Android screenshot command
```bash
adb -s "$ANDROID_DEVICE_ID" exec-out screencap -p > "$ARTIFACT_DIR/<name>.png"
```
Minimum screenshot set for a complete self-test:
1. entry screen before the changed flow
2. the changed screen or interaction in progress
3. the final success state or the reproduced bug state
Add more screenshots when the flow has multiple important states.
Do not report success without screenshot evidence.
## What to validate visually
Use the screenshots to confirm at least these points when relevant:
- the correct screen is reached
- the changed control, copy, or layout is visible
- loading, empty, error, and success states look correct
- the operation completes without obvious regressions or blocking dialogs
- the app is talking to the intended environment (`prod` or `local`)
If the UI or behavior is ambiguous, capture another screenshot instead of guessing.
## Final user-facing output
The final response must include:
- API mode used and why it was chosen
- platform and dedicated simulator/emulator name plus identifier used
- cleanup result for the temporary simulator/emulator
- whether the local server was reused or started, plus log path if started
- build command used
- whether auth bootstrap was automated or fully visual
- concise step-by-step result summary
- pass/fail conclusion
- screenshot evidence with absolute file paths
If the client supports local image rendering, attach the key screenshots as images in the final message. Otherwise, list the absolute paths clearly so the user can open them.
## Failure handling
- If doctor, typecheck, build, install, or server startup fails, stop and report the exact failing command.
- If `local` mode cannot reach the local server, do not silently fall back to `prod`.
- If the visual flow cannot be completed because the environment lacks the required interaction tooling, report that limitation clearly and still return the screenshots you captured.
================================================
FILE: .agents/skills/update-deps/SKILL.md
================================================
---
name: update-deps
description: Update all dependencies across frontend and backend projects. Reads changelogs for breaking changes, checks affected code, runs tests, and provides a summary. Use when updating npm dependencies across the monorepo.
disable-model-invocation: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Task
---
# Update All Dependencies
Update all npm dependencies across the frontend (current repo) and backend projects. This skill handles the full update workflow including changelog review, code impact analysis, testing, and summarization.
## Step 0: Ask for backend directory
Ask the user for the backend project directory path. Do NOT hardcode any path. Example prompt:
> Please provide the backend project directory path (e.g., `/path/to/backend`).
Wait for the user's response before proceeding. Save the path as `BACKEND_DIR` for later use.
## Step 1: Analyze current dependencies
### Frontend (current repo)
1. Run `pnpm outdated --recursive` in the current repo root to identify all outdated dependencies.
2. Save the full output for later analysis.
### Backend
1. Run `pnpm outdated --recursive` in `BACKEND_DIR` to identify all outdated dependencies.
2. Save the full output for later analysis.
Present the user with a summary of how many dependencies are outdated in each project.
## Step 2: Update dependencies
### Strategy
Update in two phases to isolate issues:
**Phase 1 — Patch and minor updates (safer):**
From the `pnpm outdated` output, identify all dependencies where the update is a patch or minor version bump. Update them in batch:
1. Frontend: For each patch/minor outdated package, run `pnpm update <package>@latest --recursive` in the repo root.
2. Backend: For each patch/minor outdated package, run `pnpm update <package>@latest --recursive` in `BACKEND_DIR`.
> **Why `@latest`?** Both projects use `save-exact=true`, so versions are pinned without `^` or `~`. Without `--latest`, `pnpm update` only resolves within the existing range, which for exact versions is a no-op.
**Phase 2 — Major updates (requires careful review):**
For each dependency with a major version update available:
1. Identify the dependency name, current version, and latest version.
2. **Read the changelog** (Step 3) before updating.
3. Only update after confirming no blocking breaking changes.
4. Use `pnpm update <package>@latest --recursive` to update specific packages.
**Important pnpm workspace notes:**
- The frontend project uses `pnpm catalog` in `pnpm-workspace.yaml` for some shared versions. If a dependency is managed via catalog, update the version in `pnpm-workspace.yaml` instead of individual `package.json` files.
- Both projects use `save-exact=true`, so versions are pinned without `^` or `~`.
- Check `patchedDependencies` in `pnpm-workspace.yaml` or `package.json` — if a patched dependency is being updated, verify the patch still applies or remove it if no longer needed.
## Step 3: Review changelogs for major updates
For each dependency with a **major version update**, you MUST read the changelog before updating.
### How to find changelogs
Use these methods in order of preference:
1. **npm registry**: Run `npm view <package> repository.url` to find the repo, then check for `CHANGELOG.md` or GitHub releases.
2. **GitHub releases**: Search `https://github.com/<owner>/<repo>/releases` using WebFetch.
3. **Web search**: Use WebSearch to find `<package> changelog <old-version> to <new-version>`.
### What to look for
- **Breaking changes**: API removals, renamed exports, changed defaults, dropped Node.js version support.
- **Deprecated features**: Features being removed in future versions.
- **Migration guides**: Official upgrade instructions.
- **Peer dependency changes**: New or changed peer dependency requirements.
### Document findings
For each major update, record:
- Package name and version change (e.g., `foo: 2.x → 3.x`)
- Breaking changes summary
- Whether our code is affected (and how)
## Step 4: Check affected code
For each dependency with breaking changes identified in Step 3:
1. Use `Grep` to find all imports and usages of the affected package across the relevant project (frontend or backend).
2. Read the files containing usages.
3. Compare the usage against the breaking change description.
4. If our code uses an affected API:
- Attempt to fix the code following the migration guide.
- If the fix is complex or risky, **skip updating this dependency** and note it in the summary.
5. If our code does NOT use any affected API, proceed with the update.
## Step 5: Run tests and checks
After all updates are applied:
### Frontend
Run these commands sequentially in the repo root and capture results:
```bash
pnpm install
pnpm typecheck
pnpm test
pnpm lint
```
### Backend
Run these commands sequentially in `BACKEND_DIR` and capture results:
```bash
pnpm install
pnpm typecheck
pnpm test
pnpm lint
```
### Handle failures
- **TypeScript errors**: Read the error output, identify which updated dependency caused the issue, and fix the type errors. If unfixable, revert that specific dependency update.
- **Test failures**: Analyze the failure, check if it's related to a dependency update, and fix or revert.
- **Lint errors**: Run `pnpm lint:fix` first. If issues persist, fix manually or revert the causing update.
Repeat the test cycle until all checks pass.
## Step 6: Summary
Present the user with a comprehensive summary:
### Update report
```
## Dependencies Updated
### Frontend
- <package>: <old-version> → <new-version> (patch/minor/major)
- ...
### Backend
- <package>: <old-version> → <new-version> (patch/minor/major)
- ...
## Skipped Updates (with reasons)
- <package>: <reason why not updated>
- ...
## Key Changelog Highlights
### Breaking Changes Applied
- <package> <version>: <what changed and how we adapted>
### Notable New Features
- <package> <version>: <brief description>
### Deprecation Warnings
- <package> <version>: <what's deprecated and timeline>
## Test Results
- Frontend typecheck: ✅/❌
- Frontend tests: ✅/❌
- Frontend lint: ✅/❌
- Backend typecheck: ✅/❌
- Backend tests: ✅/❌
- Backend lint: ✅/❌
```
Ask the user if they want to commit the changes.
================================================
FILE: .cursorignore
================================================
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
*.splinecode filter=lfs diff=lfs merge=lfs -text
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐞 Bug report
description: Report an issue
labels: [pending triage, bug]
type: Bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: platform
attributes:
label: Platform
description: On which platforms does this bug occur?
multiple: true
options:
- Desktop - macOS
- Desktop - Windows
- Desktop - Linux
- Desktop - Web
- Mobile - iOS
- Mobile - Android
- Mobile - Web
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
placeholder: Bug description
validations:
required: true
- type: input
id: entryId
attributes:
label: Entry ID
description: Please provide the entry id of the entry that is causing the issue. If you are not sure, please provide the entry url.
- type: textarea
id: relevant-information
attributes:
label: Relevant Information
description: Please provide your user id, feed id, feed url, or any other information that can help us reproduce the issue.
placeholder: User ID, Feed ID, Feed URL, etc.
validations:
required: false
- type: textarea
id: reproduction
attributes:
label: Reproduction Video
description: If possible, please provide a video that demonstrates the bug.
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment
description: Please provide the environment in which you are using the application. You can find this information by going to Preferences > About and clicking the copy button next to the version tag.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
required: true
- label: Check that this is a concrete bug. For Q&A, please open a GitHub Discussion instead.
required: true
- label: This issue is valid
required: true
- type: checkboxes
id: contributions
attributes:
label: Contributions
description: Please note that Open Source projects are maintained by volunteers, where your cases might not be always relevant to the others. It would make things move faster if you could help investigate and propose solutions.
options:
- label: I am willing to submit a PR to fix this issue
- label: I am willing to submit a PR with failing tests (actually just go ahead and do it, thanks!)
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 💬 Follow's Discord Server
url: https://discord.gg/tUDVZjEr
about: Want to discuss / chat with the community? Here you go!
- name: Discuss an issue
url: https://github.com/RSSNext/Follow/discussions
about: For general questions, ideas, or non-bug related discussions, please use GitHub Discussions.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 🚀 New feature proposal
description: Propose a new feature
labels: [enhancement]
type: Feature
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "As a developer using this project I want [goal / wish] so that [benefit]. If you intend to submit a PR for this issue, tell us in the description. Thanks!"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
- label: This issue is valid
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/i18n.yml
================================================
name: 🌐 Internationalization (i18n)
description: Contribute to or report issues with translations
title: "[i18n]: "
labels: ["i18n", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to contribute to our internationalization efforts!
Before proceeding, please check our [i18n Contribution Guidelines](https://github.com/RSSNext/Follow/blob/dev/wiki/contribute-i18n.md) for detailed instructions.
- type: dropdown
id: type
attributes:
label: Type of i18n contribution
options:
- New language support
- Update existing translations
- Report incorrect translation
- Other i18n-related issue
validations:
required: true
- type: input
id: language
attributes:
label: Language
description: What language are you contributing to or reporting about?
placeholder: e.g., Spanish, French, Japanese
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Please provide details about your contribution or the issue you're reporting.
placeholder: |
For new languages: List any specific challenges or considerations.
For updates: Describe what you're changing and why.
For issues: Provide the incorrect translation and suggest a correction.
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context, screenshots, or file references here.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com/code-of-conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true
- label: This issue is valid
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/typo.yml
================================================
name: 👀 Typo / Grammar fix
description: You can just go ahead and send a PR! Thank you!
labels: []
body:
- type: markdown
attributes:
value: |
## PR Welcome!
If the typo / grammar issue is trivial and straightforward, you can help by **directly sending a quick pull request**!
If you spot multiple of them, we suggest combining them into a single PR. Thanks!
- type: textarea
id: context
attributes:
label: Additional context
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: This issue is valid
required: true
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- DO NOT IGNORE THE TEMPLATE!
Thank you for contributing!
Before submitting the PR, please make sure you do the following:
- Read the [Contributing Guide](/contribute).
- Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
- Ideally, include relevant tests that fail without this PR but pass with it.
-->
### Description
<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
### PR Type
<!-- Please check the type of PR: -->
- [ ] Feature
- [ ] Bugfix
- [ ] Hotfix
- [ ] Other (please describe):
### Screenshots (if UI change)
### Demo Video (if new feature)
### Linked Issues
### Additional context
<!-- e.g. is there anything you'd like reviewers to focus on? -->
### Changelog
<!-- Please ensure the changelog/next.md is updated if this is a feature or hotfix: -->
- [ ] I have updated the changelog/next.md with my changes.
================================================
FILE: .github/actions/setup-version/action.yml
================================================
name: Setup Version
description: "Setup Version"
inputs:
type:
required: true
description: "Type of the app, either 'desktop' or 'mobile'"
default: "desktop"
outputs:
APP_VERSION:
description: "App Version"
value: ${{ steps.version.outputs.APP_VERSION }}
runs:
using: "composite"
steps:
- name: "Write Version"
id: version
shell: bash
run: |
if [ "${{ github.ref_type }}" == "tag" ]; then
# Handle new tag format: desktop/v1.2.3 or mobile/v1.2.3
if [[ "${{ github.ref_name }}" =~ ^(desktop|mobile)/v(.*)$ ]]; then
APP_VERSION="${BASH_REMATCH[2]}"
else
# Fallback for old format: v1.2.3
APP_VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
fi
else
if [ "${{ inputs.type }}" == "desktop" ]; then
APP_VERSION=$(node -p "require('./apps/desktop/package.json').version")
else
APP_VERSION=$(node -p "require('./apps/mobile/package.json').version")
fi
fi
echo $APP_VERSION
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_OUTPUT"
================================================
FILE: .github/actions/setup-xcode/action.yml
================================================
name: "Setup Xcode"
description: "Setup specific Xcode version for iOS builds"
inputs:
xcode-version:
description: "Xcode version to use"
required: false
default: "26.0.1"
runs:
using: "composite"
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ inputs.xcode-version }}
- name: Setup Xcode Environment
run: |
set -e
echo "=== Checking CI Environment ==="
echo "Current user: $(whoami)"
echo "User groups: $(groups)"
echo "Sudo available: $(sudo -n true 2>/dev/null && echo 'YES' || echo 'NO')"
echo "Xcode path: $(xcode-select -p 2>/dev/null || echo 'Not set')"
# Set Xcode path and accept license
echo "=== Setting up Xcode ==="
if command -v sudo >/dev/null && sudo -n true 2>/dev/null; then
sudo xcode-select -s /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer
sudo xcodebuild -license accept
echo "Using sudo for Xcode setup"
else
xcode-select -s /Applications/Xcode_${{ inputs.xcode-version }}.app/Contents/Developer
xcodebuild -license accept
echo "Using direct access for Xcode setup"
fi
echo "Final Xcode path: $(xcode-select -p)"
shell: bash
- name: Start Simulator Service
run: |
echo "=== Starting Simulator Service ==="
# Try to start simulator service (ignore errors if already running)
sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.CoreSimulator.CoreSimulatorService.plist 2>/dev/null || {
echo "CoreSimulator service might already be running or command unavailable"
}
# Alternative for newer macOS versions
sudo launchctl bootstrap system /System/Library/LaunchDaemons/com.apple.CoreSimulator.CoreSimulatorService.plist 2>/dev/null || {
echo "Bootstrap command failed or service already running"
}
# Wait a moment for service to start
sleep 3
shell: bash
- name: Download iOS Platform
run: |
set -e
echo "=== Checking Available SDKs ==="
echo "Currently available SDKs:"
xcodebuild -showsdks | grep -E "(iOS|Simulator)" || echo "No iOS SDKs found yet"
echo "=== Attempting iOS Platform Download ==="
# Method 1: Try standard download
if xcodebuild -downloadPlatform iOS -quiet 2>/dev/null; then
echo "✅ iOS platform downloaded successfully with xcodebuild"
elif xcrun xcodebuild -downloadPlatform iOS -quiet 2>/dev/null; then
echo "✅ iOS platform downloaded successfully with xcrun xcodebuild"
else
echo "⚠️ Platform download failed, checking if iOS SDK is already available..."
# Check if iOS SDK is available
if xcodebuild -showsdks | grep -q "iOS"; then
echo "✅ iOS SDK is already available, continuing..."
else
echo "❌ No iOS SDK found and download failed"
echo "Available SDKs:"
xcodebuild -showsdks
# Try alternative download method
echo "Trying alternative download method..."
xcodebuild -downloadAllPlatforms -quiet || {
echo "❌ All download methods failed"
exit 1
}
fi
fi
echo "=== Final SDK Status ==="
echo "Available iOS SDKs:"
xcodebuild -showsdks | grep -E "(iOS|Simulator)" || echo "No iOS SDKs found"
echo "Available simulators:"
xcrun simctl list devices available | head -20 || echo "No simulators found"
shell: bash
- name: Verify Setup
run: |
echo "=== Verification ==="
echo "Xcode version: $(xcodebuild -version | head -1)"
echo "Selected Xcode: $(xcode-select -p)"
echo "iOS SDK available: $(xcodebuild -showsdks | grep -c iOS || echo 0)"
# Test basic functionality
if xcodebuild -showsdks | grep -q "iOS"; then
echo "✅ Setup completed successfully!"
else
echo "⚠️ Setup completed but iOS SDK may not be fully available"
fi
shell: bash
================================================
FILE: .github/advanced-issue-labeler.yml
================================================
policy:
- section:
- id: [platform]
block-list: ["None", "Other"]
label:
- name: "platform: desktop"
keys: ["Desktop - macOS", "Desktop - Windows", "Desktop - Linux", "Desktop - Web"]
- name: "platform: mobile"
keys: ["Mobile - iOS", "Mobile - Android", "Mobile - Web"]
================================================
FILE: .github/copilot-instructions.md
================================================
Before you start, you need to read and follow the rules in @../CLAUDE.md
================================================
FILE: .github/dependabot.yaml
================================================
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: friday
time: "12:00"
timezone: Asia/Singapore
target-branch: dev
ignore:
- dependency-name: "@shopify/flash-list"
versions: [">1.7.3"]
# Stuck by tailwindcss 4
- dependency-name: tailwindcss
versions: [">=4.0.0"]
- dependency-name: daisyui
versions: [">=5.0.0"]
# Stuck by expo 52
- dependency-name: react-native
versions: [">=0.78.0"]
# It's using export map and metro doesn't support it well
- dependency-name: unist-util-visit-parents
versions: [">=6.0.0"]
# electron 35
- dependency-name: electron
versions: [">=35.0.0"]
- dependency-name: react-native-sheet-transitions
versions: [">0.1.2"]
# filter not work
- dependency-name: unplugin-ast
versions: ["0.14.5"]
open-pull-requests-limit: 100
groups:
minor-and-patch:
applies-to: version-updates
update-types:
- "minor"
- "patch"
pathed:
patterns:
- immer
- re-resizable
- electron-context-menu
- "@mozilla/readability"
- daisyui
- jsonpointer
- workbox-precaching
- "@pengx17/electron-forge-maker-appimage"
- "@microflash/remark-callout-directives"
- react-native-track-player
- react-native-sheet-transitions
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
target-branch: dev
open-pull-requests-limit: 100
================================================
FILE: .github/prompts/similar_issues.prompt.yml
================================================
messages:
- role: system
content: |-
You are a GitHub assistant with access to GitHub Model Context Protocol (MCP)
tools in read-only mode. Your task is to search this repository's issues to find
previously filed issues similar to the provided issue title and body. Use the
GitHub tools via MCP to perform the search and retrieve real issue data (do not
fabricate results).
Consider semantic similarity across title and body. Exclude
issues with the same ID/number as the current issue. Return up to 3 of the most similar past issues. If none are
reasonably similar, return an empty list. Output must follow the response schema
exactly and include only data you actually retrieved from GitHub tools.
The current GitHub repository is: "{{repository}}".
- role: user
content: |-
Find similar issues for this new issue:
Title: {{issue_title}}
Body:
{{issue_body}}
model: openai/gpt-4.1-mini
responseFormat: json_schema
jsonSchema: |-
{
"name": "similar_issues_result",
"strict": true,
"schema": {
"type": "object",
"properties": {
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"number": { "type": "integer" },
"title": { "type": "string" },
"url": { "type": "string" },
"similarity_score": { "type": "number", "minimum": 0, "maximum": 1 }
},
"required": ["number", "title", "url", "similarity_score"],
"additionalProperties": false
}
}
},
"required": ["matches"],
"additionalProperties": false
}
}
================================================
FILE: .github/scripts/extract-release-info.mjs
================================================
#!/usr/bin/env node
/**
* Extract release version and platform information from git commit messages
* Used by GitHub Actions to determine if a release tag should be created
*/
import { execSync } from "node:child_process"
import { appendFileSync } from "node:fs"
// Configuration
const RELEASE_PATTERNS = {
desktop: /release\(desktop\): Release (v\d+\.\d+\.\d+(-[0-9A-Z-.]+)?)/i,
mobile: /release\(mobile\): Release (v\d+\.\d+\.\d+(-[0-9A-Z-.]+)?)/i,
}
const EXIT_CODES = {
SUCCESS: 0,
GIT_ERROR: 2,
ENV_ERROR: 3,
OUTPUT_ERROR: 4,
}
/**
* Write environment variable to GitHub Environment
* @param {string} key - Environment variable key
* @param {string} value - Environment variable value
*/
function setGitHubEnv(key, value) {
try {
if (!process.env.GITHUB_ENV) {
throw new Error("GITHUB_ENV not set - not running in GitHub Actions")
}
appendFileSync(process.env.GITHUB_ENV, `${key}=${value}\n`)
} catch (error) {
console.error(`Failed to set environment variable ${key}:`, error.message)
process.exit(EXIT_CODES.ENV_ERROR)
}
}
/**
* Write output variable to GitHub Output
* @param {string} key - Output key
* @param {string} value - Output value
*/
function setGitHubOutput(key, value) {
try {
if (!process.env.GITHUB_OUTPUT) {
return
}
appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`)
} catch (error) {
console.error(`Failed to set output variable ${key}:`, error.message)
process.exit(EXIT_CODES.OUTPUT_ERROR)
}
}
/**
* Get the latest commit message
* @returns {string} Latest commit message
*/
function getLatestCommitMessage() {
try {
return execSync("git log -1 --pretty=%B", { encoding: "utf-8" }).toString().trim()
} catch (error) {
console.error("Failed to get git commit message:", error.message)
process.exit(EXIT_CODES.GIT_ERROR)
}
}
/**
* Extract release information from commit message
* @param {string} commitMessage - Git commit message
* @returns {Object|null} Release information or null if no release found
*/
function extractReleaseInfo(commitMessage) {
for (const [platform, regex] of Object.entries(RELEASE_PATTERNS)) {
const match = commitMessage.match(regex)
if (match) {
const version = match[1]
const tagName = `${platform}/${version}`
return {
platform,
version,
tagName,
}
}
}
return null
}
/**
* Main execution function
*/
function main() {
try {
console.info("Extracting release information from commit message...")
const commitMessage = getLatestCommitMessage()
console.info(`Commit message: ${commitMessage}`)
const releaseInfo = extractReleaseInfo(commitMessage)
if (!releaseInfo) {
console.info("No desktop or mobile release found in commit message.")
process.exit(EXIT_CODES.SUCCESS)
}
const { platform, version, tagName } = releaseInfo
// Set GitHub Environment variables
setGitHubEnv("tag_version", tagName)
setGitHubEnv("platform", platform)
setGitHubEnv("version", version)
setGitHubOutput("tag_version", tagName)
setGitHubOutput("platform", platform)
setGitHubOutput("version", version)
console.info(`Found ${platform} release: ${version}`)
console.info(`Tag will be created: ${tagName}`)
process.exit(EXIT_CODES.SUCCESS)
} catch (error) {
console.error("Unexpected error:", error.message)
process.exit(EXIT_CODES.GIT_ERROR)
}
}
main()
================================================
FILE: .github/workflows/build-android.yml
================================================
name: 🤖 Build Android
on:
push:
branches:
- "**"
paths:
- "apps/mobile/**"
- "pnpm-lock.yaml"
workflow_dispatch:
inputs:
profile:
type: choice
default: preview
options:
- preview
- production
description: "Build profile"
release:
type: boolean
default: false
description: "Create a release draft for the build"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.profile }}
cancel-in-progress: true
jobs:
build:
name: Build Android apk for device
if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(mobile):'))
runs-on: ubuntu-latest
steps:
- name: 🧹 Claim disk space
run: |
df -h /
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/.ghcup
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/julia*
df -h /
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "pnpm"
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: "17"
distribution: "zulu"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build Android app
working-directory: apps/mobile
run: eas build --platform android --profile ${{ github.event.inputs.profile || 'preview' }} --local --output=${{ github.workspace }}/build.${{ github.event.inputs.profile == 'production' && 'aab' || 'apk' }}
- name: 📤 Upload apk Artifact
if: github.event.inputs.profile != 'production'
uses: actions/upload-artifact@v7
with:
name: app-android
path: ${{ github.workspace }}/build.apk
retention-days: 90
- name: 📤 Upload aab Artifact
if: github.event.inputs.profile == 'production'
uses: actions/upload-artifact@v7
with:
name: aab-android
path: ${{ github.workspace }}/build.aab
retention-days: 90
- name: Submit to Google Play
if: github.event.inputs.profile == 'production'
working-directory: apps/mobile
run: eas submit --platform android --path ${{ github.workspace }}/build.aab --non-interactive
- name: Setup Version
if: github.event.inputs.release == 'true'
id: version
uses: ./.github/actions/setup-version
with:
type: "mobile"
- name: Prepare Release Notes
if: github.event.inputs.release == 'true'
id: release_notes
run: |
version="${{ steps.version.outputs.APP_VERSION }}"
changelog_file="apps/mobile/changelog/${version}.md"
release_notes_file="$RUNNER_TEMP/mobile-release-notes.md"
if [ -f "$changelog_file" ]; then
cp "$changelog_file" "$release_notes_file"
else
{
echo "# What's New in v${version}"
echo
echo "- No changelog file found at ${changelog_file}."
} > "$release_notes_file"
fi
echo "body_path=${release_notes_file}" >> "$GITHUB_OUTPUT"
- name: Create Release Draft
if: github.event.inputs.release == 'true'
uses: softprops/action-gh-release@v2
with:
name: Mobile v${{ steps.version.outputs.APP_VERSION }}
draft: true
prerelease: true
tag_name: mobile/v${{ steps.version.outputs.APP_VERSION }}
body_path: ${{ steps.release_notes.outputs.body_path }}
# .aab cannot be installed directly on your Android Emulator or device.
files: ${{ github.workspace }}/build.apk
================================================
FILE: .github/workflows/build-desktop.yml
================================================
name: 🖥️ Build Desktop
on:
push:
branches:
- "**"
paths:
- "apps/desktop/**"
- "packages/**"
- "pnpm-lock.yaml"
- ".github/workflows/build-desktop.yml"
workflow_dispatch:
inputs:
tag_version:
type: boolean
description: "Tag Version"
store:
type: boolean
description: "Build for Mac App Store and Microsoft Store"
build_version:
type: string
description: "Build Version, only available when mas is true"
# https://docs.github.com/en/enterprise-cloud@latest/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.tag_version == 'true' && 'tag-version' || github.event.inputs.store == 'true' && 'store' || 'manual') || 'build' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
env:
VITE_WEB_URL: ${{ vars.VITE_WEB_URL }}
VITE_API_URL: ${{ vars.VITE_API_URL }}
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_OPTIONS: --max-old-space-size=8192
jobs:
release:
if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(desktop):'))
runs-on: ${{ matrix.os }}
env:
PROD: ${{ github.event.inputs.tag_version == 'true' || github.ref_type == 'tag' || github.event.inputs.store == 'true' }}
RELEASE: ${{ github.event.inputs.tag_version == 'true' || github.ref_type == 'tag' }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
exclude:
- os: ${{ github.event.inputs.store == 'true' && 'ubuntu-latest' }}
permissions:
id-token: write
contents: write
attestations: write
steps:
- name: Check out Git repository Fully
uses: actions/checkout@v6
if: env.PROD == 'true'
with:
fetch-depth: 0
lfs: true
- name: Check out Git repository
uses: actions/checkout@v6
if: env.PROD == 'false'
with:
fetch-depth: 1
lfs: true
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "pnpm"
- name: Install Python setuptools
if: runner.os == 'macOS'
run: brew install python-setuptools
- name: Install appdmg
if: runner.os == 'macOS'
run: pnpm add -g appdmg
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
BUILD_CERTIFICATE_MAS_BASE64: ${{ secrets.BUILD_CERTIFICATE_MAS_BASE64 }}
BUILD_CERTIFICATE_MASPKG_BASE64: ${{ secrets.BUILD_CERTIFICATE_MASPKG_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
if: runner.os == 'macOS' && env.BUILD_CERTIFICATE_BASE64 != '' && env.BUILD_CERTIFICATE_MAS_BASE64 != '' && env.BUILD_CERTIFICATE_MASPKG_BASE64 != '' && env.BUILD_PROVISION_PROFILE_BASE64 != ''
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
CERTIFICATE_MAS_PATH=$RUNNER_TEMP/build_certificate_mas.p12
CERTIFICATE_MASPKG_PATH=$RUNNER_TEMP/build_certificate_maspkg.p12
PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_CERTIFICATE_MAS_BASE64" | base64 --decode -o $CERTIFICATE_MAS_PATH
echo -n "$BUILD_CERTIFICATE_MASPKG_BASE64" | base64 --decode -o $CERTIFICATE_MASPKG_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security import $CERTIFICATE_MAS_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security import $CERTIFICATE_MASPKG_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security find-identity $KEYCHAIN_PATH
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Install dependencies
run: pnpm i
- name: Prebuild packages (Windows)
if: runner.os == 'windows'
run: pnpm run build:packages
- name: Update main hash
working-directory: apps/desktop
run: pnpm update:main-hash
- name: Build - Vite
working-directory: apps/desktop
run: pnpm build:electron-vite ${{ env.PROD == 'false' && '--mode staging' || '' }}
- name: Build - Windows and Linux
if: runner.os == 'Linux' || (runner.os == 'Windows' && github.event.inputs.store != 'true')
working-directory: apps/desktop
run: pnpm build:electron-forge ${{ env.PROD == 'false' && '--mode=staging' || '' }}
- name: Build - macOS
if: runner.os == 'macOS' && github.event.inputs.store != 'true'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
OSX_SIGN_KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db
OSX_SIGN_IDENTITY: ${{ secrets.OSX_SIGN_IDENTITY }}
uses: nick-fields/retry@v3
with:
max_attempts: 3
timeout_minutes: 10
command: |
cd apps/desktop
npx electron-forge make --arch=x64 --platform=darwin ${{ env.PROD == 'false' && '--mode=staging' || '' }}
npx electron-forge make --arch=arm64 --platform=darwin ${{ env.PROD == 'false' && '--mode=staging' || '' }}
npx tsx scripts/merge-yml.ts
- name: Build - Mac App Store
if: runner.os == 'macOS' && github.event.inputs.store == 'true'
env:
OSX_SIGN_KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db
OSX_SIGN_IDENTITY: ${{ secrets.OSX_SIGN_IDENTITY_MAS }}
OSX_SIGN_PROVISIONING_PROFILE_PATH: ${{ runner.temp }}/build_pp.provisionprofile
BUILD_VERSION: ${{ github.event.inputs.build_version }}
working-directory: apps/desktop
run: pnpm build:electron-forge:mas
- name: Build - Microsoft Store
if: runner.os == 'Windows' && github.event.inputs.store == 'true'
working-directory: apps/desktop
run: pnpm build:electron-forge:ms
- name: Build - Renderer
if: runner.os == 'Linux'
working-directory: apps/desktop
run: pnpm build:render
- name: Upload file (macos-arm64-dmg)
uses: actions/upload-artifact@v7
if: runner.os == 'macOS'
with:
name: macos-arm64-dmg
path: |
apps/desktop/out/make/**/*arm64.dmg
apps/desktop/out/make/**/latest-mac.yml
retention-days: 90
- name: Upload file (macos-x64-dmg)
uses: actions/upload-artifact@v7
if: runner.os == 'macOS'
with:
name: macos-x64-dmg
path: |
apps/desktop/out/make/**/*x64.dmg
apps/desktop/out/make/**/latest-mac.yml
retention-days: 90
- name: Upload file (macos-mas-pkg)
uses: actions/upload-artifact@v7
if: runner.os == 'macOS'
with:
name: macos-mas-pkg
path: |
apps/desktop/out/make/**/*.pkg
retention-days: 90
- name: Upload file (windows-x64-exe unsigned)
uses: actions/upload-artifact@v7
id: upload-unsigned-windows-x64-exe
if: runner.os == 'windows' && github.event.inputs.store != 'true'
with:
name: windows-x64-exe
path: |
apps/desktop/out/make/**/*x64.exe
apps/desktop/out/make/**/latest.yml
retention-days: 90
- uses: signpath/github-action-submit-signing-request@v2.0
continue-on-error: true
if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true'
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "8c651516-fdaf-40a1-9fea-001dffde850e"
project-slug: "Folo"
signing-policy-slug: "release-signing"
artifact-configuration-slug: "github"
github-artifact-id: "${{ steps.upload-unsigned-windows-x64-exe.outputs.artifact-id }}"
output-artifact-directory: "apps/desktop/out/make/"
- name: Update latest.yml
if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true'
run: npx tsx apps/desktop/scripts/update-windows-yml.ts
- name: Upload file (windows-x64-exe signed)
uses: actions/upload-artifact@v7
if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true'
with:
name: windows-x64-exe
path: |
apps/desktop/out/make/**/*x64.exe
apps/desktop/out/make/**/latest.yml
retention-days: 90
overwrite: true
- name: Upload file (windows-x64-appx)
uses: actions/upload-artifact@v7
if: runner.os == 'windows'
with:
name: windows-x64-appx
path: |
apps/desktop/out/make/**/*.appx
retention-days: 90
- name: Upload file (linux-x64-appimage)
uses: actions/upload-artifact@v7
if: runner.os == 'linux'
with:
name: linux-x64-appimage
path: |
apps/desktop/out/make/**/*x64.AppImage
apps/desktop/out/make/**/latest-linux.yml
retention-days: 90
- name: Generate artifact attestation
if: env.RELEASE == 'true'
continue-on-error: true
uses: actions/attest-build-provenance@v4
with:
subject-path: |
apps/desktop/out/make/**/Folo-*.dmg
apps/desktop/out/make/**/Folo-*.zip
apps/desktop/out/make/**/Folo-*.exe
apps/desktop/out/make/**/Folo-*.AppImage
apps/desktop/out/make/**/*.yml
apps/desktop/dist/manifest.yml
apps/desktop/dist/*.tar.gz
- run: npx changelogithub
if: env.RELEASE == 'true'
continue-on-error: true
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Setup Version
if: env.RELEASE == 'true'
id: version
uses: ./.github/actions/setup-version
with:
type: "desktop"
- name: Prepare Release Notes
if: env.RELEASE == 'true'
id: release_notes
shell: bash
run: |
version="${{ steps.version.outputs.APP_VERSION }}"
changelog_file="apps/desktop/changelog/${version}.md"
release_notes_file="$RUNNER_TEMP/desktop-release-notes.md"
if [ -f "$changelog_file" ]; then
cp "$changelog_file" "$release_notes_file"
else
{
echo "# What's New in v${version}"
echo
echo "- No changelog file found at ${changelog_file}."
} > "$release_notes_file"
fi
echo "body_path=${release_notes_file}" >> "$GITHUB_OUTPUT"
- name: Create Release Draft
if: env.RELEASE == 'true'
uses: softprops/action-gh-release@v2
with:
name: Desktop v${{ steps.version.outputs.APP_VERSION }}
draft: false
prerelease: true
tag_name: desktop/v${{ steps.version.outputs.APP_VERSION }}
body_path: ${{ steps.release_notes.outputs.body_path }}
files: |
apps/desktop/out/make/**/Folo-*.dmg
apps/desktop/out/make/**/Folo-*.zip
apps/desktop/out/make/**/Folo-*.exe
apps/desktop/out/make/**/Folo-*.AppImage
apps/desktop/out/make/**/*.yml
apps/desktop/dist/manifest.yml
apps/desktop/dist/*.tar.gz
================================================
FILE: .github/workflows/build-ios-development.yml
================================================
name: 📱 Build iOS for development
on:
push:
branches:
- "**"
paths:
- "apps/mobile/web-app/**"
- "apps/mobile/native/**"
- "apps/mobile/package.json"
- "apps/mobile/app.config.ts"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-runner:
runs-on: ubuntu-latest
outputs:
runner-label: ${{ steps.set-runner.outputs.runner-label }}
steps:
- name: Set runner
id: set-runner
run: |
runners=$(curl -s -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUNNER_GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/actions/runners")
available=$(echo "$runners" | jq '.runners[]? | select(.status == "online" and .busy == false and .labels[] .name == "macOS")')
if [ -n "$available" ]; then
echo "runner-label=self-hosted" >> $GITHUB_OUTPUT
else
echo "runner-label=macos-latest" >> $GITHUB_OUTPUT
fi
build-ipa-device-self-hosted:
name: Build iOS IPA for device (self-hosted)
if: github.secret_source != 'None' && needs.check-runner.outputs.runner-label == 'self-hosted'
needs: check-runner
runs-on: [self-hosted, macOS]
steps:
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build iOS IPA
working-directory: apps/mobile
run: eas build --platform ios --profile development --non-interactive --local --output=./build.ipa
env:
SENTRY_AUTH_TOKEN: ${{ secrets.RN_SENTRY_AUTH_TOKEN }}
CI: true
# Optional: Upload artifact
- name: 📤 Upload IPA
uses: actions/upload-artifact@v7
with:
name: app-ios-development-device
path: apps/mobile/build.ipa
retention-days: 90
- name: Clear Xcode cache
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData
build-ipa-device-github:
name: Build iOS IPA for device (GitHub-hosted)
if: github.secret_source != 'None' && needs.check-runner.outputs.runner-label == 'macos-latest'
needs: check-runner
runs-on: macos-latest
steps:
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 🔧 Setup Xcode
uses: ./.github/actions/setup-xcode
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "pnpm"
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build iOS IPA
working-directory: apps/mobile
run: eas build --platform ios --profile development --non-interactive --local --output=./build.ipa
env:
CI: true
SENTRY_AUTH_TOKEN: ${{ secrets.RN_SENTRY_AUTH_TOKEN }}
# Optional: Upload artifact
- name: 📤 Upload IPA
uses: actions/upload-artifact@v7
with:
name: app-ios-development-device
path: apps/mobile/build.ipa
retention-days: 90
build-simulator:
name: Build iOS IPA for simulator
if: github.secret_source != 'None'
runs-on: macos-latest
steps:
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 🔧 Setup Xcode
uses: ./.github/actions/setup-xcode
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "pnpm"
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build iOS IPA
working-directory: apps/mobile
run: eas build --platform ios --profile ios-simulator --non-interactive --local --output=./build-simulator.ipa
env:
CI: true
SENTRY_AUTH_TOKEN: ${{ secrets.RN_SENTRY_AUTH_TOKEN }}
# Optional: Upload artifact
- name: 📤 Upload IPA
uses: actions/upload-artifact@v7
with:
name: app-ios-development-simulator
path: apps/mobile/build-simulator.ipa
retention-days: 90
================================================
FILE: .github/workflows/build-ios.yml
================================================
name: 🍎 Build iOS
on:
push:
branches:
- "**"
paths:
- "apps/mobile/**"
- "pnpm-lock.yaml"
workflow_dispatch:
inputs:
profile:
type: choice
default: preview
options:
- preview
- production
description: "Build profile"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.inputs.profile }}
cancel-in-progress: true
jobs:
check-runner:
if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(mobile):'))
runs-on: ubuntu-latest
outputs:
runner-label: ${{ steps.set-runner.outputs.runner-label }}
steps:
- name: Set runner
id: set-runner
run: |
runners=$(curl -s -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUNNER_GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/actions/runners")
available=$(echo "$runners" | jq '.runners[]? | select(.status == "online" and .busy == false and .labels[] .name == "macOS")')
if [ -n "$available" ]; then
echo "runner-label=self-hosted" >> $GITHUB_OUTPUT
else
echo "runner-label=macos-latest" >> $GITHUB_OUTPUT
fi
build-ipa-self-hosted:
name: Build iOS IPA (self-hosted)
if: needs.check-runner.outputs.runner-label == 'self-hosted'
needs: check-runner
runs-on: [self-hosted, macOS]
steps:
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build iOS IPA
working-directory: apps/mobile
run: eas build --platform ios --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive --local --output=./build.ipa
env:
SENTRY_AUTH_TOKEN: ${{ secrets.RN_SENTRY_AUTH_TOKEN }}
CI: true
# Optional: Upload artifact
- name: 📤 Upload IPA
uses: actions/upload-artifact@v7
with:
name: app-ios
path: apps/mobile/build.ipa
retention-days: 90
- name: Clear Xcode cache
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData
- name: Submit to App Store
if: github.event.inputs.profile == 'production'
working-directory: apps/mobile
run: eas submit --platform ios --path build.ipa --non-interactive
build-ipa-github:
name: Build iOS IPA (GitHub)
if: needs.check-runner.outputs.runner-label == 'macos-latest'
needs: check-runner
runs-on: macos-latest
steps:
- name: 📦 Checkout code
uses: actions/checkout@v6
- name: 🔧 Setup Xcode
uses: ./.github/actions/setup-xcode
- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: "pnpm"
- name: 📱 Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: pnpm install
- name: 🔨 Build iOS IPA
working-directory: apps/mobile
run: eas build --platform ios --profile ${{ github.event.inputs.profile || 'preview' }} --non-interactive --local --output=./build.ipa
env:
CI: true
SENTRY_AUTH_TOKEN: ${{ secrets.RN_SENTRY_AUTH_TOKEN }}
# Optional: Upload artifact
- name: 📤 Upload IPA
uses: actions/upload-artifact@v7
with:
name: app-ios
path: apps/mobile/build.ipa
retention-days: 90
- name: Submit to App Store
if: github.event.inputs.profile == 'production'
working-directory: apps/mobile
run: eas submit --platform ios --path build.ipa --non-interactive
================================================
FILE: .github/workflows/build-web.yml
================================================
on:
pull_request:
push:
branches: [main, dev]
name: 🌐 CI Build web and SSR server
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-ssr
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
jobs:
build:
name: Build web and SSR server
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
lfs: true
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Checkout LFS objects
run: git lfs checkout
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build web and SSR server
run: |
npm exec turbo run Folo#build:web @follow/ssr#build
================================================
FILE: .github/workflows/deploy-cloudflare-desktop.yml
================================================
name: "\u2601\ufe0f Deploy Desktop to Cloudflare"
on:
push:
branches: [main, dev]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Build & Deploy Desktop Web
runs-on: ubuntu-latest
env:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
lfs: true
- name: Checkout LFS objects
run: git lfs checkout
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/action-setup@v4
- name: Use Node.js LTS
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build desktop web (SPA)
working-directory: apps/desktop
env:
VITE_WEB_URL: ${{ github.ref == 'refs/heads/dev' && 'https://dev.folo.is' || 'https://app.folo.is' }}
VITE_API_URL: ${{ github.ref == 'refs/heads/dev' && 'https://api.dev.folo.is' || 'https://api.folo.is' }}
run: pnpm run build:web
- name: Deploy desktop web to Cloudflare (dev)
if: github.ref == 'refs/heads/dev'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/desktop
command: deploy --env dev
- name: Deploy desktop web to Cloudflare (prod)
if: github.ref == 'refs/heads/main'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/desktop
command: 'deploy --env=""'
================================================
FILE: .github/workflows/deploy-cloudflare-landing.yml
================================================
name: "\u2601\ufe0f Deploy Landing to Cloudflare"
on:
push:
branches: [main, dev]
workflow_dispatch:
inputs:
confirm_production_deploy:
description: Deploy the selected branch to production (folo.is)
required: true
default: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-prod-{0}', github.ref) || github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Build & Deploy Landing Worker
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
lfs: true
- name: Checkout LFS objects
run: git lfs checkout
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/action-setup@v4
- name: Use Node.js LTS
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Resolve deployment target
id: target
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ "${{ inputs.confirm_production_deploy }}" != "true" ]]; then
echo "Manual production deploy was not confirmed." >&2
exit 1
fi
echo "environment=prod" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "environment=dev" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "environment=prod" >> "$GITHUB_OUTPUT"
- name: Build Landing Worker
run: pnpm exec turbo run @follow/landing#cf:build
- name: Deploy Landing to Cloudflare (dev)
if: steps.target.outputs.environment == 'dev'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/landing
command: deploy --env dev --name landing-next-dev --routes landing.dev.folo.is/*
- name: Deploy Landing to Cloudflare (prod)
if: steps.target.outputs.environment == 'prod'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/landing
command: deploy
================================================
FILE: .github/workflows/deploy-cloudflare-ssr.yml
================================================
name: "\u2601\ufe0f Deploy SSR to Cloudflare"
on:
push:
branches: [main, dev]
workflow_dispatch:
inputs:
confirm_production_deploy:
description: Deploy the selected branch to production (app.folo.is)
required: true
default: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && format('manual-prod-{0}', github.ref) || github.ref }}
cancel-in-progress: true
jobs:
deploy:
name: Build & Deploy SSR Worker
runs-on: ubuntu-latest
env:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
lfs: true
- name: Checkout LFS objects
run: git lfs checkout
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- uses: pnpm/action-setup@v4
- name: Use Node.js LTS
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Resolve deployment target
id: target
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
if [[ "${{ inputs.confirm_production_deploy }}" != "true" ]]; then
echo "Manual production deploy was not confirmed." >&2
exit 1
fi
echo "environment=prod" >> "$GITHUB_OUTPUT"
echo "vite_web_url=https://app.folo.is" >> "$GITHUB_OUTPUT"
echo "vite_api_url=https://api.folo.is" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "environment=dev" >> "$GITHUB_OUTPUT"
echo "vite_web_url=https://dev.folo.is" >> "$GITHUB_OUTPUT"
echo "vite_api_url=https://api.dev.folo.is" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "environment=prod" >> "$GITHUB_OUTPUT"
echo "vite_web_url=https://app.folo.is" >> "$GITHUB_OUTPUT"
echo "vite_api_url=https://api.folo.is" >> "$GITHUB_OUTPUT"
- name: Build desktop web (SSR assets)
working-directory: apps/desktop
env:
VITE_WEB_URL: ${{ steps.target.outputs.vite_web_url }}
VITE_API_URL: ${{ steps.target.outputs.vite_api_url }}
run: pnpm run build:web
- name: Build SSR Worker
working-directory: apps/ssr
run: pnpm run build:worker
- name: Copy WASM file
run: cp node_modules/@resvg/resvg-wasm/index_bg.wasm apps/ssr/dist/worker/resvg.wasm
- name: Deploy SSR Worker to Cloudflare (dev)
if: steps.target.outputs.environment == 'dev'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/ssr
command: deploy --env dev
- name: Deploy SSR Worker to Cloudflare (prod)
if: steps.target.outputs.environment == 'prod'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/ssr
command: 'deploy --env=""'
================================================
FILE: .github/workflows/issue-labeler.yml
================================================
name: 🏷️ Issue labeler
on:
issues:
types: [opened]
permissions:
contents: read
jobs:
label-component:
runs-on: ubuntu-latest
permissions:
# required for all workflows
issues: write
# only required for workflows in private repositories
actions: read
contents: read
steps:
- uses: actions/checkout@v6
- name: Parse issue form
uses: stefanbuck/github-issue-parser@v3
id: issue-parser
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- name: Set labels based on platform field
uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/lint.yml
================================================
on:
pull_request:
push:
branches: [main, dev]
# Do not use secrets or variables to allow for public access
env:
VITE_WEB_URL: https://app.folo.is
VITE_API_URL: https://api.follow.is
name: ✨ CI Format, Typecheck and Lint
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-lint
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
jobs:
build:
name: Format, Lint and Typecheck
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [lts/*]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
lfs: true
- name: Cache turbo build setup
uses: actions/cache@v5
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- name: Checkout LFS objects
run: git lfs checkout
- uses: pnpm/action-setup@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Build web and SSR server
run: |
npm exec turbo run Folo#build:web @follow/ssr#build
- name: Format, Lint and Typecheck
run: |
export NODE_OPTIONS="--max_old_space_size=16384"
npm exec turbo run format:check typecheck lint
- name: Run test
run: npm exec turbo run test
================================================
FILE: .github/workflows/pr-title-check.yml
================================================
name: ✅ PR Conventional Commit Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
runs-on: ubuntu-latest
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.3.0
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert","release","build"]'
add_label: false
================================================
FILE: .github/workflows/similar-issues.yml
================================================
name: Similar Issues via AI MCP
on:
issues:
types: [opened]
jobs:
find-similar:
permissions:
contents: read
issues: write
models: read
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Prepare prompt variables
id: prepare_input
uses: actions/github-script@v8
with:
script: |
const issue = context.payload.issue || {};
const title = issue.title || '';
const body = issue.body || '';
const indent = ' ';
// Indent subsequent lines so YAML block scalar indentation remains valid
const bodyIndented = body.replace(/\n/g, '\n' + indent);
core.setOutput('issue_title_json', JSON.stringify(title));
core.setOutput('issue_body_indented_json', JSON.stringify(bodyIndented));
- name: Find similar issues with AI (MCP)
id: inference
uses: actions/ai-inference@v2
with:
prompt-file: ./.github/prompts/similar_issues.prompt.yml
input: |
issue_title: ${{ steps.prepare_input.outputs.issue_title_json }}
issue_body: ${{ steps.prepare_input.outputs.issue_body_indented_json }}
repository: ${{ github.repository }}
enable-github-mcp: true
token: ${{ secrets.GITHUB_TOKEN }}
github-mcp-token: ${{ secrets.USER_PAT }}
endpoint: https://models.github.ai/orgs/RSSNext/inference
- name: Prepare comment body
id: prepare
uses: actions/github-script@v8
env:
AI_RESPONSE: ${{ steps.inference.outputs.response }}
with:
script: |
let data;
try {
data = JSON.parse(process.env.AI_RESPONSE || '{}');
} catch (e) {
core.setOutput('has_matches', 'false');
return;
}
const matches = Array.isArray(data.matches) ? data.matches : [];
const rawIssueNumber = context?.payload?.issue?.number;
const issueNumber = typeof rawIssueNumber === 'string' ? Number(rawIssueNumber) : rawIssueNumber;
const filteredMatches = Number.isFinite(issueNumber)
? matches.filter((m) => {
const matchNumber = typeof m?.number === 'string' ? Number(m.number) : m?.number;
if (Number.isFinite(matchNumber)) {
return matchNumber !== issueNumber;
}
if (typeof m?.url === 'string') {
const urlMatch = m.url.match(/\/issues\/(\d+)(?:$|[?#])/);
if (urlMatch) {
return Number(urlMatch[1]) !== issueNumber;
}
}
return true;
})
: matches;
if (!filteredMatches.length) {
core.setOutput('has_matches', 'false');
return;
}
const lines = [];
lines.push('I found similar issues that might help:');
for (const m of filteredMatches.slice(0, 3)) {
const num = m.number != null ? `#${m.number}` : '';
const title = m.title || 'Untitled';
const url = m.url || '';
const score = typeof m.similarity_score === 'number' ? ` (similarity: ${m.similarity_score.toFixed(2)})` : '';
lines.push(`- ${url}${score}`.trim());
}
core.setOutput('has_matches', 'true');
core.setOutput('comment_body', lines.join('\n'));
- name: Comment similar issues
if: steps.prepare.outputs.has_matches == 'true'
uses: actions/github-script@v8
with:
script: |
const body = ${{ toJson(steps.prepare.outputs.comment_body) }};
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body
});
================================================
FILE: .github/workflows/sync.yaml
================================================
name: 🔄 Sync Release Branches To Dev
on:
push:
branches:
- main
- mobile-main
permissions:
contents: write
jobs:
sync-to-dev:
runs-on: ubuntu-latest
if: |
(github.ref == 'refs/heads/main' && contains(github.event.head_commit.message || '', 'release(desktop):')) ||
(github.ref == 'refs/heads/mobile-main' && contains(github.event.head_commit.message || '', 'release(mobile):'))
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Merge source branch into dev
run: |
source_branch="${GITHUB_REF_NAME}"
git fetch origin main mobile-main dev
git checkout dev
git merge --no-ff "origin/${source_branch}" -m "chore(sync): merge ${source_branch} into dev"
git push origin dev
================================================
FILE: .github/workflows/tag.yml
================================================
name: 🏷️ Release Orchestrator
on:
push:
branches:
- main
- mobile-main
permissions:
contents: write
actions: write
jobs:
create_tag:
name: Create Release Tag
runs-on: ubuntu-latest
outputs:
tag_version: ${{ steps.release_info.outputs.tag_version }}
platform: ${{ steps.release_info.outputs.platform }}
version: ${{ steps.release_info.outputs.version }}
ref_name: ${{ steps.release_info.outputs.ref_name }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Make script executable
run: |
chmod +x .github/scripts/extract-release-info.mjs
- name: Extract release information
id: extract_info
run: .github/scripts/extract-release-info.mjs
continue-on-error: true
- name: Expose release outputs
id: release_info
run: |
echo "tag_version=${tag_version:-}" >> "$GITHUB_OUTPUT"
echo "platform=${platform:-}" >> "$GITHUB_OUTPUT"
echo "version=${version:-}" >> "$GITHUB_OUTPUT"
echo "ref_name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
- name: Validate git configuration
if: steps.release_info.outputs.tag_version != ''
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Create and push tag
if: steps.release_info.outputs.tag_version != ''
run: |
git fetch --tags --force
echo "Creating tag: ${{ steps.release_info.outputs.tag_version }}"
if git rev-parse "${{ steps.release_info.outputs.tag_version }}" >/dev/null 2>&1; then
echo "Tag ${{ steps.release_info.outputs.tag_version }} already exists, skipping creation"
exit 0
fi
git tag "${{ steps.release_info.outputs.tag_version }}"
git push origin "${{ steps.release_info.outputs.tag_version }}"
echo "Successfully created and pushed tag: ${{ steps.release_info.outputs.tag_version }}"
trigger_builds:
name: Trigger Platform Builds
needs: create_tag
if: needs.create_tag.outputs.tag_version != ''
runs-on: ubuntu-latest
steps:
- name: Checkout repository
if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main'
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure Git
if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main'
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Resolve desktop build version
id: desktop_build_version
if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main'
run: |
git fetch --tags --force
latest_tag="$(git tag -l 'desktop-build/v*' --sort=-v:refname | head -n 1)"
if [ -z "$latest_tag" ]; then
last_build=110
else
last_build="${latest_tag#desktop-build/v}"
fi
next_build=$((last_build + 1))
build_tag="desktop-build/v${next_build}"
if git rev-parse "$build_tag" >/dev/null 2>&1; then
echo "Build tag $build_tag already exists."
exit 1
fi
git tag "$build_tag"
git push origin "$build_tag"
echo "build_version=${next_build}" >> "$GITHUB_OUTPUT"
echo "Desktop build version: ${next_build}"
- name: Trigger Desktop Tag Version Build
if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build-desktop.yml',
ref: 'main',
inputs: {
tag_version: 'true',
store: 'false'
}
});
console.log('Desktop Tag Version build triggered successfully');
- name: Trigger Desktop Store Build
if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build-desktop.yml',
ref: 'main',
inputs: {
tag_version: 'false',
store: 'true',
build_version: '${{ steps.desktop_build_version.outputs.build_version }}'
}
});
console.log('Desktop store build triggered successfully');
- name: Trigger Mobile Preview Release Build
if: needs.create_tag.outputs.platform == 'mobile' && needs.create_tag.outputs.ref_name == 'mobile-main'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build-android.yml',
ref: 'mobile-main',
inputs: {
profile: 'preview',
release: 'true'
}
});
console.log('Mobile preview release build triggered successfully');
- name: Trigger Mobile Production Android Build
if: needs.create_tag.outputs.platform == 'mobile' && needs.create_tag.outputs.ref_name == 'mobile-main'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build-android.yml',
ref: 'mobile-main',
inputs: {
profile: 'production',
release: 'false'
}
});
console.log('Mobile production Android build triggered successfully');
- name: Trigger Mobile Production iOS Build
if: needs.create_tag.outputs.platform == 'mobile' && needs.create_tag.outputs.ref_name == 'mobile-main'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const response = await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'build-ios.yml',
ref: 'mobile-main',
inputs: {
profile: 'production'
}
});
console.log('Mobile production iOS build triggered successfully');
================================================
FILE: .github/workflows/translator.yml
================================================
name: "🌍 translator"
on:
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
discussion:
types: [created, edited]
discussion_comment:
types: [created, edited]
jobs:
translate:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: lizheming/github-translate-action@c55aac477e98562d4faed9f77c54ab8306ae6ebf
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
IS_MODIFY_TITLE: true
================================================
FILE: .gitignore
================================================
node_modules
dist
out
.next
.open-next
next-env.d.ts
.DS_Store
*.log*
.env
.eslintcache
.env.*
!.env.example
# Sentry Config File
.env.sentry-build-plugin
.vercel
stats.html
electron.vite.config.*.mjs
vite.config.*.mjs
.generated
.turbo
apps/desktop/src/renderer/dev-dist
tsconfig.tsbuildinfo
buildServer.json
**/**/generated-routes.ts
apps/desktop/build/appxmanifest.xml
apps/desktop/resources/cli
.claude/settings.local.json
.serena
.wrangler
# Local agent artifacts
.codex/
# E2E outputs
/apps/desktop/e2e/playwright-report/
/apps/desktop/e2e/test-results/
/apps/mobile/e2e/artifacts/
/apps/mobile/report.xml
/report.xml
# Mobile local E2E build artifacts
apps/mobile/build-*.tar.gz
================================================
FILE: .npmrc
================================================
shamefully-hoist=true
node-linker=hoisted
save-exact=true
================================================
FILE: .nvmrc
================================================
stable
================================================
FILE: .prettierignore
================================================
pnpm-lock.yaml
CHANGELOG.md
.context
apps/external/postcss.config.cjs
apps/mobile/android
apps/mobile/ios
apps/mobile/native/example
apps/mobile/native/android
apps/mobile/native/ios
apps/mobile/.expo
generated-routes.ts
================================================
FILE: .prettierrc.mjs
================================================
/** @type {import("prettier").Config & import("prettier-plugin-tailwindcss").PluginOptions} */
export default {
semi: false,
singleQuote: false,
printWidth: 100,
tabWidth: 2,
trailingComma: "all",
objectWrap: "preserve",
plugins: ["prettier-plugin-tailwindcss"],
tailwindConfig: "./apps/desktop/tailwind.config.ts",
overrides: [
{
files: "apps/mobile/**/*.{css,js,jsx,ts,tsx}",
options: {
tailwindConfig: "./apps/mobile/tailwind.config.ts",
},
},
{
files: "apps/mobile/web-app/html-renderer/**/*.{css,js,jsx,ts,tsx}",
options: {
tailwindConfig: "./apps/mobile/web-app/html-renderer/tailwind.config.ts",
},
},
{
files: "apps/ssr/**/*.{css,html,js,jsx,ts,tsx}",
options: {
tailwindConfig: "./apps/ssr/tailwind.config.ts",
},
},
],
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"johnsoncodehk.vscode-tsslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript][javascriptreact][typescript][typescriptreact][json][jsonc]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"files.associations": {
"*.css": "tailwindcss"
},
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["tw`([^`]*)`", "([^`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"]
],
"github.copilot.chat.codeGeneration.useInstructionFiles": true,
"tailwindCSS.experimental.configFile": {
"apps/mobile/tailwind.config.ts": "apps/mobile/**",
"apps/ssr/tailwind.config.ts": "apps/ssr/**",
"apps/desktop/tailwind.config.ts": ["!apps/mobile/**", "!apps/ssr/**", "**"]
},
"typescript.tsserver.maxTsServerMemory": 8096,
"typescript.tsserver.nodePath": "node",
// If you do not want to autofix some rules on save
// You can put this in your user settings or workspace settings
"eslint.codeActionsOnSave.rules": [
"!prefer-const",
"!unused-imports/no-unused-imports",
"!@stylistic/jsx-self-closing-comp",
"!tailwindcss/classnames-order",
"!arrow-body-style",
"*"
],
// If you want to silent stylistic rules
// You can put this in your user settings or workspace settings
"eslint.rules.customizations": [
{
"rule": "@stylistic/*",
"severity": "off",
"fixable": true
},
{
"rule": "tailwindcss/classnames-order",
"severity": "off"
},
{
"rule": "antfu/consistent-list-newline",
"severity": "off"
},
{
"rule": "hyoban/jsx-attribute-spacing",
"severity": "off"
},
{
"rule": "simple-import-sort/*",
"severity": "off"
},
{
"rule": "prefer-const",
"severity": "off"
},
{
"rule": "unused-imports/no-unused-imports",
"severity": "off"
}
],
"cSpell.words": ["Hydable", "rsshub", "Русский"],
// "editor.foldingImportsByDefault": true,
"commentTranslate.hover.enabled": false,
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.enabledFrameworks": ["i18next"],
"i18n-ally.displayLanguage": "en",
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{namespaces}/{locale}.json",
"lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
"lldb.launch.expressions": "native",
"[swift]": {
"editor.defaultFormatter": "swiftlang.swift-vscode"
},
"npm.exclude": "**/packages/**"
}
================================================
FILE: AGENTS.md
================================================
# AGENTS.md
This file provides concise, agent-focused guidance for working in this monorepo. It consolidates the repository's CLAUDE.md guides, .cursor rules, Cursor rules improvements, and modern agent best practices.
## Project overview
- Monorepo managed by pnpm workspaces + Turbo.
- Apps:
- `apps/desktop` – Electron app (Vite + React renderer is the primary web app)
- `apps/mobile` – React Native app via Expo
- `apps/ssr` – Minimal SSR site for external sharing
- Shared packages: `packages/internal` (components, atoms, hooks, store, utils, database, etc.).
## Setup commands
```bash
# Install deps
pnpm install
# Desktop – recommended (browser renderer)
cd apps/desktop && pnpm run dev:web
# Desktop – full Electron
cd apps/desktop && pnpm run dev:electron
# Mobile – Expo
cd apps/mobile && pnpm run dev
# or target platforms
cd apps/mobile && pnpm run ios
cd apps/mobile && pnpm run android
# SSR
cd apps/ssr && pnpm run dev
# Build web version (desktop renderer)
pnpm run build:web
```
## Quality gates (must-pass before commit/PR)
```bash
# 1) Typecheck first (required)
pnpm run typecheck
# 2) Lint and auto-fix
pnpm run lint:fix
# 3) Tests
pnpm run test
```
- Run the above at the root, or use per-package variants as needed.
- Follow this order strictly: typecheck → lint → test.
- After every modification, run the following checks to catch errors early:
```bash
npm exec turbo run format:check typecheck lint
npm exec turbo run test
```
## Code style and conventions
- TypeScript strict; avoid `any` (use precise types). Comments in English. Keep solutions simple and maintainable.
- Prefer CSS transitions/animations for simple UI interactions. Use JS-driven motion only when necessary to avoid frame drops.
- Imports: use `pathe` instead of `node:path` for cross‑platform paths.
- Organize shared, reusable UI in `packages/internal/components`; app-specific UI stays in its app.
- **Style extraction**: Avoid inline styles in JSX. Extract complex styles (especially those using CSS variables, gradients, or multiple properties) to external style objects similar to React Native's `StyleSheet.create`. Place style objects in a `styles.ts` file alongside the component, using `CSSProperties` type for type safety.
## Team preferences
- Prefer CSS transitions/animations over JS-based motion for simple interactions to avoid frame drops.
- Prefer simple, easy-to-maintain solutions.
- Avoid using the `any` type in TypeScript.
- Write code comments in English.
## Architecture quick reference
- State: Jotai for atoms, Zustand for complex stores, TanStack Query for server state.
- Database: Drizzle + SQLite (see `packages/internal/database`).
- Error handling: custom utils in `packages/internal/utils`; Sentry integrated.
- i18n: i18next with flat keys only; no `defaultValue`. Provide `en`, `zh-CN`, `ja` for each feature. Avoid conflicting dotted keys.
## UI system and design tokens
### Tailwind + Apple UIKit colors (Desktop/Web)
- Use Tailwind classes bound to Apple UIKit color tokens (light/dark adaptive). Prefix by CSS property:
- System colors: `text-red`, `bg-blue`, `border-gray`, etc. for `red|orange|yellow|green|mint|teal|cyan|blue|indigo|purple|pink|brown|gray`.
- Fill: `bg-fill[-secondary|-tertiary|-quaternary|-quinary]` and `bg-fill-vibrant[-secondary|-tertiary|-quaternary|-quinary]` (and `border-*` as needed).
- Text: `text-text`, `text-text-secondary|tertiary|quaternary|quinary`, `text-text-vibrant(-secondary|-tertiary|-quaternary|-quinary)`.
- Material: `bg-material-ultra-thick|thick|medium|thin|ultra-thin|opaque`.
- Control: `bg-control-enabled|disabled`.
- Interface: `bg-menu|popover|titlebar|sidebar|selection-focused|selection-focused-fill|selection-unfocused|selection-unfocused-fill|header-view|tooltip|under-window-background`.
These classes map to the UIKit color variables (see `.cursor rules/color` and `apps/desktop/AGENTS.md`).
### Icons (Desktop/Web)
- Prefer MingCute with `i-mgc-` prefix (e.g., `i-mgc-copy-cute-re`). Use `i-mingcute-` only if no `i-mgc-` exists.
### Motion (Desktop/Web)
- Use Framer Motion with LazyMotion via `m` from `motion/react` (e.g., `m.div`).
- Prefer spring presets from `@follow/components/constants/spring.js` (`Spring.presets.smooth|snappy|bouncy`).
- For simple micro-interactions, prefer CSS transitions first.
### React Native (Mobile)
- Styling: NativewindCSS; do not use external `StyleSheet.create` for new UI.
- Icons: use `@/apps/mobile/icons` only.
- Colors: use React Native UIKit color system (via `react-native-uikit-colors`) with Tailwind utilities:
- Backgrounds: `system-background`, `secondary-system-background`, `tertiary-system-background`.
- Grouped backgrounds: `system-grouped-background`, `secondary-system-grouped-background`, `tertiary-system-grouped-background`.
- Labels: `label`, `secondary-label`, `tertiary-label`, `quaternary-label`.
- Fills: `system-fill`, `secondary-system-fill`, `tertiary-system-fill`, `quaternary-system-fill`.
- Separators: `separator`, `opaque-separator`, `non-opaque-separator`.
- Semantic colors: `red|orange|yellow|green|mint|teal|cyan|blue|indigo|purple|pink|brown`, grays `gray..gray6`, and interactive `link`, `placeholder-text`.
## Component placement
1. Check existing components in `apps/desktop/layer/renderer/src/modules/renderer/components` for app-specific UI.
2. If generic and reusable, implement in `packages/internal/components` and export from the package index.
## Testing & CI tips
- Use Vitest for unit tests; co-locate tests near source files.
- After moving files or changing imports, run `pnpm lint` and `pnpm typecheck` for the affected package.
- CI expects `pnpm typecheck`, `pnpm lint`, and `pnpm test` to pass before merge.
## Agent workflow (Cursor-oriented improvements)
- Status updates: provide brief progress notes when running tool batches.
- Prefer semantic code search to explore unfamiliar areas; use exact grep only for symbols.
- Default to parallelizing independent searches/reads to reduce latency.
- Avoid multi-line speculative edits; keep edits minimal and targeted; preserve existing indentation.
- When editing TypeScript, do not introduce `any`; keep types precise.
- For UI, prefer CSS transitions for simple effects; use Framer Motion `m.*` only when needed.
## Context7 (up-to-date docs)
- Use Context7 to fetch current library docs before using APIs prone to change.
- Workflow:
1. Resolve a library ID: resolve the library (e.g., React Native, Vite, TanStack Query).
2. Fetch docs scoped to the topic (e.g., hooks, routing).
3. Integrate code examples following our style rules.
## Sequential Thinking (step-by-step problem solving)
- Break work into small thought steps:
1. Define immediate goal/assumption.
2. Use suitable tool (search, code edit, error explainer, docs).
3. Record the output/results.
4. Decide next step or branch alternatives; compare trade-offs.
- Encourage rollback/iteration if new information contradicts prior steps.
## Subproject guides
- This root AGENTS.md sets global rules. Each app/package should include its own `AGENTS.md` (e.g., `apps/desktop/AGENTS.md`, `apps/mobile/AGENTS.md`). The closest guide to the edited file takes precedence when rules conflict.
## Quick checklists
- Implementation
- [ ] Is code placed in the right package/app?
- [ ] Type-safe (no `any`), readable names, English comments where needed.
- [ ] Uses correct UIKit Tailwind tokens and icon sources.
- [ ] For motion: CSS first; `m.*` only if necessary.
- Validation
- [ ] `pnpm typecheck` passes
- [ ] `pnpm lint:fix` passes cleanly
- [ ] Tests updated and pass
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at follow@rss3.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Folo
Thank you for considering contributing to Folo! We welcome contributions from the community to help improve and expand the project.
## Getting Started
Before you start contributing, please ensure you have enabled [Corepack](https://nodejs.org/api/corepack.html). Corepack ensures you are using the correct version of the package manager specified in the `package.json`.
```sh
corepack enable && corepack prepare
```
### Installing Dependencies
To install the necessary dependencies, run:
```sh
pnpm install
```
## Development Setup
### Develop in the Browser
For a more convenient development experience, we recommend developing in the browser:
```sh
cd apps/desktop && pnpm run dev:web
```
This will open the browser at `https://app.folo.is/__debug_proxy`, allowing you to access the online API environment for development and debugging.
### Develop in Electron
If you prefer to develop in Electron, follow these steps:
0. Go to the `apps/desktop` directory:
```sh
cd apps/desktop
```
1. Copy the example environment variables file:
```sh
cp .env.example .env
```
2. Set `VITE_API_URL` to `https://api.follow.is` in your `.env` file.
3. Run the development server:
```sh
pnpm run dev:electron
```
> **Tip:** If you encounter login issues, copy the `__Secure-better-auth.session_token` from your browser's cookies into the app.
### Develop in External SSR Web App
To develop in SSR, follow these steps:
1. Go to the `apps/ssr` directory:
```sh
cd apps/ssr
```
2. Run the development server:
```sh
pnpm run dev
```
### Develop in Mobile App
To develop in the mobile app, follow these steps:
> [!NOTE]
> You need to have a Mac device to develop in the mobile app.
>
> And already installed Xcode and the necessary dependencies.
1. Go to the `apps/mobile` directory:
```sh
cd apps/mobile
```
2. Copy the example environment variables file:
```sh
cp .env.example .env
```
Then set the required environment variables in your `.env` file:
```sh
echo 'EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN="xxx"' >> .env
```
Or manually edit the `.env` file to add:
```
EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN="xxx"
```
the value is any string.
3. Build and install Folo(dev) app from source: (This step will take a while and only need to be done once)
```sh
pnpm expo prebuild --clean # Optional
pnpm run ios
```
4. Run the development server:
```sh
pnpm run dev
```
#### Development Native Modules
To develop native iOS modules, follow these steps:
1. Go to the `apps/mobile` directory:
```sh
cd apps/mobile/ios
```
2. Open project in Xcode:
```sh
open Folo.xcworkspace
```
3. Open `Pods` in left sidebar and select `FollowNative`:

4. Build and run the project.
## Contribution Guidelines
- Ensure your code follows the project's coding standards and conventions.
- Write clear, concise commit messages.
- Include relevant tests for your changes.
- Update documentation as necessary.
## Community
Join our community to discuss ideas, ask questions, and share your contributions:
- [Discord](https://discord.gg/AwWcAQ7euc)
- [Twitter](https://x.com/intent/follow?screen_name=folo_is)
We look forward to your contributions!
## License
By contributing to Folo, you agree that your contributions will be licensed under the GNU Affero General Public License version 3, with the special exceptions noted in the `README.md`.
================================================
FILE: LICENSE
================================================
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
---
Folo is licensed under the GNU Affero General Public License version 3 with the addition of the following special exception:
All content in the `icons/mgc` directory is copyrighted by https://mgc.mingcute.com/ and cannot be redistributed.
================================================
FILE: README.md
================================================
<div align="center">
<a href="https://github.com/RSSNext/Folo">
<img src="https://github.com/RSSNext/Folo/raw/refs/heads/dev/apps/desktop/layer/renderer/public/icon.svg" alt="Logo" width="80" height="80">
</a>
<h3>Folo</h3>
<p>
<img src="https://github.com/user-attachments/assets/cbe924f2-d8b0-48b0-814e-7c06ccb1911c" height="60" />
<img src="https://github.com/user-attachments/assets/6997a236-3df3-49d5-98a4-514f6d1a02c4" height="60" />
<br />
<br />
<a href="https://github.com/RSSNext/Folo/stargazers"><img src="https://img.shields.io/github/stars/RSSNext/Follow?color=ffcb47&labelColor=black&style=flat-square&logo=github&label=Stars" /></a>
<a href="https://github.com/RSSNext/Folo/graphs/contributors"><img src="https://img.shields.io/github/contributors/RSSNext/Folo?style=flat-square&logo=github&label=Contributors&labelColor=black" /></a>
<a href="https://status.follow.is/" target="_blank"><img src="https://status.follow.is/api/badge/18/uptime?color=%2344CC10&labelColor=black&style=flat-square"/></a>
<a href="https://github.com/RSSNext/Folo/releases"><img src="https://img.shields.io/github/downloads/RSSNext/Folo/total?color=369eff&labelColor=black&logo=github&style=flat-square&label=Downloads" /></a>
<a href="https://x.com/intent/follow?screen_name=folo_is"><img src="https://img.shields.io/badge/Follow-blue?color=1d9bf0&logo=x&labelColor=black&style=flat-square" /></a>
<a href="https://discord.gg/AwWcAQ7euc" target="_blank"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2Ffollowapp%3Fwith_counts%3Dtrue&query=approximate_member_count&color=5865F2&label=Discord&labelColor=black&logo=discord&logoColor=white&style=flat-square"/></a>
<br />
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/itunes/v/6739802604?style=flat-square&logo=apple&label=App%20Store&color=FF5C00&labelColor=black" /></a>
<a href="https://play.google.com/store/apps/details?id=is.follow" target="_blank"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fplay.cuzi.workers.dev%2Fplay%3Fi%3Dis.follow%26gl%3DUS%26hl%3Den%26l%3DAndroid%26m%3D%24version&style=flat-square&logo=google-play&label=Google%20Play&labelColor=black&color=FF5C00"/></a>
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.folo.is%2Fupdates%2Fdistribution%2Fmas&query=data.storeVersion&prefix=v&style=flat-square&logo=apple&label=Mac%20App%20Store&labelColor=black&color=FF5C00&cacheSeconds=3600" /></a>
<a href="https://apps.microsoft.com/detail/9nvfzpv0v0ht?mode=direct"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.folo.is%2Fupdates%2Fdistribution%2Fmss&query=data.storeVersion&style=flat-square&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMyAzaDguNTN2OC41M0gzek0xMi40NjkgM2g4LjUzdjguNTNoLTguNTN6TTMgMTIuNDdoOC41M1YyMUgzek0xMi40NjkgMTIuNDdoOC41M1YyMWgtOC41M3oiLz48L3N2Zz4%3D&logoColor=white&label=Microsoft%20Store&labelColor=black&color=FF5C00&cacheSeconds=3600&prefix=v" /></a>
<br />
<br />
<!-- <a href="https://github.com/RSSNext/Folo" target="_blank"><img src="https://github.com/user-attachments/assets/59b957fb-59ed-4ef0-994e-f6a402a6fe2b" alt="GitHub Trending" height="55"/></a>
<br />
<br /> -->
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604" target="_blank"><img src="https://github.com/user-attachments/assets/35747716-28bf-413a-822b-aa49d49f1aa0" alt="Folo Mobile" width="52%"/></a>
<a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604" target="_blank"><img src="https://github.com/user-attachments/assets/198a0165-b8c9-45c1-9116-b473a13a8d0c" alt="Folo Desktop" width="46%"/></a>
<br />
<br />
</p>
</div>
As they say, your thoughts are what you read—and we’ve been consuming noisy feeds for too long! Folo organizes content into one timeline, keeping you updated on what matters, noise-free. Share lists, explore collections, and enjoy distraction-free browsing.
## 👋🏻 Getting Started & Join Our Community
Whether for users or professional developers, Folo will be your open information playground. Please be aware that Folo is currently under active development, and feedback is welcome for any [issue](https://github.com/RSSNext/Folo/issues) encountered.
Feel free to try it using the following methods:
| Operating System | Source |
| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Any | <a href="https://app.folo.is" target="_blank"><img src="https://github.com/user-attachments/assets/51ef7800-b683-4493-83e8-eb4752366997" alt="Browser" height="55"/></a> |
| iOS | <a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604" target="_blank"><img src="https://github.com/user-attachments/assets/a94d8698-2a11-4f43-9b0a-b756b17b61f7" alt="App Store" height="55"/></a> |
| Android | <a href="https://play.google.com/store/apps/details?id=is.follow" target="_blank"><img src="https://github.com/user-attachments/assets/0d178e0b-3ace-4f75-bbde-ab3c0a416ce8" alt="Google Play" height="55"/></a> <a href="https://github.com/RSSNext/Folo/releases/latest" target="_blank"><img src="https://github.com/user-attachments/assets/cf61e197-d756-4606-a8ad-fb591f79fdfc" alt="App Store" height="55"/></a> |
| macOS | <a href="https://apps.apple.com/us/app/folo-follow-everything/id6739802604" target="_blank"><img src="https://github.com/user-attachments/assets/0d47f902-7fa3-494e-ad28-9ab11af5e6d4" alt="Microsoft Store" height="55"/></a> <a href="https://github.com/RSSNext/Folo/releases/latest" target="_blank"><img src="https://github.com/user-attachments/assets/cf61e197-d756-4606-a8ad-fb591f79fdfc" alt="App Store" height="55"/></a> |
| Windows | <a href="https://apps.microsoft.com/detail/9nvfzpv0v0ht?mode=direct" target="_blank"><img src="https://github.com/user-attachments/assets/b3112bab-9dd0-4893-9488-890dcb368f70" alt="Microsoft Store" height="55"/></a> <a href="https://github.com/RSSNext/Folo/releases/latest" target="_blank"><img src="https://github.com/user-attachments/assets/cf61e197-d756-4606-a8ad-fb591f79fdfc" alt="App Store" height="55"/></a> |
| Linux | <a href="https://github.com/RSSNext/Folo/releases/latest" target="_blank"><img src="https://github.com/user-attachments/assets/cf61e197-d756-4606-a8ad-fb591f79fdfc" alt="App Store" height="55"/></a> |
You can also install using the following methods maintained by our community:
- If you are using Arch Linux, you can install the package [folo-appimage](https://aur.archlinux.org/packages/folo-appimage) that is maintained by [timochan](https://github.com/ttimochan) and [grtsinry43](https://github.com/grtsinry43).
- If you are using Nix, you can install the package [follow](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/fo/follow/package.nix) that is maintained by [iosmanthus](https://github.com/iosmanthus).
- If you are using macOS with [Homebrew](https://brew.sh), you can install the cask [folo](https://formulae.brew.sh/cask/folo) that is maintained by [realSunyz](https://github.com/realSunyz).
- If you are using Windows with [Scoop](https://scoop.sh), you can install the manifest [folo](https://github.com/cscnk52/cetacea/blob/master/bucket/folo.json) that is maintained by [cscnk52](https://github.com/cscnk52).
| [](https://discord.gg/AwWcAQ7euc) | Join our Discord server to connect with developers, request features, and receive support. |
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- |
| [](https://x.com/intent/follow?screen_name=folo_is) | Follow us on X/Twitter for product updates and to join in on reward activities. |
> \[!IMPORTANT]
>
> **Star Us**, You will receive all release notifications from GitHub without any delay \~

<a href="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats?repo_id=783512367" target="_blank" style="display: block" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=783512367&image_size=auto&color_scheme=dark" width="655" height="auto">
<img alt="Performance Stats of RSSNext/Folo - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=783512367&image_size=auto&color_scheme=light" width="655" height="auto">
</picture>
</a>
## ✨ Features
### Customized Information Hub
Subscribe to a vast range of feeds and curated lists. Curate your favorites and keep track of what matters most to you.

### AI At Your Fingertips
A smarter and more efficient browsing with AI-powered features like translation, summary, and more.

### Dynamic Content Support
Because we know content is more than just text. From articles to videos, images to audio — Folo gets it all covered.

### More Than Just An App
This isn’t just another app. Folo is a community — introducing a new era of openness and community-driven experience.

## 🤝 Contributing
You are welcome to join the open source community to build together, please check our [Contributing Guide](./CONTRIBUTING.md) for more details.
## 🔏 Code signing policy
Folo for Windows uses free code signing provided by [SignPath.io](https://about.signpath.io/), a certificate by [SignPath Foundation](https://signpath.org/).
Folo for macOS and iOS is signed and notarized by [Apple Developer Program](https://developer.apple.com/programs/).
All released files are verified with [GitHub artifact attestations](https://github.com/RSSNext/Folo/attestations) to ensure their provenance and integrity.
## 📝 License
Folo is licensed under the GNU Affero General Public License version 3 with the addition of the following special exception:
All content in the `icons/mgc` directory is copyrighted by https://mgc.mingcute.com/ and cannot be redistributed.
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
We always recommend using the latest version of Follow to ensure you get all security updates.
## Reporting a Vulnerability
Please report security vulnerabilities to follow@rss3.io.
================================================
FILE: api/vercel_webhook.ts
================================================
import crypto from "node:crypto"
import type { VercelRequest, VercelResponse } from "@vercel/node"
import getRawBody from "raw-body"
export default async function handler(request: VercelRequest, response: VercelResponse) {
const { WEBHOOK_SECRET: INTEGRATION_SECRET } = process.env
if (typeof INTEGRATION_SECRET != "string") {
return response.status(400).json({
code: "invalid_secret",
error: "No integration secret found",
})
}
const rawBody = await getRawBody(request)
const bodySignature = sha1(rawBody, INTEGRATION_SECRET)
if (bodySignature !== request.headers["x-vercel-signature"]) {
return response.status(403).json({
code: "invalid_signature",
error: "signature didn't match",
})
}
const json = JSON.parse(rawBody.toString("utf-8"))
switch (json.type) {
// https://vercel.com/docs/observability/webhooks-overview/webhooks-api#deployment.succeeded
case "deployment.succeeded": {
const { target } = json.payload || json.data
if (target === "production") {
await purgeCloudflareCache()
} else {
console.info(`Skipping non-production deployment: ${target}`, json)
}
}
// ...
}
return response.status(200).end("OK")
}
function sha1(data: Buffer, secret: string): string {
return crypto.createHmac("sha1", secret).update(data).digest("hex")
}
export const config = {
api: {
bodyParser: false,
},
}
async function purgeCloudflareCache() {
const { CF_TOKEN, CF_ZONE_ID } = process.env
if (typeof CF_TOKEN !== "string" || typeof CF_ZONE_ID !== "string") {
throw new TypeError("No Cloudflare token or zone ID found")
}
const apiUrl = `https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache`
const manifestPath = await fetch(`https://app.folo.is/assets/manifest.txt?t=${Date.now()}`).then(
(res) => res.text(),
)
try {
await fetch(apiUrl, {
method: "POST",
headers: {
Authorization: CF_TOKEN,
},
body: JSON.stringify({
tags: ["follow-assets"],
}),
})
console.info("Successfully purged Cloudflare cache")
} catch {
console.error("Failed to purge Cloudflare cache by tags, fallback to purge by files")
const allPath = manifestPath.split("\n").map((path) => `https://app.folo.is/${path}`)
// Function to delay execution
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const taskPromise = [] as Promise<Response>[]
// Batch processing
for (let i = 0; i < allPath.length; i += 30) {
const batch = allPath.slice(i, i + 30)
const r = fetch(apiUrl, {
method: "POST",
headers: {
Authorization: CF_TOKEN,
},
body: JSON.stringify({
files: batch,
}),
})
taskPromise.push(r)
// Delay for 0.5 seconds between batches
if (i + 30 < allPath.length) {
await delay(500)
}
}
const result = await Promise.allSettled(taskPromise)
console.info(`Success: ${result.filter((r) => r.status === "fulfilled").length}`)
console.info(`Failed: ${result.filter((r) => r.status === "rejected").length}`)
}
}
================================================
FILE: apps/cli/package.json
================================================
{
"name": "folocli",
"type": "module",
"version": "0.0.1",
"description": "Folo CLI for terminal workflows and automation",
"author": "Folo Team",
"license": "AGPL-3.0-only",
"homepage": "https://github.com/RSSNext/Folo",
"repository": {
"type": "git",
"url": "git+https://github.com/RSSNext/Folo.git"
},
"keywords": [
"folo",
"rss",
"reader",
"cli",
"automation"
],
"bin": {
"folo": "./dist/index.js"
},
"files": [
"dist",
"skill.md"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsup --config tsup.config.ts && chmod +x dist/index.js",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"test": "pnpm run build && vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@follow-app/client-sdk": "catalog:",
"commander": "14.0.1",
"pathe": "2.0.3"
},
"devDependencies": {
"@follow/configs": "workspace:*",
"@types/node": "25.2.3",
"tsup": "8.5.0",
"tsx": "4.21.0",
"typescript": "catalog:"
}
}
================================================
FILE: apps/cli/skill.md
================================================
# Folo CLI Skill
## Trigger Conditions
Use this skill when a user asks to:
- Manage RSS subscriptions
- Browse timeline entries
- Read entry details or readability content
- Mark entries as read/unread
- Search feeds/lists or trending sources
- Import/export OPML
- Check unread counts
## Preconditions
1. Node.js and npm are installed so the CLI can be executed with `npx`.
2. Authentication is configured:
- `npx --yes folocli@latest login` (recommended, opens browser and auto-logins)
- or `npx --yes folocli@latest login --token <session-token>`
- or set `FOLO_TOKEN=<token>`
## Execution Policy
- Prefer `npx --yes folocli@latest ...` for all agent runs.
- Do not require `npm install -g folocli`.
- No separate update preflight is needed. Using `folocli@latest` is the update strategy.
- If a user already has a working global `folo` binary, it is acceptable, but `npx --yes folocli@latest` remains the recommended default in docs and automation.
## Output Contract
Default output is JSON with a stable envelope:
```json
{
"ok": true,
"data": {},
"error": null
}
```
Errors return:
```json
{
"ok": false,
"data": null,
"error": {
"code": "UNAUTHORIZED",
"message": "Token is invalid or expired."
}
}
```
You can switch output mode:
- `--format json` (default)
- `--format table`
- `--format plain`
## Core Workflows
### 1. Timeline Reading
1. Fetch timeline:
- `npx --yes folocli@latest timeline --limit 10`
2. Get entry detail:
- `npx --yes folocli@latest entry get <entryId>`
3. Get readability content:
- `npx --yes folocli@latest entry read <entryId>`
### 2. Subscription Management
1. Discover:
- `npx --yes folocli@latest search discover <keyword>`
2. Add subscription:
- `npx --yes folocli@latest subscription add --feed <url>`
- or `npx --yes folocli@latest subscription add --list <listId>`
3. List subscriptions:
- `npx --yes folocli@latest subscription list`
### 3. Unread Processing
1. Check unread total:
- `npx --yes folocli@latest unread count`
2. List unread subscriptions:
- `npx --yes folocli@latest unread list`
3. Read unread entries:
- `npx --yes folocli@latest timeline --unread-only --limit 20`
4. Mark read:
- `npx --yes folocli@latest entry mark-read <entryId>`
- or batch: `npx --yes folocli@latest entry mark-all-read --view articles`
### 4. Collection Operations
- Add: `npx --yes folocli@latest collection add <entryId>`
- Remove: `npx --yes folocli@latest collection remove <entryId>`
- List: `npx --yes folocli@latest collection list --limit 20`
### 5. OPML Import / Export
- Export:
- `npx --yes folocli@latest opml export --output backup.opml`
- Import:
- `npx --yes folocli@latest opml import feeds.opml`
## Pagination Pattern
`npx --yes folocli@latest timeline` returns:
- `entries`
- `nextCursor`
- `hasNext`
Loop until `hasNext` is `false`:
1. `npx --yes folocli@latest timeline --limit 20`
2. Read `nextCursor`
3. `npx --yes folocli@latest timeline --limit 20 --cursor <nextCursor>`
4. Repeat
## Command Reference
- `npx --yes folocli@latest login [--timeout <seconds>] [--token <token>]`
- `npx --yes folocli@latest logout`
- `npx --yes folocli@latest whoami`
- `npx --yes folocli@latest auth login [--timeout <seconds>] [--token <token>]`
- `npx --yes folocli@latest auth logout`
- `npx --yes folocli@latest auth whoami`
- `npx --yes folocli@latest timeline [--view <type>] [--limit <n>] [--unread-only] [--cursor <datetime>]`
- `npx --yes folocli@latest timeline --feed <feedId> [--limit <n>] [--cursor <datetime>]`
- `npx --yes folocli@latest timeline --list <listId> [--limit <n>] [--cursor <datetime>]`
- `npx --yes folocli@latest timeline --category <name> [--view <type>] [--limit <n>]`
- `npx --yes folocli@latest subscription list [--view <type>] [--category <name>]`
- `npx --yes folocli@latest subscription add --feed <url> [--category <name>] [--view <type>] [--private]`
- `npx --yes folocli@latest subscription add --list <listId> [--category <name>] [--view <type>]`
- `npx --yes folocli@latest subscription remove <id> [--target feed|list|url]`
- `npx --yes folocli@latest subscription update <id> [--target feed|list] [--category <name>] [--title <title>] [--view <type>] [--private|--public]`
- `npx --yes folocli@latest entry get <entryId>`
- `npx --yes folocli@latest entry read <entryId>`
- `npx --yes folocli@latest entry mark-read <entryId>`
- `npx --yes folocli@latest entry mark-unread <entryId>`
- `npx --yes folocli@latest entry mark-all-read [--feed <feedId>] [--list <listId>] [--view <type>]`
- `npx --yes folocli@latest feed get <feedId|feedUrl>`
- `npx --yes folocli@latest feed refresh <feedId>`
- `npx --yes folocli@latest feed analytics <feedId>`
- `npx --yes folocli@latest list ls`
- `npx --yes folocli@latest list get <listId>`
- `npx --yes folocli@latest list create --title <title> [--description <desc>] [--view <type>] [--fee <n>]`
- `npx --yes folocli@latest list update <listId> [--title <title>] [--description <desc>] [--view <type>] [--fee <n>]`
- `npx --yes folocli@latest list delete <listId>`
- `npx --yes folocli@latest list add-feed <listId> --feed <feedId>`
- `npx --yes folocli@latest list remove-feed <listId> --feed <feedId>`
- `npx --yes folocli@latest search discover <keyword> [--type feeds|lists]`
- `npx --yes folocli@latest search rsshub <keyword> [--lang <lang>]`
- `npx --yes folocli@latest search trending [--range 1d|3d|7d|30d] [--view <type>] [--limit <n>] [--language eng|cmn] [--category <keyword>]`
- `npx --yes folocli@latest collection list [--limit <n>] [--cursor <datetime>]`
- `npx --yes folocli@latest collection add <entryId> [--view <type>]`
- `npx --yes folocli@latest collection remove <entryId>`
- `npx --yes folocli@latest opml export [--output <file>]`
- `npx --yes folocli@latest opml import <file> [--items <url1,url2,...>]`
- `npx --yes folocli@latest unread count`
- `npx --yes folocli@latest unread list [--view <type>]`
## Error Recovery
- `UNAUTHORIZED`
- Re-login: `npx --yes folocli@latest login`
- or `npx --yes folocli@latest login --token <token>`
- Or set `FOLO_TOKEN`
- `HTTP_4xx` / `HTTP_5xx`
- Retry with `--verbose` for request details
- Verify `--api-url` if using non-default endpoint
- `INVALID_ARGUMENT`
- Run `npx --yes folocli@latest <command> --help` to inspect accepted options
================================================
FILE: apps/cli/src/args.test.ts
================================================
import { describe, expect, it } from "vitest"
import { parseFormat, parseISODate, parseNonNegativeInt, parsePositiveInt, parseView } from "./args"
describe("args parsers", () => {
it("parses named view values", () => {
expect(parseView("articles")).toBe(0)
expect(parseView("social")).toBe(1)
expect(parseView("pictures")).toBe(2)
expect(parseView("videos")).toBe(3)
expect(parseView("audio")).toBe(4)
expect(parseView("notifications")).toBe(5)
})
it("parses numeric view values", () => {
expect(parseView("0")).toBe(0)
expect(parseView("5")).toBe(5)
})
it("throws for invalid view values", () => {
expect(() => parseView("foo")).toThrowError(/Invalid view/)
expect(() => parseView("6")).toThrowError(/Invalid view/)
})
it("parses positive integers", () => {
expect(parsePositiveInt("1")).toBe(1)
expect(parsePositiveInt("99")).toBe(99)
})
it("throws for non-positive integers", () => {
expect(() => parsePositiveInt("0")).toThrowError(/positive integer/)
expect(() => parsePositiveInt("-1")).toThrowError(/positive integer/)
})
it("parses non-negative integers", () => {
expect(parseNonNegativeInt("0")).toBe(0)
expect(parseNonNegativeInt("3")).toBe(3)
})
it("throws for negative integers", () => {
expect(() => parseNonNegativeInt("-1")).toThrowError(/non-negative integer/)
})
it("parses ISO datetime", () => {
expect(parseISODate("2
gitextract_ni4pl4rv/
├── .agents/
│ ├── settings.local.json
│ └── skills/
│ ├── desktop-release/
│ │ └── SKILL.md
│ ├── installing-mobile-preview-builds/
│ │ └── SKILL.md
│ ├── mobile-e2e/
│ │ └── SKILL.md
│ ├── mobile-release/
│ │ └── SKILL.md
│ ├── mobile-self-test/
│ │ └── SKILL.md
│ └── update-deps/
│ └── SKILL.md
├── .cursorignore
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ ├── i18n.yml
│ │ └── typo.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── actions/
│ │ ├── setup-version/
│ │ │ └── action.yml
│ │ └── setup-xcode/
│ │ └── action.yml
│ ├── advanced-issue-labeler.yml
│ ├── copilot-instructions.md
│ ├── dependabot.yaml
│ ├── prompts/
│ │ └── similar_issues.prompt.yml
│ ├── scripts/
│ │ └── extract-release-info.mjs
│ └── workflows/
│ ├── build-android.yml
│ ├── build-desktop.yml
│ ├── build-ios-development.yml
│ ├── build-ios.yml
│ ├── build-web.yml
│ ├── deploy-cloudflare-desktop.yml
│ ├── deploy-cloudflare-landing.yml
│ ├── deploy-cloudflare-ssr.yml
│ ├── issue-labeler.yml
│ ├── lint.yml
│ ├── pr-title-check.yml
│ ├── similar-issues.yml
│ ├── sync.yaml
│ ├── tag.yml
│ └── translator.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.mjs
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── AGENTS.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── api/
│ └── vercel_webhook.ts
├── apps/
│ ├── cli/
│ │ ├── package.json
│ │ ├── skill.md
│ │ ├── src/
│ │ │ ├── args.test.ts
│ │ │ ├── args.ts
│ │ │ ├── browser-login.test.ts
│ │ │ ├── browser-login.ts
│ │ │ ├── cli.e2e.test.ts
│ │ │ ├── client.ts
│ │ │ ├── command.ts
│ │ │ ├── commands/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── collection.ts
│ │ │ │ ├── entry.ts
│ │ │ │ ├── feed.ts
│ │ │ │ ├── list.ts
│ │ │ │ ├── opml.ts
│ │ │ │ ├── search.ts
│ │ │ │ ├── subscription.ts
│ │ │ │ ├── timeline.ts
│ │ │ │ └── unread.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ ├── output.test.ts
│ │ │ └── output.ts
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts
│ │ └── vitest.config.ts
│ ├── desktop/
│ │ ├── .env.example
│ │ ├── AGENTS.md
│ │ ├── build/
│ │ │ ├── appxmanifest-template.xml
│ │ │ ├── dev.pfx
│ │ │ ├── entitlements.mac.plist
│ │ │ ├── entitlements.mas.child.plist
│ │ │ └── entitlements.mas.plist
│ │ ├── bump.config.ts
│ │ ├── bump.hotfix.config.js
│ │ ├── changelog/
│ │ │ ├── 0.1.2.md
│ │ │ ├── 0.2.0.md
│ │ │ ├── 0.2.1.md
│ │ │ ├── 0.2.2.md
│ │ │ ├── 0.2.3.md
│ │ │ ├── 0.2.4.md
│ │ │ ├── 0.2.5.md
│ │ │ ├── 0.2.6.md
│ │ │ ├── 0.2.7.md
│ │ │ ├── 0.2.8.md
│ │ │ ├── 0.2.9.md
│ │ │ ├── 0.3.0.md
│ │ │ ├── 0.3.1.md
│ │ │ ├── 0.3.10.md
│ │ │ ├── 0.3.11.md
│ │ │ ├── 0.3.12.md
│ │ │ ├── 0.3.13.md
│ │ │ ├── 0.3.2.md
│ │ │ ├── 0.3.3.md
│ │ │ ├── 0.3.4.md
│ │ │ ├── 0.3.5.md
│ │ │ ├── 0.3.6.md
│ │ │ ├── 0.3.7.md
│ │ │ ├── 0.3.8.md
│ │ │ ├── 0.3.9.md
│ │ │ ├── 0.4.0.md
│ │ │ ├── 0.4.1.md
│ │ │ ├── 0.4.2.md
│ │ │ ├── 0.4.3.md
│ │ │ ├── 0.4.4.md
│ │ │ ├── 0.4.5.md
│ │ │ ├── 0.4.6.md
│ │ │ ├── 0.4.8.md
│ │ │ ├── 0.5.0.md
│ │ │ ├── 0.6.0.md
│ │ │ ├── 0.6.1.md
│ │ │ ├── 0.6.2.md
│ │ │ ├── 0.6.3.md
│ │ │ ├── 0.7.0.md
│ │ │ ├── 0.8.0.md
│ │ │ ├── 0.9.0.md
│ │ │ ├── 1.0.0.md
│ │ │ ├── 1.1.0.md
│ │ │ ├── 1.2.2.md
│ │ │ ├── 1.2.6.md
│ │ │ ├── 1.3.0.md
│ │ │ ├── 1.3.1.md
│ │ │ ├── 1.4.0.md
│ │ │ ├── next.md
│ │ │ └── next.template.md
│ │ ├── configs/
│ │ │ ├── vite.electron-render.config.ts
│ │ │ └── vite.render.config.ts
│ │ ├── dev-only/
│ │ │ └── dev-app-update.yml
│ │ ├── e2e/
│ │ │ ├── playwright.config.ts
│ │ │ ├── scripts/
│ │ │ │ └── capture-ui-audit.ts
│ │ │ ├── support/
│ │ │ │ ├── account.ts
│ │ │ │ ├── app.ts
│ │ │ │ ├── auth-bootstrap.ts
│ │ │ │ ├── electron.ts
│ │ │ │ └── env.ts
│ │ │ └── tests/
│ │ │ ├── electron/
│ │ │ │ └── core.spec.ts
│ │ │ └── web/
│ │ │ ├── core.spec.ts
│ │ │ └── settings-sync.spec.ts
│ │ ├── electron.vite.config.ts
│ │ ├── forge.config.cts
│ │ ├── layer/
│ │ │ ├── main/
│ │ │ │ ├── export.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── preload/
│ │ │ │ │ ├── index.d.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── src/
│ │ │ │ │ ├── @types/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── i18next.d.ts
│ │ │ │ │ │ └── resources.ts
│ │ │ │ │ ├── before-bootstrap.ts
│ │ │ │ │ ├── bootstrap.ts
│ │ │ │ │ ├── constants/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ └── system.ts
│ │ │ │ │ ├── env.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ipc/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── services/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ ├── auth.ts
│ │ │ │ │ │ ├── cli.ts
│ │ │ │ │ │ ├── debug.ts
│ │ │ │ │ │ ├── dock.ts
│ │ │ │ │ │ ├── integration.ts
│ │ │ │ │ │ ├── menu.ts
│ │ │ │ │ │ ├── reader.ts
│ │ │ │ │ │ └── setting.ts
│ │ │ │ │ ├── lib/
│ │ │ │ │ │ ├── api-client.ts
│ │ │ │ │ │ ├── auth-cookie-migration.ts
│ │ │ │ │ │ ├── cleaner.ts
│ │ │ │ │ │ ├── cli-session-sync.ts
│ │ │ │ │ │ ├── dock.ts
│ │ │ │ │ │ ├── download.ts
│ │ │ │ │ │ ├── i18n.ts
│ │ │ │ │ │ ├── proxy.test.ts
│ │ │ │ │ │ ├── proxy.ts
│ │ │ │ │ │ ├── router.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── tray.ts
│ │ │ │ │ │ ├── user.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── logger.ts
│ │ │ │ │ ├── manager/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ ├── bootstrap.ts
│ │ │ │ │ │ ├── lifecycle.ts
│ │ │ │ │ │ └── window.ts
│ │ │ │ │ ├── menu.ts
│ │ │ │ │ ├── modules/
│ │ │ │ │ │ └── language-detection/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── shims/
│ │ │ │ │ │ └── utf-8-validate.cjs
│ │ │ │ │ └── updater/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── configs.ts
│ │ │ │ │ ├── follow-update-provider.ts
│ │ │ │ │ ├── hot-updater.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── logger.ts
│ │ │ │ │ └── windows-updater.ts
│ │ │ │ ├── tsconfig.json
│ │ │ │ └── vitest.config.ts
│ │ │ └── renderer/
│ │ │ ├── debug_proxy.html
│ │ │ ├── global.d.ts
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── pwa-assets.config.ts
│ │ │ ├── setup-file.ts
│ │ │ ├── src/
│ │ │ │ ├── @types/
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── default-resource.electron.ts
│ │ │ │ │ ├── default-resource.ts
│ │ │ │ │ └── i18next.d.ts
│ │ │ │ ├── App.tsx
│ │ │ │ ├── atoms/
│ │ │ │ │ ├── ai-summary.ts
│ │ │ │ │ ├── ai-translation.ts
│ │ │ │ │ ├── app.ts
│ │ │ │ │ ├── context-menu.ts
│ │ │ │ │ ├── corner-player.ts
│ │ │ │ │ ├── debug-feature.ts
│ │ │ │ │ ├── dom.ts
│ │ │ │ │ ├── lang.ts
│ │ │ │ │ ├── network.ts
│ │ │ │ │ ├── player.ts
│ │ │ │ │ ├── popover.ts
│ │ │ │ │ ├── preview.ts
│ │ │ │ │ ├── readability.ts
│ │ │ │ │ ├── server-configs.ts
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── ai.ts
│ │ │ │ │ │ ├── general.ts
│ │ │ │ │ │ ├── integration.ts
│ │ │ │ │ │ └── ui.ts
│ │ │ │ │ ├── sidebar.ts
│ │ │ │ │ ├── source-content.tsx
│ │ │ │ │ ├── updater.ts
│ │ │ │ │ └── user.ts
│ │ │ │ ├── components/
│ │ │ │ │ ├── common/
│ │ │ │ │ │ ├── AppErrorBoundary.tsx
│ │ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ │ ├── ErrorElement.tsx
│ │ │ │ │ │ ├── ErrorTooltip.tsx
│ │ │ │ │ │ ├── ExPromise.tsx
│ │ │ │ │ │ ├── Focusable.tsx
│ │ │ │ │ │ ├── Fragment.tsx
│ │ │ │ │ │ ├── ImpressionTracker.tsx
│ │ │ │ │ │ ├── LCPEndDetector.tsx
│ │ │ │ │ │ ├── LoadMoreIndicator.tsx
│ │ │ │ │ │ ├── LoadRemixAsyncComponent.tsx
│ │ │ │ │ │ ├── Motion.tsx
│ │ │ │ │ │ ├── NotFound.tsx
│ │ │ │ │ │ ├── PoweredByFooter.tsx
│ │ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ │ ├── ReloadPrompt.tsx
│ │ │ │ │ │ ├── ShadowDOM.tsx
│ │ │ │ │ │ ├── SharePanel.tsx
│ │ │ │ │ │ └── withAppErrorBoundary.tsx
│ │ │ │ │ ├── errors/
│ │ │ │ │ │ ├── EntryNotFound.tsx
│ │ │ │ │ │ ├── FeedNotFound.tsx
│ │ │ │ │ │ ├── ModalError.tsx
│ │ │ │ │ │ ├── PageError.tsx
│ │ │ │ │ │ ├── RSSHubError.tsx
│ │ │ │ │ │ ├── enum.ts
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── mobile/
│ │ │ │ │ │ └── button.tsx
│ │ │ │ │ ├── ui/
│ │ │ │ │ │ ├── ai-summary-card/
│ │ │ │ │ │ │ ├── AISummaryCardBase.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── auto-completion/
│ │ │ │ │ │ │ ├── AutoCompletion.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── background/
│ │ │ │ │ │ │ ├── WindowUnderBlur.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── button/
│ │ │ │ │ │ │ ├── AnimatedCommandButton.tsx
│ │ │ │ │ │ │ ├── CommandActionButton.tsx
│ │ │ │ │ │ │ ├── CopyButton.tsx
│ │ │ │ │ │ │ ├── GlassButton.tsx
│ │ │ │ │ │ │ └── HeaderActionButton.tsx
│ │ │ │ │ │ ├── code-highlighter/
│ │ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── shiki/
│ │ │ │ │ │ │ ├── Shiki.tsx
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── shared.ts
│ │ │ │ │ │ │ └── shiki.module.css
│ │ │ │ │ │ ├── crop/
│ │ │ │ │ │ │ └── AvatarUploadModal.tsx
│ │ │ │ │ │ ├── datetime/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ │ └── dropdown-menu.tsx
│ │ │ │ │ │ ├── fab/
│ │ │ │ │ │ │ ├── FABContainer.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── hover-preview/
│ │ │ │ │ │ │ ├── EntryPreviewCard.tsx
│ │ │ │ │ │ │ ├── FeedPreviewCard.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── keyboard-recorder/
│ │ │ │ │ │ │ ├── KeyRecorder.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── markdown/
│ │ │ │ │ │ │ ├── HTML.tsx
│ │ │ │ │ │ │ ├── Markdown.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ ├── Toc.tsx
│ │ │ │ │ │ │ │ ├── TocItem.tsx
│ │ │ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── renderers/
│ │ │ │ │ │ │ │ ├── BlockErrorBoundary.tsx
│ │ │ │ │ │ │ │ ├── BlockImage.tsx
│ │ │ │ │ │ │ │ ├── Heading.tsx
│ │ │ │ │ │ │ │ ├── InlineImage.tsx
│ │ │ │ │ │ │ │ ├── MarkdownLink.tsx
│ │ │ │ │ │ │ │ ├── MarkdownP.tsx
│ │ │ │ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── media/
│ │ │ │ │ │ │ ├── Media.tsx
│ │ │ │ │ │ │ ├── MediaContainerWidthContext.tsx
│ │ │ │ │ │ │ ├── MediaContainerWidthProvider.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecord.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecordContext.tsx
│ │ │ │ │ │ │ ├── MediaInfoRecordProvider.tsx
│ │ │ │ │ │ │ ├── PreviewMediaContent.tsx
│ │ │ │ │ │ │ ├── SwipeMedia.tsx
│ │ │ │ │ │ │ ├── VideoPlayer.tsx
│ │ │ │ │ │ │ ├── VolumeSlider.tsx
│ │ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ │ ├── modal/
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── close.tsx
│ │ │ │ │ │ │ ├── helper/
│ │ │ │ │ │ │ │ ├── useAsyncModal.tsx
│ │ │ │ │ │ │ │ └── useModalStackCalculationAndEffect.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── inspire/
│ │ │ │ │ │ │ │ ├── InPeekModal.tsx
│ │ │ │ │ │ │ │ └── PeekModal.tsx
│ │ │ │ │ │ │ └── stacked/
│ │ │ │ │ │ │ ├── AsyncModalContent.tsx
│ │ │ │ │ │ │ ├── atom.ts
│ │ │ │ │ │ │ ├── bus.ts
│ │ │ │ │ │ │ ├── components.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── custom-modal.tsx
│ │ │ │ │ │ │ ├── declarative-modal.tsx
│ │ │ │ │ │ │ ├── helper.tsx
│ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── internal/
│ │ │ │ │ │ │ │ ├── use-animate.ts
│ │ │ │ │ │ │ │ ├── use-drag.ts
│ │ │ │ │ │ │ │ ├── use-select.ts
│ │ │ │ │ │ │ │ └── use-subscriber.ts
│ │ │ │ │ │ │ ├── modal-stack.tsx
│ │ │ │ │ │ │ ├── modal.tsx
│ │ │ │ │ │ │ ├── overlay.tsx
│ │ │ │ │ │ │ ├── provider.tsx
│ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ ├── paper/
│ │ │ │ │ │ │ ├── Paper.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── peek-modal/
│ │ │ │ │ │ ├── EntryModalPreview.tsx
│ │ │ │ │ │ ├── EntryMoreActions.tsx
│ │ │ │ │ │ └── EntryToastPreview.tsx
│ │ │ │ │ └── ux/
│ │ │ │ │ ├── pull-to-refresh/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── transition/
│ │ │ │ │ └── icon.tsx
│ │ │ │ ├── constants/
│ │ │ │ │ ├── app.tsx
│ │ │ │ │ ├── copy.ts
│ │ │ │ │ ├── dom.ts
│ │ │ │ │ ├── env.ts
│ │ │ │ │ ├── hotkeys.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── ui.ts
│ │ │ │ ├── env.d.ts
│ │ │ │ ├── errors/
│ │ │ │ │ └── CustomSafeError.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── biz/
│ │ │ │ │ │ ├── useAsRead.ts
│ │ │ │ │ │ ├── useContextMenuActionShortCutTrigger.ts
│ │ │ │ │ │ ├── useDiscoverRSSHubRoute.tsx
│ │ │ │ │ │ ├── useEntryActions.tsx
│ │ │ │ │ │ ├── useEntryContextMenu.ts
│ │ │ │ │ │ ├── useFeature.ts
│ │ │ │ │ │ ├── useFeedActions.tsx
│ │ │ │ │ │ ├── useFollow.tsx
│ │ │ │ │ │ ├── useNavigateEntry.ts
│ │ │ │ │ │ ├── usePeekModal.tsx
│ │ │ │ │ │ ├── useProxySetting.ts
│ │ │ │ │ │ ├── useReduceMotion.ts
│ │ │ │ │ │ ├── useRenderStyle.tsx
│ │ │ │ │ │ ├── useRouteParams.ts
│ │ │ │ │ │ ├── useShowEntryDetailsColumn.ts
│ │ │ │ │ │ ├── useSubscriptionActions.tsx
│ │ │ │ │ │ ├── useTimelineList.ts
│ │ │ │ │ │ └── useTraySetting.ts
│ │ │ │ │ └── common/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── useBizQuery.ts
│ │ │ │ │ ├── useContextMenu.tsx
│ │ │ │ │ ├── useFeedSafeUrl.ts
│ │ │ │ │ ├── useI18n.ts
│ │ │ │ │ ├── useLoginModal.tsx
│ │ │ │ │ ├── usePreventOverscrollBounce.ts
│ │ │ │ │ ├── useRecaptchaToken.ts
│ │ │ │ │ ├── useRequireLogin.ts
│ │ │ │ │ └── useSyncTheme.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── initialize/
│ │ │ │ │ ├── analytics.ts
│ │ │ │ │ ├── global-shortcuts.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── history.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── migrates/
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── v/
│ │ │ │ │ │ └── v1.ts
│ │ │ │ │ ├── queue.ts
│ │ │ │ │ └── settings.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── parse-html.test.ts
│ │ │ │ │ ├── api-client.ts
│ │ │ │ │ ├── app.ts
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── avatar-upload.ts
│ │ │ │ │ ├── client-session.ts
│ │ │ │ │ ├── client.ts
│ │ │ │ │ ├── clipboard.ts
│ │ │ │ │ ├── defineQuery.ts
│ │ │ │ │ ├── dev.tsx
│ │ │ │ │ ├── error-parser.ts
│ │ │ │ │ ├── export.ts
│ │ │ │ │ ├── features.tsx
│ │ │ │ │ ├── ga4.ts
│ │ │ │ │ ├── img-proxy.ts
│ │ │ │ │ ├── issues.ts
│ │ │ │ │ ├── jotai.ts
│ │ │ │ │ ├── language.ts
│ │ │ │ │ ├── load-language.ts
│ │ │ │ │ ├── log.ts
│ │ │ │ │ ├── native-menu.ts
│ │ │ │ │ ├── observe-resize.ts
│ │ │ │ │ ├── parse-html.ts
│ │ │ │ │ ├── parse-markdown.ts
│ │ │ │ │ ├── parsers.ts
│ │ │ │ │ ├── query-client.ts
│ │ │ │ │ ├── simple-text-selection.ts
│ │ │ │ │ ├── url-builder.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── main.tsx
│ │ │ │ ├── modules/
│ │ │ │ │ ├── action/
│ │ │ │ │ │ ├── action-setting.tsx
│ │ │ │ │ │ ├── constants.tsx
│ │ │ │ │ │ ├── rule-card.tsx
│ │ │ │ │ │ ├── rule-summary.ts
│ │ │ │ │ │ ├── then-section.tsx
│ │ │ │ │ │ ├── utils.ts
│ │ │ │ │ │ └── when-section.tsx
│ │ │ │ │ ├── ai-chat/
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ └── session.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── 3d-models/
│ │ │ │ │ │ │ │ ├── AISpline.ts
│ │ │ │ │ │ │ │ └── AISplineLoader.tsx
│ │ │ │ │ │ │ ├── context-bar/
│ │ │ │ │ │ │ │ ├── MentionButton.tsx
│ │ │ │ │ │ │ │ ├── blocks/
│ │ │ │ │ │ │ │ │ ├── ContextBlock.tsx
│ │ │ │ │ │ │ │ │ ├── TitleComponents.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── menus/
│ │ │ │ │ │ │ │ ├── ShortcutsMenuContent.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── displays/
│ │ │ │ │ │ │ │ ├── AIChainOfThought.tsx
│ │ │ │ │ │ │ │ ├── AIDisplayFlowPart.tsx
│ │ │ │ │ │ │ │ ├── AIReasoningPart.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── share.tsx
│ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ ├── AnalyticsMetrics.tsx
│ │ │ │ │ │ │ │ ├── CategoryTag.tsx
│ │ │ │ │ │ │ │ ├── DisplayHeader.tsx
│ │ │ │ │ │ │ │ ├── EmptyState.tsx
│ │ │ │ │ │ │ │ ├── FeedItemCard.tsx
│ │ │ │ │ │ │ │ ├── GroupedContent.tsx
│ │ │ │ │ │ │ │ ├── StatCard.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── file/
│ │ │ │ │ │ │ │ └── GlobalFileDropZone.tsx
│ │ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ │ ├── AIChatContextBar.tsx
│ │ │ │ │ │ │ │ ├── AIChatRoot.tsx
│ │ │ │ │ │ │ │ ├── AIChatSendButton.tsx
│ │ │ │ │ │ │ │ ├── AIErrorFallback.tsx
│ │ │ │ │ │ │ │ ├── AIModelIndicator.tsx
│ │ │ │ │ │ │ │ ├── AISmartSidebar.css
│ │ │ │ │ │ │ │ ├── AISmartSidebar.tsx
│ │ │ │ │ │ │ │ ├── ChatBottomPanel.tsx
│ │ │ │ │ │ │ │ ├── ChatHeader.tsx
│ │ │ │ │ │ │ │ ├── ChatHistoryDropdown.tsx
│ │ │ │ │ │ │ │ ├── ChatInput.tsx
│ │ │ │ │ │ │ │ ├── ChatInterface.tsx
│ │ │ │ │ │ │ │ ├── ChatMessageContainer.tsx
│ │ │ │ │ │ │ │ ├── ChatMoreDropdown.tsx
│ │ │ │ │ │ │ │ ├── ChatShortcutsRow.tsx
│ │ │ │ │ │ │ │ ├── ChatTitle.tsx
│ │ │ │ │ │ │ │ ├── Messages.tsx
│ │ │ │ │ │ │ │ ├── RateLimitNotice.tsx
│ │ │ │ │ │ │ │ ├── ScrollToBottomButton.tsx
│ │ │ │ │ │ │ │ ├── TaskReportDropdown.tsx
│ │ │ │ │ │ │ │ ├── WelcomeScreen.tsx
│ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ ├── ChatSessionComponents.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── useChatSessionHandlers.tsx
│ │ │ │ │ │ │ │ └── utils.ts
│ │ │ │ │ │ │ ├── message/
│ │ │ │ │ │ │ │ ├── AIChatMessage.tsx
│ │ │ │ │ │ │ │ ├── AIDataBlockPart.tsx
│ │ │ │ │ │ │ │ ├── AIMarkdownMessage.tsx
│ │ │ │ │ │ │ │ ├── AIMessageIdContext.tsx
│ │ │ │ │ │ │ │ ├── AIMessageParts.tsx
│ │ │ │ │ │ │ │ ├── BlockTitleComponents.tsx
│ │ │ │ │ │ │ │ ├── EditableMessage.tsx
│ │ │ │ │ │ │ │ ├── ErrorMessage.tsx
│ │ │ │ │ │ │ │ ├── ImageThumbnail.tsx
│ │ │ │ │ │ │ │ ├── TokenUsagePill.tsx
│ │ │ │ │ │ │ │ ├── ToolInvocationComponent.tsx
│ │ │ │ │ │ │ │ ├── UserChatMessage.tsx
│ │ │ │ │ │ │ │ ├── UserMessageParts.tsx
│ │ │ │ │ │ │ │ ├── UserRichTextMessage.tsx
│ │ │ │ │ │ │ │ ├── ai-block-constants.ts
│ │ │ │ │ │ │ │ ├── animated/
│ │ │ │ │ │ │ │ │ ├── AnimatedMarkdown.tsx
│ │ │ │ │ │ │ │ │ ├── TokenizedText.tsx
│ │ │ │ │ │ │ │ │ └── constants.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── parse-incomplete-markdown.ts
│ │ │ │ │ │ │ │ └── useContextBlockPresentation.tsx
│ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ └── common-states.tsx
│ │ │ │ │ │ │ ├── ui/
│ │ │ │ │ │ │ │ ├── AIShortcutButton.tsx
│ │ │ │ │ │ │ │ ├── ShortcutTooltip.tsx
│ │ │ │ │ │ │ │ └── UploadProgress.tsx
│ │ │ │ │ │ │ └── welcome/
│ │ │ │ │ │ │ ├── DefaultWelcomeContent.tsx
│ │ │ │ │ │ │ ├── EntrySummaryCard.tsx
│ │ │ │ │ │ │ ├── EntryWelcomeContent.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── editor/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── plugins/
│ │ │ │ │ │ │ ├── file-upload/
│ │ │ │ │ │ │ │ ├── FileAttachmentNode.tsx
│ │ │ │ │ │ │ │ ├── FileUploadPlugin.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ └── FileDropZone.tsx
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── useFileAttachmentBlockSync.ts
│ │ │ │ │ │ │ │ │ └── useFileUploadIntegration.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ └── file-handling.ts
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── mention/
│ │ │ │ │ │ │ │ ├── MentionNode.tsx
│ │ │ │ │ │ │ │ ├── MentionPlugin.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── MentionComponent.tsx
│ │ │ │ │ │ │ │ │ ├── MentionDropdown.tsx
│ │ │ │ │ │ │ │ │ └── shared/
│ │ │ │ │ │ │ │ │ └── MentionTypeIcon.tsx
│ │ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── dateMentionConfig.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionParsers.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionSearch.ts
│ │ │ │ │ │ │ │ │ ├── dateMentionUtils.ts
│ │ │ │ │ │ │ │ │ ├── useMentionKeyboard.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSearch.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSearchService.ts
│ │ │ │ │ │ │ │ │ ├── useMentionSelection.ts
│ │ │ │ │ │ │ │ │ └── useMentionTrigger.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ ├── mentionTextValue.ts
│ │ │ │ │ │ │ │ ├── parseNaturalLanguageDate.ts
│ │ │ │ │ │ │ │ ├── textReplacement.ts
│ │ │ │ │ │ │ │ └── triggerDetection.ts
│ │ │ │ │ │ │ ├── selection/
│ │ │ │ │ │ │ │ ├── SelectedTextNode.tsx
│ │ │ │ │ │ │ │ ├── SelectedTextNodeComponent.tsx
│ │ │ │ │ │ │ │ ├── SelectedTextPlugin.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── insertSelectedTextNode.ts
│ │ │ │ │ │ │ │ └── selectedTextBridge.ts
│ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── MentionLikePill.tsx
│ │ │ │ │ │ │ │ │ ├── TypeaheadDropdown.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── useListKeyboardNavigation.ts
│ │ │ │ │ │ │ │ │ ├── useTextTrigger.ts
│ │ │ │ │ │ │ │ │ └── useTypeaheadSelection.ts
│ │ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ │ └── positioning.ts
│ │ │ │ │ │ │ └── shortcut/
│ │ │ │ │ │ │ ├── ShortcutNode.tsx
│ │ │ │ │ │ │ ├── ShortcutPlugin.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ ├── ShortcutComponent.tsx
│ │ │ │ │ │ │ │ └── ShortcutDropdown.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ ├── useShortcutKeyboard.ts
│ │ │ │ │ │ │ │ ├── useShortcutSearch.ts
│ │ │ │ │ │ │ │ ├── useShortcutSearchService.ts
│ │ │ │ │ │ │ │ ├── useShortcutSelection.ts
│ │ │ │ │ │ │ │ └── useShortcutTrigger.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ └── utils/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── positioning.ts
│ │ │ │ │ │ │ ├── shortcutTextValue.ts
│ │ │ │ │ │ │ ├── textReplacement.ts
│ │ │ │ │ │ │ └── triggerDetection.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── useAIConfiguration.ts
│ │ │ │ │ │ │ ├── useAIModel.ts
│ │ │ │ │ │ │ ├── useAIShortcut.ts
│ │ │ │ │ │ │ ├── useAttachScrollBeyond.tsx
│ │ │ │ │ │ │ ├── useAutoScroll.tsx
│ │ │ │ │ │ │ ├── useAutoTimelineSummaryShortcut.ts
│ │ │ │ │ │ │ ├── useChatHistory.ts
│ │ │ │ │ │ │ ├── useDisplayBlocks.ts
│ │ │ │ │ │ │ ├── useFeedEntrySearchService.ts
│ │ │ │ │ │ │ ├── useFileUpload.ts
│ │ │ │ │ │ │ ├── useLoadMessages.ts
│ │ │ │ │ │ │ ├── useMainEntryId.ts
│ │ │ │ │ │ │ ├── useSendAIShortcut.ts
│ │ │ │ │ │ │ └── useTimelineSummaryAutoContext.ts
│ │ │ │ │ │ ├── services/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── store/
│ │ │ │ │ │ │ ├── AIChatContext.ts
│ │ │ │ │ │ │ ├── chat-core/
│ │ │ │ │ │ │ │ ├── chat-actions.ts
│ │ │ │ │ │ │ │ ├── chat-instance.ts
│ │ │ │ │ │ │ │ ├── chat-state.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ ├── event-system/
│ │ │ │ │ │ │ │ ├── event-emitter.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── slices/
│ │ │ │ │ │ │ │ ├── block.slice.ts
│ │ │ │ │ │ │ │ ├── chat.slice.ts
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ │ ├── transport.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── types/
│ │ │ │ │ │ │ └── ChatSession.ts
│ │ │ │ │ │ └── utils/
│ │ │ │ │ │ ├── error.ts
│ │ │ │ │ │ ├── extractor.ts
│ │ │ │ │ │ ├── file-processing.ts
│ │ │ │ │ │ ├── file-validation.ts
│ │ │ │ │ │ ├── mentionDate.ts
│ │ │ │ │ │ ├── rate-limit.ts
│ │ │ │ │ │ ├── shortcut.ts
│ │ │ │ │ │ └── titleGeneration.ts
│ │ │ │ │ ├── ai-chat-session/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── query.ts
│ │ │ │ │ │ ├── service.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── ai-onboarding/
│ │ │ │ │ │ ├── ai-chat-pane.tsx
│ │ │ │ │ │ ├── ai-onboarding-modal-content.tsx
│ │ │ │ │ │ ├── feeds-selection-list.tsx
│ │ │ │ │ │ ├── modal.tsx
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── ai-task/
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── ai-item-actions.tsx
│ │ │ │ │ │ │ ├── ai-task-modal.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── notify-channels-config.tsx
│ │ │ │ │ │ │ ├── schedule-config.tsx
│ │ │ │ │ │ │ ├── task-item.tsx
│ │ │ │ │ │ │ └── task-list.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── query.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── app/
│ │ │ │ │ │ ├── EnvironmentIndicator.tsx
│ │ │ │ │ │ ├── NetworkStatusIndicator.tsx
│ │ │ │ │ │ └── Titlebar.tsx
│ │ │ │ │ ├── app-layout/
│ │ │ │ │ │ ├── LAYOUT_ARCHITECTURE.md
│ │ │ │ │ │ ├── MainDestopLayout.tsx
│ │ │ │ │ │ ├── ai/
│ │ │ │ │ │ │ ├── AIChatFixedPanel.tsx
│ │ │ │ │ │ │ ├── AIChatFloatingPanel.tsx
│ │ │ │ │ │ │ └── AISplineButton.tsx
│ │ │ │ │ │ ├── ai-enhanced-timeline/
│ │ │ │ │ │ │ ├── AIEnhancedTimelineLayout.tsx
│ │ │ │ │ │ │ ├── MobileTimelineLayout.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ │ └── EntryContentPlaceholder.tsx
│ │ │ │ │ │ ├── subscription-column/
│ │ │ │ │ │ │ ├── SubscriptionColumn.tsx
│ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ └── PodcastButton.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── subview/
│ │ │ │ │ │ ├── SubviewLayout.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── app-tip/
│ │ │ │ │ │ ├── AICopilotMedia.tsx
│ │ │ │ │ │ ├── AppTipDialog.tsx
│ │ │ │ │ │ ├── AppTipMediaPreview.tsx
│ │ │ │ │ │ ├── AppTipModalContent.tsx
│ │ │ │ │ │ ├── OverviewMedia.tsx
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── useNewUserGuideState.ts
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ ├── Form.tsx
│ │ │ │ │ │ ├── LoginModalContent.tsx
│ │ │ │ │ │ └── TokenModal.tsx
│ │ │ │ │ ├── claim/
│ │ │ │ │ │ ├── feed-claim-modal.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── command/
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── command-button.test-d.ts
│ │ │ │ │ │ ├── command-button.tsx
│ │ │ │ │ │ ├── command-manager.ts
│ │ │ │ │ │ ├── commands/
│ │ │ │ │ │ │ ├── entry-render.tsx
│ │ │ │ │ │ │ ├── entry.tsx
│ │ │ │ │ │ │ ├── global.tsx
│ │ │ │ │ │ │ ├── id.ts
│ │ │ │ │ │ │ ├── integration.tsx
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ │ ├── settings.tsx
│ │ │ │ │ │ │ ├── subscription.tsx
│ │ │ │ │ │ │ ├── timeline.tsx
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── use-command-binding.ts
│ │ │ │ │ │ │ ├── use-command.test-d.ts
│ │ │ │ │ │ │ ├── use-command.ts
│ │ │ │ │ │ │ ├── use-register-command.test-d.ts
│ │ │ │ │ │ │ ├── use-register-command.ts
│ │ │ │ │ │ │ ├── use-register-hotkey.test-d.ts
│ │ │ │ │ │ │ └── use-register-hotkey.ts
│ │ │ │ │ │ ├── mutation-command-ids.ts
│ │ │ │ │ │ ├── registry/
│ │ │ │ │ │ │ ├── command.test-d.ts
│ │ │ │ │ │ │ ├── command.ts
│ │ │ │ │ │ │ └── registry.ts
│ │ │ │ │ │ ├── shortcuts/
│ │ │ │ │ │ │ └── SettingShortcuts.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── customize-toolbar/
│ │ │ │ │ │ ├── constant.ts
│ │ │ │ │ │ ├── dnd.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── modal.tsx
│ │ │ │ │ ├── debug/
│ │ │ │ │ │ └── registry.ts
│ │ │ │ │ ├── discover/
│ │ │ │ │ │ ├── DiscoverFeedCard.tsx
│ │ │ │ │ │ ├── DiscoverFeedForm.tsx
│ │ │ │ │ │ ├── DiscoverForm.tsx
│ │ │ │ │ │ ├── DiscoverImport.tsx
│ │ │ │ │ │ ├── DiscoverInboxList.tsx
│ │ │ │ │ │ ├── DiscoverTransform.tsx
│ │ │ │ │ │ ├── DiscoverUser.tsx
│ │ │ │ │ │ ├── DiscoveryContent.tsx
│ │ │ │ │ │ ├── FeedForm.tsx
│ │ │ │ │ │ ├── FeedSummary.tsx
│ │ │ │ │ │ ├── Inbox/
│ │ │ │ │ │ │ ├── ConfirmDestroyModalContent.tsx
│ │ │ │ │ │ │ ├── InboxActions.tsx
│ │ │ │ │ │ │ ├── InboxEmail.tsx
│ │ │ │ │ │ │ ├── InboxSecret.tsx
│ │ │ │ │ │ │ ├── InboxTable.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── InboxForm.tsx
│ │ │ │ │ │ ├── ListForm.tsx
│ │ │ │ │ │ ├── OpmlAbstractGraphic.tsx
│ │ │ │ │ │ ├── OpmlSelectionModal.tsx
│ │ │ │ │ │ ├── RecommendationContent.tsx
│ │ │ │ │ │ ├── TrendingFeedCard.tsx
│ │ │ │ │ │ ├── UnifiedDiscoverForm.tsx
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ └── discover.ts
│ │ │ │ │ │ ├── example-data.json
│ │ │ │ │ │ ├── recommendations.tsx
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── download/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── editor/
│ │ │ │ │ │ └── css-editor.tsx
│ │ │ │ │ ├── entry-column/
│ │ │ │ │ │ ├── EntryColumnShortcutHandler.tsx
│ │ │ │ │ │ ├── EntryItemSkeleton.tsx
│ │ │ │ │ │ ├── Items/
│ │ │ │ │ │ │ ├── all-item.tsx
│ │ │ │ │ │ │ ├── article-item.tsx
│ │ │ │ │ │ │ ├── audio-item.tsx
│ │ │ │ │ │ │ ├── getItemComponentByView.ts
│ │ │ │ │ │ │ ├── getSkeletonItemComponentByView.ts
│ │ │ │ │ │ │ ├── list-item.tsx
│ │ │ │ │ │ │ ├── media-gallery.tsx
│ │ │ │ │ │ │ ├── notification-item.tsx
│ │ │ │ │ │ │ ├── picture-item-skeleton.tsx
│ │ │ │ │ │ │ ├── picture-item-stateless.tsx
│ │ │ │ │ │ │ ├── picture-item.tsx
│ │ │ │ │ │ │ ├── picture-masonry.tsx
│ │ │ │ │ │ │ ├── social-media-item.tsx
│ │ │ │ │ │ │ └── video-item.tsx
│ │ │ │ │ │ ├── atoms/
│ │ │ │ │ │ │ ├── ai-timeline.ts
│ │ │ │ │ │ │ └── social-media-content-width.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── DateItem.tsx
│ │ │ │ │ │ │ ├── FooterMarkItem.tsx
│ │ │ │ │ │ │ ├── VirtualRowItem.tsx
│ │ │ │ │ │ │ ├── ai-timeline-loading/
│ │ │ │ │ │ │ │ ├── AITimelineLoadingOverlay.css
│ │ │ │ │ │ │ │ └── AITimelineLoadingOverlay.tsx
│ │ │ │ │ │ │ ├── entry-column-wrapper/
│ │ │ │ │ │ │ │ ├── EntryColumnWrapper.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ └── mark-all-button.tsx
│ │ │ │ │ │ ├── context/
│ │ │ │ │ │ │ └── EntriesContext.tsx
│ │ │ │ │ │ ├── grid.tsx
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── useAttachScrollBeyond.tsx
│ │ │ │ │ │ │ ├── useEntriesByView.ts
│ │ │ │ │ │ │ ├── useEntryIdListSnap.ts
│ │ │ │ │ │ │ ├── useEntryMarkReadHandler.tsx
│ │ │ │ │ │ │ ├── useEntryVirtualization.ts
│ │ │ │ │ │ │ ├── useIsPreviewFeed.ts
│ │ │ │ │ │ │ ├── useLocalEntries.ts
│ │ │ │ │ │ │ ├── useMarkAll.ts
│ │ │ │ │ │ │ ├── useNavigateFirstEntry.tsx
│ │ │ │ │ │ │ └── useWheelGestureClose.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── item-stateless.tsx
│ │ │ │ │ │ ├── item.tsx
│ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ ├── AppendTaildingDivider.tsx
│ │ │ │ │ │ │ ├── EntryItemWrapper.tsx
│ │ │ │ │ │ │ ├── EntryListHeader.tsx
│ │ │ │ │ │ │ └── buttons/
│ │ │ │ │ │ │ └── SwitchToMasonryButton.tsx
│ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ ├── star-icon.tsx
│ │ │ │ │ │ ├── store/
│ │ │ │ │ │ │ └── EntryColumnContext.ts
│ │ │ │ │ │ ├── styles.ts
│ │ │ │ │ │ ├── templates/
│ │ │ │ │ │ │ ├── grid-item-template.tsx
│ │ │ │ │ │ │ └── list-item-template.tsx
│ │ │ │ │ │ ├── translation.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ ├── EntryContent.tsx
│ │ │ │ │ │ ├── EntryContentForPreview.tsx
│ │ │ │ │ │ ├── actions/
│ │ │ │ │ │ │ ├── header-actions.tsx
│ │ │ │ │ │ │ ├── more-actions.tsx
│ │ │ │ │ │ │ └── picture-gallery.tsx
│ │ │ │ │ │ ├── atoms.tsx
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── AISummary.tsx
│ │ │ │ │ │ │ ├── ApplyEntryActions.tsx
│ │ │ │ │ │ │ ├── EntryAttachments.tsx
│ │ │ │ │ │ │ ├── EntryPlaceholderLogo.tsx
│ │ │ │ │ │ │ ├── EntryTimelineSidebar.tsx
│ │ │ │ │ │ │ ├── EntryTitle.tsx
│ │ │ │ │ │ │ ├── ImageGalleryContent.tsx
│ │ │ │ │ │ │ ├── SourceContentView.tsx
│ │ │ │ │ │ │ ├── WarnGoToExternalLink.tsx
│ │ │ │ │ │ │ ├── entry-content/
│ │ │ │ │ │ │ │ ├── EntryCommandShortcutRegister.tsx
│ │ │ │ │ │ │ │ ├── EntryContentFallback.tsx
│ │ │ │ │ │ │ │ ├── EntryContentLoading.tsx
│ │ │ │ │ │ │ │ ├── EntryNoContent.tsx
│ │ │ │ │ │ │ │ ├── EntryRenderError.tsx
│ │ │ │ │ │ │ │ ├── EntryScrollingAndNavigationHandler.tsx
│ │ │ │ │ │ │ │ ├── EntryTitleMetaHandler.tsx
│ │ │ │ │ │ │ │ ├── ReadabilityNotice.tsx
│ │ │ │ │ │ │ │ ├── accessories/
│ │ │ │ │ │ │ │ │ ├── ContainerToc.tsx
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ ├── entry-header/
│ │ │ │ │ │ │ │ ├── AIEntryHeader.tsx
│ │ │ │ │ │ │ │ ├── EntryHeader.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── internal/
│ │ │ │ │ │ │ │ │ ├── EntryHeaderActionsContainer.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderBreadcrumb.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderMeta.tsx
│ │ │ │ │ │ │ │ │ ├── EntryHeaderReadHistory.tsx
│ │ │ │ │ │ │ │ │ └── context.tsx
│ │ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ │ ├── entry-read-history/
│ │ │ │ │ │ │ │ ├── EntryReadHistory.tsx
│ │ │ │ │ │ │ │ ├── EntryUser.tsx
│ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ ├── layouts/
│ │ │ │ │ │ │ │ ├── ArticleLayout.tsx
│ │ │ │ │ │ │ │ ├── MediaLayout.tsx
│ │ │ │ │ │ │ │ ├── PicturesLayout.tsx
│ │ │ │ │ │ │ │ ├── SocialMediaLayout.tsx
│ │ │ │ │ │ │ │ ├── VideosLayout.tsx
│ │ │ │ │ │ │ │ ├── factory.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ │ │ ├── AudioPlayer.tsx
│ │ │ │ │ │ │ │ │ ├── AuthorHeader.tsx
│ │ │ │ │ │ │ │ │ ├── ContentBody.tsx
│ │ │ │ │ │ │ │ │ ├── MediaTranscript.tsx
│ │ │ │ │ │ │ │ │ ├── TranscriptToggle.tsx
│ │ │ │ │ │ │ │ │ ├── VideoPlayer.tsx
│ │ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ │ └── useTranscription.ts
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ └── selection/
│ │ │ │ │ │ │ ├── GlassButton.tsx
│ │ │ │ │ │ │ ├── SharePosterModal.tsx
│ │ │ │ │ │ │ └── TextSelectionToolbar.tsx
│ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ └── navigation-hints.ts
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ └── useEntryNavigationHints.ts
│ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ ├── feed-certification.tsx
│ │ │ │ │ │ ├── feed-icon.tsx
│ │ │ │ │ │ ├── feed-summary.tsx
│ │ │ │ │ │ ├── feed-title.tsx
│ │ │ │ │ │ └── view-select-content.tsx
│ │ │ │ │ ├── integration/
│ │ │ │ │ │ ├── CustomIntegrationPreview.tsx
│ │ │ │ │ │ ├── CustomIntegrationValidator.tsx
│ │ │ │ │ │ ├── PlaceholderHelp.tsx
│ │ │ │ │ │ ├── URLSchemePreview.tsx
│ │ │ │ │ │ ├── custom-integration-manager.ts
│ │ │ │ │ │ ├── fetch-adapter.ts
│ │ │ │ │ │ └── url-scheme-handler.ts
│ │ │ │ │ ├── modal/
│ │ │ │ │ │ ├── ConfirmDestroyModalContent.tsx
│ │ │ │ │ │ ├── ShortcutModalContent.tsx
│ │ │ │ │ │ └── hooks/
│ │ │ │ │ │ ├── useConfirmUnsubscribeSubscriptionModal.tsx
│ │ │ │ │ │ └── useShortcutsModal.tsx
│ │ │ │ │ ├── new-user-guide/
│ │ │ │ │ │ ├── ai-chat-pane.tsx
│ │ │ │ │ │ ├── discover-import-step.tsx
│ │ │ │ │ │ ├── feeds-selection-list.tsx
│ │ │ │ │ │ ├── pre-finish.tsx
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── panel/
│ │ │ │ │ │ ├── cmdf.tsx
│ │ │ │ │ │ ├── cmdk.module.css
│ │ │ │ │ │ ├── cmdk.tsx
│ │ │ │ │ │ └── cmdn.tsx
│ │ │ │ │ ├── plan/
│ │ │ │ │ │ ├── UpgradePlanModalContent.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── player/
│ │ │ │ │ │ ├── corner-player.tsx
│ │ │ │ │ │ └── entry-tts.ts
│ │ │ │ │ ├── power/
│ │ │ │ │ │ ├── my-wallet-section/
│ │ │ │ │ │ │ ├── create-wallet.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── withdraw.tsx
│ │ │ │ │ │ └── transaction-section/
│ │ │ │ │ │ ├── TransactionsSection.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── tx-table/
│ │ │ │ │ │ ├── TxTable.tsx
│ │ │ │ │ │ ├── components.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── profile/
│ │ │ │ │ │ ├── account-management.tsx
│ │ │ │ │ │ ├── email-management.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── profile-setting-form.tsx
│ │ │ │ │ │ ├── two-factor.tsx
│ │ │ │ │ │ ├── update-password-form.tsx
│ │ │ │ │ │ ├── user-profile-modal/
│ │ │ │ │ │ │ ├── UserProfileModalContent.tsx
│ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── shared.tsx
│ │ │ │ │ │ └── user-profile-modal.constants.ts
│ │ │ │ │ ├── renderer/
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ └── TimeStamp.tsx
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ ├── html.tsx
│ │ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── review-prompt/
│ │ │ │ │ │ ├── ReviewPromptModalContent.tsx
│ │ │ │ │ │ ├── debug.ts
│ │ │ │ │ │ ├── provider.tsx
│ │ │ │ │ │ ├── use-review-prompt-state.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── rsshub/
│ │ │ │ │ │ ├── add-modal-content.tsx
│ │ │ │ │ │ ├── delete-modal-content.tsx
│ │ │ │ │ │ └── set-modal-content.tsx
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ ├── control.tsx
│ │ │ │ │ │ ├── helper/
│ │ │ │ │ │ │ ├── EnhancedIndicator.tsx
│ │ │ │ │ │ │ ├── SyncIndicator.tsx
│ │ │ │ │ │ │ ├── builder.ts
│ │ │ │ │ │ │ ├── setting-builder.tsx
│ │ │ │ │ │ │ ├── sync-queue.ts
│ │ │ │ │ │ │ └── withSettingEnable.tsx
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ ├── use-setting-ctx.ts
│ │ │ │ │ │ │ └── useWrapEnhancedSettingItem.ts
│ │ │ │ │ │ ├── modal/
│ │ │ │ │ │ │ ├── SettingModalContent.tsx
│ │ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── use-setting-modal-hack.ts
│ │ │ │ │ │ │ └── useSettingModal.ts
│ │ │ │ │ │ ├── section.tsx
│ │ │ │ │ │ ├── sections/
│ │ │ │ │ │ │ └── fonts.tsx
│ │ │ │ │ │ ├── settings-glob.ts
│ │ │ │ │ │ ├── tabs/
│ │ │ │ │ │ │ ├── about.tsx
│ │ │ │ │ │ │ ├── ai/
│ │ │ │ │ │ │ │ ├── PanelStyleSection.tsx
│ │ │ │ │ │ │ │ ├── PersonalizePromptSection.tsx
│ │ │ │ │ │ │ │ ├── TimelinePromptSection.tsx
│ │ │ │ │ │ │ │ ├── byok/
│ │ │ │ │ │ │ │ │ ├── ByokProviderItem.tsx
│ │ │ │ │ │ │ │ │ ├── ByokProviderModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── ByokSection.tsx
│ │ │ │ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── mcp/
│ │ │ │ │ │ │ │ │ ├── MCPPresetCard.tsx
│ │ │ │ │ │ │ │ │ ├── MCPPresetSelectionModal.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServiceItem.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServiceModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── MCPServicesSection.tsx
│ │ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ │ │ ├── shortcuts/
│ │ │ │ │ │ │ │ │ ├── AIShortcutsSection.tsx
│ │ │ │ │ │ │ │ │ ├── ShortcutItem.tsx
│ │ │ │ │ │ │ │ │ ├── ShortcutModalContent.tsx
│ │ │ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── tasks/
│ │ │ │ │ │ │ │ │ ├── TaskSchedulingSection.tsx
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ └── usage/
│ │ │ │ │ │ │ │ ├── UsageAnalysisSection.tsx
│ │ │ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ │ │ ├── DetailedUsageModal.tsx
│ │ │ │ │ │ │ │ │ ├── EfficiencyTab.tsx
│ │ │ │ │ │ │ │ │ ├── HistoryTab.tsx
│ │ │ │ │ │ │ │ │ ├── OverviewTab.tsx
│ │ │ │ │ │ │ │ │ ├── PatternsTab.tsx
│ │ │ │ │ │ │ │ │ ├── UsageProgressRing.tsx
│ │ │ │ │ │ │ │ │ ├── UsageWarningBanner.tsx
│ │ │ │ │ │ │ │ │ ├── charts/
│ │ │ │ │ │ │ │ │ │ ├── BarList.tsx
│ │ │ │ │ │ │ │ │ │ ├── Sparkline.tsx
│ │ │ │ │ │ │ │ │ │ ├── TinyBars.tsx
│ │ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ │ │ └── utils.ts
│ │ │ │ │ │ │ ├── ai.tsx
│ │ │ │ │ │ │ ├── appearance.tsx
│ │ │ │ │ │ │ ├── cli.tsx
│ │ │ │ │ │ │ ├── data-control.tsx
│ │ │ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ │ ├── integration/
│ │ │ │ │ │ │ │ ├── CustomIntegrationModal.tsx
│ │ │ │ │ │ │ │ ├── CustomIntegrationSection.tsx
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── lists/
│ │ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── modals.tsx
│ │ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ │ ├── plan.tsx
│ │ │ │ │ │ │ └── shortcut.tsx
│ │ │ │ │ │ ├── title.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ └── ViewSelectorRadioGroup.tsx
│ │ │ │ │ ├── subscription-column/
│ │ │ │ │ │ ├── CategoryRemoveDialogContent.tsx
│ │ │ │ │ │ ├── CategoryUnsubscribeDialogContent.tsx
│ │ │ │ │ │ ├── FeedCategory.tsx
│ │ │ │ │ │ ├── FeedItem.tsx
│ │ │ │ │ │ ├── RenameCategoryForm.tsx
│ │ │ │ │ │ ├── SimpleDiscoverModal.tsx
│ │ │ │ │ │ ├── SortedFeedItems.tsx
│ │ │ │ │ │ ├── SubscriptionColumnHeader.tsx
│ │ │ │ │ │ ├── SubscriptionTabButton.tsx
│ │ │ │ │ │ ├── TimelineTabsSettingsModal.tsx
│ │ │ │ │ │ ├── UnreadNumber.tsx
│ │ │ │ │ │ ├── atom.ts
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── hook.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── sort-by/
│ │ │ │ │ │ │ ├── SortByAlphabeticalList.tsx
│ │ │ │ │ │ │ ├── SortByUnreadList.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── types.tsx
│ │ │ │ │ │ ├── styles.ts
│ │ │ │ │ │ └── subscription-list/
│ │ │ │ │ │ ├── EmptyFeedList.tsx
│ │ │ │ │ │ ├── ListHeader.tsx
│ │ │ │ │ │ ├── SortButton.tsx
│ │ │ │ │ │ ├── StarredItem.tsx
│ │ │ │ │ │ ├── SubscriptionList.tsx
│ │ │ │ │ │ ├── SubscriptionListGuard.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── trending/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── update-notice/
│ │ │ │ │ │ └── UpdateNotice.tsx
│ │ │ │ │ ├── upgrade/
│ │ │ │ │ │ ├── container.tsx
│ │ │ │ │ │ ├── lazy/
│ │ │ │ │ │ │ ├── index.electron.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── user/
│ │ │ │ │ │ ├── LoginButton.tsx
│ │ │ │ │ │ ├── ProfileButton.tsx
│ │ │ │ │ │ ├── UserAvatar.tsx
│ │ │ │ │ │ ├── UserGallery.tsx
│ │ │ │ │ │ ├── UserProBadge.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ └── wallet/
│ │ │ │ │ └── balance.tsx
│ │ │ │ ├── pages/
│ │ │ │ │ ├── (main)/
│ │ │ │ │ │ ├── (layer)/
│ │ │ │ │ │ │ ├── (ai)/
│ │ │ │ │ │ │ │ └── ai/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── (subview)/
│ │ │ │ │ │ │ │ ├── action/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── discover/
│ │ │ │ │ │ │ │ │ ├── category/
│ │ │ │ │ │ │ │ │ │ └── [category].tsx
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ │ ├── power/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ └── rsshub/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── timeline/
│ │ │ │ │ │ │ └── [timelineId]/
│ │ │ │ │ │ │ ├── [feedId]/
│ │ │ │ │ │ │ │ ├── [entryId]/
│ │ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ │ ├── index.sync.tsx
│ │ │ │ │ │ └── layout.tsx
│ │ │ │ │ └── settings/
│ │ │ │ │ ├── (settings)/
│ │ │ │ │ │ ├── about.tsx
│ │ │ │ │ │ ├── ai.tsx
│ │ │ │ │ │ ├── appearance.tsx
│ │ │ │ │ │ ├── cli.tsx
│ │ │ │ │ │ ├── data-control.tsx
│ │ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ │ ├── general.tsx
│ │ │ │ │ │ ├── integration.tsx
│ │ │ │ │ │ ├── list.tsx
│ │ │ │ │ │ ├── notifications.tsx
│ │ │ │ │ │ ├── plan.tsx
│ │ │ │ │ │ ├── profile.tsx
│ │ │ │ │ │ └── shortcuts.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── layout.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ ├── app-grid-layout-container-provider.tsx
│ │ │ │ │ ├── context-menu-provider.tsx
│ │ │ │ │ ├── extension-expose-provider.tsx
│ │ │ │ │ ├── external-jump-in-provider.tsx
│ │ │ │ │ ├── global-focusable-provider.tsx
│ │ │ │ │ ├── global-hotkeys-provider.tsx
│ │ │ │ │ ├── hotkey-provider.tsx
│ │ │ │ │ ├── i18n-provider.tsx
│ │ │ │ │ ├── inject-styles-provider.tsx
│ │ │ │ │ ├── invalidate-query-provider.tsx
│ │ │ │ │ ├── lazy/
│ │ │ │ │ │ ├── index.electron.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── main-view-hotkeys-provider.tsx
│ │ │ │ │ ├── popover-provider.tsx
│ │ │ │ │ ├── root-providers.tsx
│ │ │ │ │ ├── server-configs-provider.tsx
│ │ │ │ │ ├── setting-sync.tsx
│ │ │ │ │ ├── user-provider.tsx
│ │ │ │ │ └── wrapped-element-provider.tsx
│ │ │ │ ├── push-notification.ts
│ │ │ │ ├── queries/
│ │ │ │ │ ├── _.ts
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── discover.ts
│ │ │ │ │ ├── entries.ts
│ │ │ │ │ ├── feed.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── mcp.ts
│ │ │ │ │ ├── messaging.ts
│ │ │ │ │ ├── rsshub.ts
│ │ │ │ │ ├── server-configs.ts
│ │ │ │ │ ├── settings.ts
│ │ │ │ │ ├── types.d.ts
│ │ │ │ │ ├── users.ts
│ │ │ │ │ └── wallet.tsx
│ │ │ │ ├── router.tsx
│ │ │ │ ├── router.web.tsx
│ │ │ │ ├── store/
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── db.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── search/
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── clear.ts
│ │ │ │ │ ├── helper.test.ts
│ │ │ │ │ └── helper.ts
│ │ │ │ ├── styles/
│ │ │ │ │ ├── additional.css
│ │ │ │ │ ├── base.css
│ │ │ │ │ ├── cursor.css
│ │ │ │ │ ├── font.css
│ │ │ │ │ ├── main.css
│ │ │ │ │ └── scrollbar.css
│ │ │ │ ├── sw.ts
│ │ │ │ ├── wdyr.ts
│ │ │ │ └── workers/
│ │ │ │ └── sw/
│ │ │ │ ├── index.ts
│ │ │ │ └── pusher.ts
│ │ │ ├── tsconfig.json
│ │ │ └── vitest.config.ts
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ └── vite/
│ │ │ ├── ast.ts
│ │ │ ├── cleanup.ts
│ │ │ ├── compress.ts
│ │ │ ├── deps.ts
│ │ │ ├── generate-main-hash.ts
│ │ │ ├── hmr.ts
│ │ │ ├── html-inject.ts
│ │ │ ├── i18n-hmr.ts
│ │ │ ├── locales-json.ts
│ │ │ ├── locales.ts
│ │ │ ├── manifest.ts
│ │ │ ├── specific-import.ts
│ │ │ └── utils/
│ │ │ └── i18n-completeness.ts
│ │ ├── postcss.config.cjs
│ │ ├── resources/
│ │ │ ├── app-update.yml
│ │ │ ├── icon-staging.icns
│ │ │ └── icon.icns
│ │ ├── scripts/
│ │ │ ├── apply-changelog.ts
│ │ │ ├── generate-appx-manifest.ts
│ │ │ ├── merge-yml.ts
│ │ │ └── update-windows-yml.ts
│ │ ├── static/
│ │ │ └── dmg-icon.icns
│ │ ├── tailwind.config.ts
│ │ ├── vite.config.electron-render.ts
│ │ ├── vite.config.ts
│ │ └── wrangler.jsonc
│ ├── landing/
│ │ ├── .prettierrc.mjs
│ │ ├── components.json
│ │ ├── eslint.config.mjs
│ │ ├── global.d.ts
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ └── eslint-recursive-sort.mjs
│ │ ├── postcss.config.mjs
│ │ ├── public/
│ │ │ ├── discover-sources.json
│ │ │ └── manifest.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── ClientInit.tsx
│ │ │ │ ├── InitInClient.ts
│ │ │ │ ├── [locale]/
│ │ │ │ │ ├── download/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── error.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── pricing/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── privacy-policy/
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── terms-of-service/
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── apple-app-site-association/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── discover-sources/
│ │ │ │ │ └── route.ts
│ │ │ │ ├── globals.css
│ │ │ │ ├── init.ts
│ │ │ │ ├── layout.tsx
│ │ │ │ └── robots.ts
│ │ │ ├── atoms/
│ │ │ │ ├── css-media.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-interactive.ts
│ │ │ │ └── viewport.ts
│ │ │ ├── components/
│ │ │ │ ├── brand/
│ │ │ │ │ ├── Folo.tsx
│ │ │ │ │ └── Logo.tsx
│ │ │ │ ├── common/
│ │ │ │ │ ├── ClientOnly.tsx
│ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ ├── GithubTrending.tsx
│ │ │ │ │ ├── HydrationEndDetector.tsx
│ │ │ │ │ ├── Lazyload.tsx
│ │ │ │ │ ├── LightRays.tsx
│ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ ├── QueryHydrate.tsx
│ │ │ │ │ └── ScrollTop.tsx
│ │ │ │ ├── hoc/
│ │ │ │ │ └── with-no-ssr.tsx
│ │ │ │ ├── layout/
│ │ │ │ │ ├── container/
│ │ │ │ │ │ └── Normal.tsx
│ │ │ │ │ ├── content/
│ │ │ │ │ │ ├── Content.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── footer/
│ │ │ │ │ │ └── Footer.tsx
│ │ │ │ │ └── root/
│ │ │ │ │ └── Root.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── 3d-models/
│ │ │ │ │ │ ├── AISpline.tsx
│ │ │ │ │ │ └── AISplineLoader.tsx
│ │ │ │ │ ├── accordion/
│ │ │ │ │ │ └── Accordion.tsx
│ │ │ │ │ ├── border-beam.tsx
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── Button.tsx
│ │ │ │ │ │ ├── MotionButton.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ ├── Checkbox.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── collapse/
│ │ │ │ │ │ ├── CollapseCss.tsx
│ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dialog/
│ │ │ │ │ │ ├── Dialog.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── Divider.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ └── DropdownMenu.tsx
│ │ │ │ │ ├── effects/
│ │ │ │ │ │ ├── GridGuides.tsx
│ │ │ │ │ │ ├── ParticlesAura.tsx
│ │ │ │ │ │ └── TiltCard.tsx
│ │ │ │ │ ├── glass/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── highlighter.tsx
│ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ ├── HoverCard.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── input/
│ │ │ │ │ │ ├── Input.tsx
│ │ │ │ │ │ ├── TextArea.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── json-highlighter/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── label/
│ │ │ │ │ │ └── Label.tsx
│ │ │ │ │ ├── light-rays.tsx
│ │ │ │ │ ├── loading/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── magic-card.tsx
│ │ │ │ │ ├── markdown/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── modal/
│ │ │ │ │ │ ├── ModalContainer.tsx
│ │ │ │ │ │ ├── ModalManager.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── panel/
│ │ │ │ │ │ └── PanelSplitter.tsx
│ │ │ │ │ ├── portal/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── provider.tsx
│ │ │ │ │ ├── prompts/
│ │ │ │ │ │ ├── BasePrompt.tsx
│ │ │ │ │ │ ├── InputPrompt.tsx
│ │ │ │ │ │ ├── Prompt.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── radio/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── relative-time/
│ │ │ │ │ │ ├── RelativeTime.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── scroll-areas/
│ │ │ │ │ │ ├── ScrollArea.tsx
│ │ │ │ │ │ ├── ctx.ts
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── segment-tab/
│ │ │ │ │ │ ├── SegmentTab.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── select/
│ │ │ │ │ │ ├── ComboboxSelect.tsx
│ │ │ │ │ │ ├── MultiSelect.tsx
│ │ │ │ │ │ ├── Select.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── sheet/
│ │ │ │ │ │ ├── Sheet.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── skeleton/
│ │ │ │ │ │ ├── Skeleton.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── switch/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── theme-switcher/
│ │ │ │ │ │ ├── ThemeSwitcher.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── tooltip/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── transition/
│ │ │ │ │ │ ├── BottomToUpSoftScaleTransitionView.tsx
│ │ │ │ │ │ ├── BottomToUpTransitionView.tsx
│ │ │ │ │ │ ├── FadeInOutTransitionView.tsx
│ │ │ │ │ │ ├── IconTransiton.tsx
│ │ │ │ │ │ ├── LeftToRightTransitionView.tsx
│ │ │ │ │ │ ├── RightToLeftTransitionView.tsx
│ │ │ │ │ │ ├── ScaleTransitionView.tsx
│ │ │ │ │ │ ├── TextUpTransitionView.tsx
│ │ │ │ │ │ ├── factor.tsx
│ │ │ │ │ │ └── typings.ts
│ │ │ │ │ └── viewport/
│ │ │ │ │ ├── OnlyDesktop.tsx
│ │ │ │ │ ├── OnlyMobile.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── widgets/
│ │ │ │ ├── download/
│ │ │ │ │ ├── DownloadHero.tsx
│ │ │ │ │ └── PlatformDownloads.tsx
│ │ │ │ ├── landing/
│ │ │ │ │ ├── Audience.tsx
│ │ │ │ │ ├── BuiltOpen.tsx
│ │ │ │ │ ├── Features.tsx
│ │ │ │ │ ├── Header.tsx
│ │ │ │ │ ├── Hero.tsx
│ │ │ │ │ ├── PromptDemo.tsx
│ │ │ │ │ ├── RepoStats.tsx
│ │ │ │ │ ├── SocialProof.tsx
│ │ │ │ │ ├── TrustedBy.tsx
│ │ │ │ │ ├── ViewsShowcase.tsx
│ │ │ │ │ └── WindowChrome.tsx
│ │ │ │ ├── pricing/
│ │ │ │ │ └── PricingPlans.tsx
│ │ │ │ └── simulators/
│ │ │ │ ├── EntryChatPanel.tsx
│ │ │ │ ├── EntryPage.tsx
│ │ │ │ ├── ListDemo.tsx
│ │ │ │ ├── TimelineChatDemo.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── EntryPageOverlay.tsx
│ │ │ │ │ ├── ai/
│ │ │ │ │ │ ├── AIChainOfThought.tsx
│ │ │ │ │ │ ├── AIMarkdownMessage.tsx
│ │ │ │ │ │ ├── AIReasoningPart.tsx
│ │ │ │ │ │ ├── ToolInvocationComponent.tsx
│ │ │ │ │ │ ├── animated/
│ │ │ │ │ │ │ ├── AnimatedMarkdown.tsx
│ │ │ │ │ │ │ ├── TokenizedText.tsx
│ │ │ │ │ │ │ └── constants.ts
│ │ │ │ │ │ ├── mocks.ts
│ │ │ │ │ │ ├── parse-incomplete-markdown.ts
│ │ │ │ │ │ ├── reasoning-mock.json
│ │ │ │ │ │ └── shiny-text/
│ │ │ │ │ │ ├── ShinyText.tsx
│ │ │ │ │ │ └── index.module.css
│ │ │ │ │ └── chat/
│ │ │ │ │ ├── AiMessageContextBar.tsx
│ │ │ │ │ ├── AiMockMessage.tsx
│ │ │ │ │ ├── AiUserMessage.tsx
│ │ │ │ │ ├── ListChatPlayer.tsx
│ │ │ │ │ ├── MarkdownMessage.tsx
│ │ │ │ │ └── stream.ts
│ │ │ │ └── mocks.tsx
│ │ │ ├── constants/
│ │ │ │ ├── download.ts
│ │ │ │ ├── env.ts
│ │ │ │ ├── site.ts
│ │ │ │ └── spring.ts
│ │ │ ├── hooks/
│ │ │ │ ├── biz/
│ │ │ │ │ └── use-github-star.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── use-before-mounted.ts
│ │ │ │ │ ├── use-click-away.ts
│ │ │ │ │ ├── use-debounce-value.ts
│ │ │ │ │ ├── use-event-callback.ts
│ │ │ │ │ ├── use-input-composition.ts
│ │ │ │ │ ├── use-is-active.ts
│ │ │ │ │ ├── use-is-client.ts
│ │ │ │ │ ├── use-is-dark.ts
│ │ │ │ │ ├── use-is-mounted.ts
│ │ │ │ │ ├── use-is-unmounted.ts
│ │ │ │ │ ├── use-previous.ts
│ │ │ │ │ ├── use-ref-value.ts
│ │ │ │ │ ├── use-safe-setState.ts
│ │ │ │ │ ├── use-state-ref.ts
│ │ │ │ │ ├── use-sync-effect.ts
│ │ │ │ │ └── useMeasure.ts
│ │ │ │ └── shared/
│ │ │ │ └── use-mask-scrollarea.ts
│ │ │ ├── i18n/
│ │ │ │ ├── request.ts
│ │ │ │ └── routing.ts
│ │ │ ├── legal/
│ │ │ │ ├── privacy.md
│ │ │ │ └── tos.md
│ │ │ ├── lib/
│ │ │ │ ├── apple-app-site-association.ts
│ │ │ │ ├── cn.ts
│ │ │ │ ├── color.ts
│ │ │ │ ├── cookie.ts
│ │ │ │ ├── datetime.ts
│ │ │ │ ├── dom.ts
│ │ │ │ ├── env.ts
│ │ │ │ ├── fonts.ts
│ │ │ │ ├── helper.ts
│ │ │ │ ├── jotai.ts
│ │ │ │ ├── landing-data.ts
│ │ │ │ ├── noop.ts
│ │ │ │ ├── platform.ts
│ │ │ │ ├── pricing-data.ts
│ │ │ │ ├── query-client.server.ts
│ │ │ │ ├── scroller.ts
│ │ │ │ ├── sleep.ts
│ │ │ │ ├── spring.ts
│ │ │ │ └── store.ts
│ │ │ ├── messages/
│ │ │ │ ├── en.json
│ │ │ │ ├── jp.json
│ │ │ │ └── zh.json
│ │ │ ├── providers/
│ │ │ │ ├── root/
│ │ │ │ │ ├── debug-provider.tsx
│ │ │ │ │ ├── event-provider.tsx
│ │ │ │ │ ├── framer-lazy-feature.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── jotai-provider.tsx
│ │ │ │ │ ├── page-scroll-info-provider.tsx
│ │ │ │ │ ├── react-query-provider.tsx
│ │ │ │ │ └── sonner.tsx
│ │ │ │ └── shared/
│ │ │ │ ├── LayoutRightSideProvider.tsx
│ │ │ │ └── WrappedElementProvider.tsx
│ │ │ ├── proxy.ts
│ │ │ └── styles/
│ │ │ ├── globals.css
│ │ │ └── pastel-theme-oklch.css
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ ├── worker/
│ │ │ └── index.js
│ │ └── wrangler.jsonc
│ ├── mobile/
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── .watchmanconfig
│ │ ├── AGENTS.md
│ │ ├── README.md
│ │ ├── app.config.ts
│ │ ├── assets/
│ │ │ └── font/
│ │ │ └── sn-pro/
│ │ │ ├── SNPro-Black.otf
│ │ │ ├── SNPro-BlackItalic.otf
│ │ │ ├── SNPro-Bold.otf
│ │ │ ├── SNPro-BoldItalic.otf
│ │ │ ├── SNPro-Book.otf
│ │ │ ├── SNPro-BookItalic.otf
│ │ │ ├── SNPro-Heavy.otf
│ │ │ ├── SNPro-HeavyItalic.otf
│ │ │ ├── SNPro-Light.otf
│ │ │ ├── SNPro-LightItalic.otf
│ │ │ ├── SNPro-Medium.otf
│ │ │ ├── SNPro-MediumItalic.otf
│ │ │ ├── SNPro-Regular.otf
│ │ │ ├── SNPro-RegularItalic.otf
│ │ │ ├── SNPro-Semibold.otf
│ │ │ ├── SNPro-SemiboldItalic.otf
│ │ │ ├── SNPro-Thin.otf
│ │ │ └── SNPro-ThinItalic.otf
│ │ ├── babel.config.js
│ │ ├── build/
│ │ │ ├── GoogleService-Info.plist
│ │ │ └── google-services.json
│ │ ├── bump.config.ts
│ │ ├── changelog/
│ │ │ ├── 0.1.3.md
│ │ │ ├── 0.1.4.md
│ │ │ ├── 0.1.5.md
│ │ │ ├── 0.1.6.md
│ │ │ ├── 0.1.7.md
│ │ │ ├── 0.1.8.md
│ │ │ ├── 0.1.9.md
│ │ │ ├── 0.2.0.md
│ │ │ ├── 0.2.1.md
│ │ │ ├── 0.2.10.md
│ │ │ ├── 0.2.2.md
│ │ │ ├── 0.2.3.md
│ │ │ ├── 0.2.4.md
│ │ │ ├── 0.2.5.md
│ │ │ ├── 0.2.6.md
│ │ │ ├── 0.2.8.md
│ │ │ ├── 0.3.0.md
│ │ │ ├── 0.4.0.md
│ │ │ ├── next.md
│ │ │ └── next.template.md
│ │ ├── e2e/
│ │ │ ├── README.md
│ │ │ ├── flows/
│ │ │ │ ├── ios/
│ │ │ │ │ ├── auth.yaml
│ │ │ │ │ ├── content.yaml
│ │ │ │ │ ├── core.yaml
│ │ │ │ │ ├── dismiss-overlays.yaml
│ │ │ │ │ ├── ensure-onboarding-unfollowed.yaml
│ │ │ │ │ ├── follow-onboarding.yaml
│ │ │ │ │ ├── login.yaml
│ │ │ │ │ ├── register.yaml
│ │ │ │ │ ├── sign-out.yaml
│ │ │ │ │ ├── timeline-entry.yaml
│ │ │ │ │ └── unfollow-onboarding.yaml
│ │ │ │ └── shared/
│ │ │ │ ├── core.yaml
│ │ │ │ ├── dismiss-ios-system-modal.yaml
│ │ │ │ ├── ensure-onboarding-unfollowed.yaml
│ │ │ │ ├── follow-onboarding.yaml
│ │ │ │ ├── login.yaml
│ │ │ │ ├── open-auth.yaml
│ │ │ │ ├── register.yaml
│ │ │ │ ├── sign-out.yaml
│ │ │ │ ├── timeline-entry.yaml
│ │ │ │ └── unfollow-onboarding.yaml
│ │ │ └── run-maestro.sh
│ │ ├── eas.json
│ │ ├── global.d.ts
│ │ ├── ios/
│ │ │ ├── .gitignore
│ │ │ ├── .xcode.env
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── Contents.json
│ │ │ │ ├── black_board_2_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── black_board_2_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── home_5_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── home_5_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── search_3_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── search_3_cute_re.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── settings_1_cute_fi.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── settings_1_cute_re.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Folo/
│ │ │ │ ├── AppDelegate.swift
│ │ │ │ ├── Folo-Bridging-Header.h
│ │ │ │ ├── Folo.entitlements
│ │ │ │ ├── Images.xcassets/
│ │ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ │ └── Contents.json
│ │ │ │ │ ├── Contents.json
│ │ │ │ │ └── SplashScreenBackground.colorset/
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Info.plist
│ │ │ │ ├── PrivacyInfo.xcprivacy
│ │ │ │ ├── SplashScreen.storyboard
│ │ │ │ └── Supporting/
│ │ │ │ └── Expo.plist
│ │ │ ├── Folo - Follow everything.storekit
│ │ │ ├── Folo.xcodeproj/
│ │ │ │ ├── project.pbxproj
│ │ │ │ └── xcshareddata/
│ │ │ │ └── xcschemes/
│ │ │ │ └── Folo.xcscheme
│ │ │ ├── Folo.xcworkspace/
│ │ │ │ └── contents.xcworkspacedata
│ │ │ ├── Podfile
│ │ │ └── Podfile.properties.json
│ │ ├── metro.config.js
│ │ ├── native/
│ │ │ ├── .eslintrc.js
│ │ │ ├── .gitignore
│ │ │ ├── .npmignore
│ │ │ ├── README.md
│ │ │ ├── expo-module.config.json
│ │ │ ├── ios/
│ │ │ │ ├── Controllers/
│ │ │ │ │ ├── ModalWebViewController.swift
│ │ │ │ │ ├── RNSViewController.swift
│ │ │ │ │ └── WebViewController.swift
│ │ │ │ ├── Extensions/
│ │ │ │ │ ├── UIColor+Hex.swift
│ │ │ │ │ ├── UIImage+asActivityItemSource.swift
│ │ │ │ │ ├── UIImage.swift
│ │ │ │ │ └── UIWindow.swift
│ │ │ │ ├── FollowNative.podspec
│ │ │ │ ├── Models/
│ │ │ │ │ ├── ProfileData.swift
│ │ │ │ │ └── UserData.swift
│ │ │ │ ├── Modules/
│ │ │ │ │ ├── AppleIntelligenceGlowEffect/
│ │ │ │ │ │ ├── AppleIntelligenceGlowEffectModule.swift
│ │ │ │ │ │ ├── IntelligenceAnimationController.swift
│ │ │ │ │ │ └── IntelligenceAnimationView.swift
│ │ │ │ │ ├── Helper/
│ │ │ │ │ │ ├── Helper+Image.swift
│ │ │ │ │ │ └── HelperModule.swift
│ │ │ │ │ ├── ItemPressable/
│ │ │ │ │ │ └── ItemPressableModule.swift
│ │ │ │ │ ├── PagerView/
│ │ │ │ │ │ ├── EnhancePageViewModule.swift
│ │ │ │ │ │ ├── EnhancePagerController.swift
│ │ │ │ │ │ └── EnhancePagerViewModule.swift
│ │ │ │ │ ├── SharedWebView/
│ │ │ │ │ │ ├── FOWebView.swift
│ │ │ │ │ │ ├── FollowImageURLSchemeHandler.swift
│ │ │ │ │ │ ├── Injected/
│ │ │ │ │ │ │ ├── at_end.js
│ │ │ │ │ │ │ └── at_start.js
│ │ │ │ │ │ ├── SharedWebView+BridgeData.swift
│ │ │ │ │ │ ├── SharedWebView.swift
│ │ │ │ │ │ ├── SharedWebViewModule.swift
│ │ │ │ │ │ ├── WebViewManager.swift
│ │ │ │ │ │ └── WebViewState.swift
│ │ │ │ │ ├── StoreKitTestHelper/
│ │ │ │ │ │ └── StoreKitTestHelperModule.swift
│ │ │ │ │ ├── TabBar/
│ │ │ │ │ │ ├── TabBarBottomAccessoryModule.swift
│ │ │ │ │ │ ├── TabBarModule.swift
│ │ │ │ │ │ ├── TabBarPortalModule.swift
│ │ │ │ │ │ ├── TabBarRootView.swift
│ │ │ │ │ │ ├── TabScreenModule.swift
│ │ │ │ │ │ └── TabScreenView.swift
│ │ │ │ │ └── Toaster/
│ │ │ │ │ ├── Toast.swift
│ │ │ │ │ └── ToasterModule.swift
│ │ │ │ ├── Packages/
│ │ │ │ │ ├── ImageViewer_swift/
│ │ │ │ │ │ ├── ImageCarouselViewController.swift
│ │ │ │ │ │ ├── ImageCarouselViewControllerProtocol.swift
│ │ │ │ │ │ ├── ImageItem.swift
│ │ │ │ │ │ ├── ImageLoader.swift
│ │ │ │ │ │ ├── ImageViewerController.swift
│ │ │ │ │ │ ├── ImageViewerOption.swift
│ │ │ │ │ │ ├── ImageViewerTransitionPresentationManager.swift
│ │ │ │ │ │ ├── ImageViewer_swift.h
│ │ │ │ │ │ ├── LISENCE
│ │ │ │ │ │ ├── SimpleImageDatasource.swift
│ │ │ │ │ │ ├── UIImageView_Extensions.swift
│ │ │ │ │ │ ├── UINavigationBar_Extensions.swift
│ │ │ │ │ │ └── UIView_Extensions.swift
│ │ │ │ │ └── SPIndicator/
│ │ │ │ │ └── LICENSE
│ │ │ │ └── Utils/
│ │ │ │ └── Utils.swift
│ │ │ └── package.json
│ │ ├── nativewind-env.d.ts
│ │ ├── package.json
│ │ ├── plugins/
│ │ │ ├── android-trust-user-certs.js
│ │ │ ├── network_security_config.xml
│ │ │ ├── with-android-jdk-21.js
│ │ │ ├── with-android-manifest-plugin.js
│ │ │ ├── with-follow-app-delegate.js
│ │ │ ├── with-follow-assets.js
│ │ │ └── with-gradle-jvm-heap-size-increase.js
│ │ ├── postcss.config.js
│ │ ├── scripts/
│ │ │ ├── apply-changelog.ts
│ │ │ ├── e2e-prod-ios-auth-bootstrap.ts
│ │ │ └── expo-update.ts
│ │ ├── shim-env.d.ts
│ │ ├── src/
│ │ │ ├── @types/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── default-resource.ts
│ │ │ │ └── i18next.d.ts
│ │ │ ├── App.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── app.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ └── useDeviceType.ts
│ │ │ │ ├── server-configs.ts
│ │ │ │ └── settings/
│ │ │ │ ├── data.ts
│ │ │ │ ├── general.ts
│ │ │ │ ├── internal/
│ │ │ │ │ └── helper.ts
│ │ │ │ └── ui.ts
│ │ │ ├── components/
│ │ │ │ ├── common/
│ │ │ │ │ ├── AnimatedComponents.tsx
│ │ │ │ │ ├── Balance.tsx
│ │ │ │ │ ├── BlurEffect.tsx
│ │ │ │ │ ├── CopyButton.tsx
│ │ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ │ ├── FullWindowOverlay.ios.tsx
│ │ │ │ │ ├── FullWindowOverlay.tsx
│ │ │ │ │ ├── Link.tsx
│ │ │ │ │ ├── NoLoginInfo.tsx
│ │ │ │ │ ├── RefreshControl.tsx
│ │ │ │ │ ├── RotateableLoading.tsx
│ │ │ │ │ ├── SubmitButton.tsx
│ │ │ │ │ ├── SwipeableItem.tsx
│ │ │ │ │ └── ThemedBlurView.tsx
│ │ │ │ ├── errors/
│ │ │ │ │ ├── GlobalErrorScreen.tsx
│ │ │ │ │ ├── ListErrorView.tsx
│ │ │ │ │ └── ScreenErrorScreen.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── OouiUserAnonymous.tsx
│ │ │ │ │ └── PhUsersBold.tsx
│ │ │ │ ├── layouts/
│ │ │ │ │ ├── contexts/
│ │ │ │ │ │ └── ModalScrollViewContext.ts
│ │ │ │ │ ├── header/
│ │ │ │ │ │ ├── FakeNativeHeaderTitle.tsx
│ │ │ │ │ │ ├── HeaderElements.tsx
│ │ │ │ │ │ ├── NavigationHeader.tsx
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── tabbar/
│ │ │ │ │ │ ├── BottomTabHeightProvider.tsx
│ │ │ │ │ │ ├── BottomTabProvider.tsx
│ │ │ │ │ │ ├── BottomTabs.tsx
│ │ │ │ │ │ ├── ReactNativeTab.ios.tsx
│ │ │ │ │ │ ├── ReactNativeTab.tsx
│ │ │ │ │ │ ├── Tabbar.tsx
│ │ │ │ │ │ ├── contexts/
│ │ │ │ │ │ │ ├── BottomTabBarBackgroundContext.tsx
│ │ │ │ │ │ │ ├── BottomTabBarHeightContext.tsx
│ │ │ │ │ │ │ └── BottomTabBarVisibleContext.tsx
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── views/
│ │ │ │ │ ├── NavigationHeaderContext.tsx
│ │ │ │ │ └── SafeNavigationScrollView.tsx
│ │ │ │ ├── native/
│ │ │ │ │ ├── PagerView/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── specs.ts
│ │ │ │ │ └── webview/
│ │ │ │ │ ├── DebugPanel.tsx
│ │ │ │ │ ├── EntryContentWebView.tsx
│ │ │ │ │ ├── atom.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.android.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── injected-js.ts
│ │ │ │ │ ├── native-webview.android.tsx
│ │ │ │ │ ├── native-webview.tsx
│ │ │ │ │ └── webview-manager.ts
│ │ │ │ └── ui/
│ │ │ │ ├── accordion/
│ │ │ │ │ └── AccordionItem.tsx
│ │ │ │ ├── action-bar/
│ │ │ │ │ └── ActionBarItem.tsx
│ │ │ │ ├── avatar/
│ │ │ │ │ └── UserAvatar.tsx
│ │ │ │ ├── button/
│ │ │ │ │ └── UIBarButton.tsx
│ │ │ │ ├── carousel/
│ │ │ │ │ └── MediaCarousel.tsx
│ │ │ │ ├── context-menu/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── datetime/
│ │ │ │ │ └── RelativeDateTime.tsx
│ │ │ │ ├── form/
│ │ │ │ │ ├── FormProvider.tsx
│ │ │ │ │ ├── Label.tsx
│ │ │ │ │ ├── PickerIos.tsx
│ │ │ │ │ ├── Select.android.tsx
│ │ │ │ │ ├── Select.tsx
│ │ │ │ │ ├── Slider.tsx
│ │ │ │ │ ├── Switch.tsx
│ │ │ │ │ └── TextField.tsx
│ │ │ │ ├── grid/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── grouped/
│ │ │ │ │ ├── GroupedInsetListCardItemStyle.tsx
│ │ │ │ │ ├── GroupedList.tsx
│ │ │ │ │ └── constants.ts
│ │ │ │ ├── icon/
│ │ │ │ │ ├── fallback-icon.tsx
│ │ │ │ │ └── feed-icon.tsx
│ │ │ │ ├── image/
│ │ │ │ │ ├── Image.tsx
│ │ │ │ │ ├── ImageContextMenu.tsx
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── lightbox/
│ │ │ │ │ ├── ImageViewing/
│ │ │ │ │ │ ├── @types/
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── components/
│ │ │ │ │ │ │ ├── ImageDefaultHeader.tsx
│ │ │ │ │ │ │ └── ImageItem/
│ │ │ │ │ │ │ ├── ImageItem.android.tsx
│ │ │ │ │ │ │ ├── ImageItem.ios.tsx
│ │ │ │ │ │ │ └── ImageItem.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── transforms.ts
│ │ │ │ │ ├── Lightbox.tsx
│ │ │ │ │ └── lightboxState.tsx
│ │ │ │ ├── loading/
│ │ │ │ │ └── PlatformActivityIndicator.tsx
│ │ │ │ ├── logo/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── modal/
│ │ │ │ │ ├── BottomModal.tsx
│ │ │ │ │ └── imperative-modal/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── modal.tsx
│ │ │ │ │ └── templates.tsx
│ │ │ │ ├── overlay/
│ │ │ │ │ └── Overlay.tsx
│ │ │ │ ├── pressable/
│ │ │ │ │ ├── IosItemPressable.ios.tsx
│ │ │ │ │ ├── IosItemPressable.tsx
│ │ │ │ │ ├── ItemPressable.ios.tsx
│ │ │ │ │ ├── ItemPressable.tsx
│ │ │ │ │ ├── NativePressable.ios.tsx
│ │ │ │ │ ├── NativePressable.tsx
│ │ │ │ │ ├── NativePressable.types.tsx
│ │ │ │ │ └── enum.ts
│ │ │ │ ├── qrcode/
│ │ │ │ │ ├── LICENSE
│ │ │ │ │ ├── QRCode.tsx
│ │ │ │ │ ├── SVGPieces.tsx
│ │ │ │ │ ├── SVGRadialGradient.tsx
│ │ │ │ │ ├── adapter.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── useQRCodeData.ts
│ │ │ │ ├── slider/
│ │ │ │ │ ├── Slider.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── switch/
│ │ │ │ │ └── Switch.tsx
│ │ │ │ ├── tabview/
│ │ │ │ │ ├── TabBar.tsx
│ │ │ │ │ ├── TabView.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── toast/
│ │ │ │ │ ├── CenteredToast.tsx
│ │ │ │ │ ├── ToastContainer.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ ├── manager.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── typography/
│ │ │ │ │ ├── HtmlWeb.tsx
│ │ │ │ │ ├── MarkdownNative.tsx
│ │ │ │ │ ├── MonoText.tsx
│ │ │ │ │ └── Text.tsx
│ │ │ │ └── video/
│ │ │ │ ├── PlayerAction.tsx
│ │ │ │ └── VideoPlayer.tsx
│ │ │ ├── constants/
│ │ │ │ ├── native-images.ts
│ │ │ │ ├── spring.ts
│ │ │ │ ├── ui.ts
│ │ │ │ └── views.tsx
│ │ │ ├── database/
│ │ │ │ └── index.ts
│ │ │ ├── global.css
│ │ │ ├── hooks/
│ │ │ │ ├── useBackHandler.ts
│ │ │ │ ├── useDefaultHeaderHeight.ts
│ │ │ │ ├── useIntentHandler.ts
│ │ │ │ ├── useLoadingCallback.tsx
│ │ │ │ ├── useMessaging.ts
│ │ │ │ ├── useOnboarding.ts
│ │ │ │ ├── useUnreadCountBadge.ts
│ │ │ │ └── useWebViewNavigation.tsx
│ │ │ ├── icons/
│ │ │ │ ├── AZ_sort_ascending_letters_cute_re.tsx
│ │ │ │ ├── AZ_sort_descending_letters_cute_re.tsx
│ │ │ │ ├── VIP_2_cute_fi.tsx
│ │ │ │ ├── VIP_2_cute_re.tsx
│ │ │ │ ├── add_cute_fi.tsx
│ │ │ │ ├── add_cute_re.tsx
│ │ │ │ ├── ai_cute_fi.tsx
│ │ │ │ ├── ai_cute_re.tsx
│ │ │ │ ├── alert_cute_fi.tsx
│ │ │ │ ├── align_justify_cute_re.tsx
│ │ │ │ ├── align_left_cute_re.tsx
│ │ │ │ ├── announcement_cute_fi.tsx
│ │ │ │ ├── apple_cute_fi.tsx
│ │ │ │ ├── arrow_left_cute_re.tsx
│ │ │ │ ├── arrow_right_circle_cute_fi.tsx
│ │ │ │ ├── arrow_right_up_cute_re.tsx
│ │ │ │ ├── arrow_up_circle_cute_fi.tsx
│ │ │ │ ├── at_cute_re.tsx
│ │ │ │ ├── attachment_cute_re.tsx
│ │ │ │ ├── back_2_cute_re.tsx
│ │ │ │ ├── black_board_2_cute_fi.tsx
│ │ │ │ ├── black_board_2_cute_re.tsx
│ │ │ │ ├── book_6_cute_re.tsx
│ │ │ │ ├── bookmark_cute_re.tsx
│ │ │ │ ├── bubble_cute_fi.tsx
│ │ │ │ ├── bug_cute_re.tsx
│ │ │ │ ├── calendar_time_add_cute_re.tsx
│ │ │ │ ├── celebrate_cute_re.tsx
│ │ │ │ ├── certificate_cute_fi.tsx
│ │ │ │ ├── certificate_cute_re.tsx
│ │ │ │ ├── check_circle_cute_re.tsx
│ │ │ │ ├── check_circle_filled.tsx
│ │ │ │ ├── check_cute_re.tsx
│ │ │ │ ├── check_filled.tsx
│ │ │ │ ├── check_line.tsx
│ │ │ │ ├── classify_2_cute_re.tsx
│ │ │ │ ├── close_circle_fill.tsx
│ │ │ │ ├── close_cute_re.tsx
│ │ │ │ ├── comment_2_cute_re.tsx
│ │ │ │ ├── comment_cute_fi.tsx
│ │ │ │ ├── comment_cute_li.tsx
│ │ │ │ ├── comment_cute_re.tsx
│ │ │ │ ├── compass_3_cute_re.tsx
│ │ │ │ ├── compass_cute_fi.tsx
│ │ │ │ ├── copy_2_cute_re.tsx
│ │ │ │ ├── copy_cute_re.tsx
│ │ │ │ ├── cursor_3_cute_re.tsx
│ │ │ │ ├── danmaku_cute_fi.tsx
│ │ │ │ ├── database.tsx
│ │ │ │ ├── delete_2_cute_re.tsx
│ │ │ │ ├── department_cute_re.tsx
│ │ │ │ ├── discord_cute_fi.tsx
│ │ │ │ ├── docment_cute_fi.tsx
│ │ │ │ ├── docment_cute_re.tsx
│ │ │ │ ├── documents_cute_re.tsx
│ │ │ │ ├── download_2_cute_fi.tsx
│ │ │ │ ├── download_2_cute_re.tsx
│ │ │ │ ├── edit_cute_re.tsx
│ │ │ │ ├── emoji_2_cute_re.tsx
│ │ │ │ ├── exit_cute_fi.tsx
│ │ │ │ ├── exit_cute_re.tsx
│ │ │ │ ├── external_link_cute_re.tsx
│ │ │ │ ├── eye_2_cute_re.tsx
│ │ │ │ ├── eye_close_cute_re.tsx
│ │ │ │ ├── facebook_cute_fi.tsx
│ │ │ │ ├── facebook_cute_re.tsx
│ │ │ │ ├── fast_forward_cute_re.tsx
│ │ │ │ ├── file_import_cute_re.tsx
│ │ │ │ ├── file_upload_cute_re.tsx
│ │ │ │ ├── filter_cute_re.tsx
│ │ │ │ ├── finger_press_cute_re.tsx
│ │ │ │ ├── fire_cute_fi.tsx
│ │ │ │ ├── fire_cute_re.tsx
│ │ │ │ ├── flag_1_cute_fi.tsx
│ │ │ │ ├── folder_open_cute_re.tsx
│ │ │ │ ├── forward_2_cute_re.tsx
│ │ │ │ ├── fullscreen_2_cute_re.tsx
│ │ │ │ ├── fullscreen_cute_re.tsx
│ │ │ │ ├── fullscreen_exit_cute_re.tsx
│ │ │ │ ├── ghost_cute_re.tsx
│ │ │ │ ├── gift_cute_re.tsx
│ │ │ │ ├── github_2_cute_fi.tsx
│ │ │ │ ├── github_cute_fi.tsx
│ │ │ │ ├── google_cute_fi.tsx
│ │ │ │ ├── grid_2_cute_re.tsx
│ │ │ │ ├── grid_cute_re.tsx
│ │ │ │ ├── hammer_cute_re.tsx
│ │ │ │ ├── heart_cute_fi.tsx
│ │ │ │ ├── history_cute_re.tsx
│ │ │ │ ├── home_5_cute_fi.tsx
│ │ │ │ ├── home_5_cute_re.tsx
│ │ │ │ ├── hotkey_cute_re.tsx
│ │ │ │ ├── inbox_cute_fi.tsx
│ │ │ │ ├── inbox_cute_re.tsx
│ │ │ │ ├── info_circle_fill.tsx
│ │ │ │ ├── information_cute_re.tsx
│ │ │ │ ├── instagram_cute_fi.tsx
│ │ │ │ ├── key_2_cute_re.tsx
│ │ │ │ ├── layout_4_cute_re.tsx
│ │ │ │ ├── layout_leftbar_close_cute_re.tsx
│ │ │ │ ├── layout_leftbar_open_cute_re.tsx
│ │ │ │ ├── left_cute_fi.tsx
│ │ │ │ ├── left_small_sharp.tsx
│ │ │ │ ├── line_cute_re.tsx
│ │ │ │ ├── link_cute_re.tsx
│ │ │ │ ├── list_check_2_cute_re.tsx
│ │ │ │ ├── list_check_3_cute_re.tsx
│ │ │ │ ├── list_check_cute_re.tsx
│ │ │ │ ├── list_collapse_cute_fi.tsx
│ │ │ │ ├── list_collapse_cute_re.tsx
│ │ │ │ ├── list_expansion_cute_fi.tsx
│ │ │ │ ├── list_expansion_cute_re.tsx
│ │ │ │ ├── loading_3_cute_li.tsx
│ │ │ │ ├── loading_3_cute_re.tsx
│ │ │ │ ├── love_cute_fi.tsx
│ │ │ │ ├── love_cute_re.tsx
│ │ │ │ ├── magic_2_cute_fi.tsx
│ │ │ │ ├── magic_2_cute_re.tsx
│ │ │ │ ├── mail_cute_re.tsx
│ │ │ │ ├── mic_cute_fi.tsx
│ │ │ │ ├── mic_cute_re.tsx
│ │ │ │ ├── mind_map_cute_re.tsx
│ │ │ │ ├── mingcute_down_line.tsx
│ │ │ │ ├── mingcute_left_line.tsx
│ │ │ │ ├── mingcute_right_line.tsx
│ │ │ │ ├── more_1_cute_re.tsx
│ │ │ │ ├── music_2_cute_fi.tsx
│ │ │ │ ├── notification_cute_re.tsx
│ │ │ │ ├── numbers_09_sort_ascending_cute_re.tsx
│ │ │ │ ├── numbers_09_sort_descending_cute_re.tsx
│ │ │ │ ├── numbers_90_sort_ascending_cute_re.tsx
│ │ │ │ ├── numbers_90_sort_descending_cute_re.tsx
│ │ │ │ ├── palette_cute_fi.tsx
│ │ │ │ ├── palette_cute_re.tsx
│ │ │ │ ├── paper_cute_fi.tsx
│ │ │ │ ├── paste_cute_re.tsx
│ │ │ │ ├── pause_cute_fi.tsx
│ │ │ │ ├── pause_cute_re.tsx
│ │ │ │ ├── pdf_cute_re.tsx
│ │ │ │ ├── photo_album_cute_fi.tsx
│ │ │ │ ├── photo_album_cute_re.tsx
│ │ │ │ ├── pic_cute_fi.tsx
│ │ │ │ ├── pic_cute_re.tsx
│ │ │ │ ├── play_cute_fi.tsx
│ │ │ │ ├── play_cute_re.tsx
│ │ │ │ ├── plugin_2_cute_re.tsx
│ │ │ │ ├── polygon_cute_re.tsx
│ │ │ │ ├── power.tsx
│ │ │ │ ├── power_mono.tsx
│ │ │ │ ├── power_outline.tsx
│ │ │ │ ├── question_cute_re.tsx
│ │ │ │ ├── quill_pen_cute_re.tsx
│ │ │ │ ├── rada_cute_fi.tsx
│ │ │ │ ├── rada_cute_re.tsx
│ │ │ │ ├── refresh_2_cute_re.tsx
│ │ │ │ ├── rewind_backward_15_cute_re.tsx
│ │ │ │ ├── rewind_forward_30_cute_re.tsx
│ │ │ │ ├── right_cute_fi.tsx
│ │ │ │ ├── right_cute_li.tsx
│ │ │ │ ├── right_cute_re.tsx
│ │ │ │ ├── right_small_sharp.tsx
│ │ │ │ ├── rocket_cute_fi.tsx
│ │ │ │ ├── rocket_cute_re.tsx
│ │ │ │ ├── round_cute_fi.tsx
│ │ │ │ ├── round_cute_re.tsx
│ │ │ │ ├── rss_2_cute_fi.tsx
│ │ │ │ ├── rss_cute_fi.tsx
│ │ │ │ ├── sad_cute_re.tsx
│ │ │ │ ├── safe_alert_cute_re.tsx
│ │ │ │ ├── safe_lock_filled.tsx
│ │ │ │ ├── safety_certificate_cute_re.tsx
│ │ │ │ ├── save_cute_re.tsx
│ │ │ │ ├── search_2_cute_re.tsx
│ │ │ │ ├── search_3_cute_fi.tsx
│ │ │ │ ├── search_3_cute_re.tsx
│ │ │ │ ├── search_cute_re.tsx
│ │ │ │ ├── send_plane_cute_fi.tsx
│ │ │ │ ├── send_plane_cute_re.tsx
│ │ │ │ ├── settings_1_cute_fi.tsx
│ │ │ │ ├── settings_1_cute_re.tsx
│ │ │ │ ├── settings_7_cute_re.tsx
│ │ │ │ ├── share_forward_cute_re.tsx
│ │ │ │ ├── shuffle_2_cute_re.tsx
│ │ │ │ ├── social_x_cute_li.tsx
│ │ │ │ ├── social_x_cute_re.tsx
│ │ │ │ ├── sort_ascending_cute_re.tsx
│ │ │ │ ├── sort_descending_cute_re.tsx
│ │ │ │ ├── star_cute_fi.tsx
│ │ │ │ ├── star_cute_re.tsx
│ │ │ │ ├── stop_circle_cute_fi.tsx
│ │ │ │ ├── telegram_cute_fi.tsx
│ │ │ │ ├── telegram_cute_re.tsx
│ │ │ │ ├── thought_cute_fi.tsx
│ │ │ │ ├── time_cute_re.tsx
│ │ │ │ ├── tool_cute_re.tsx
│ │ │ │ ├── train_cute_fi.tsx
│ │ │ │ ├── translate_2_ai_cute_re.tsx
│ │ │ │ ├── translate_2_cute_re.tsx
│ │ │ │ ├── trending_up_cute_re.tsx
│ │ │ │ ├── trophy_cute_fi.tsx
│ │ │ │ ├── trophy_cute_re.tsx
│ │ │ │ ├── twitter_cute_fi.tsx
│ │ │ │ ├── up_cute_re.tsx
│ │ │ │ ├── user_3_cute_fi.tsx
│ │ │ │ ├── user_3_cute_re.tsx
│ │ │ │ ├── user_4_cute_fi.tsx
│ │ │ │ ├── user_4_cute_re.tsx
│ │ │ │ ├── user_add_2_cute_fi.tsx
│ │ │ │ ├── user_heart_cute_fi.tsx
│ │ │ │ ├── user_heart_cute_re.tsx
│ │ │ │ ├── user_setting_cute_fi.tsx
│ │ │ │ ├── user_setting_cute_re.tsx
│ │ │ │ ├── video_cute_fi.tsx
│ │ │ │ ├── video_cute_re.tsx
│ │ │ │ ├── voice_cute_re.tsx
│ │ │ │ ├── volume_cute_re.tsx
│ │ │ │ ├── volume_mute_cute_re.tsx
│ │ │ │ ├── volume_off_cute_re.tsx
│ │ │ │ ├── wallet_2_cute_fi.tsx
│ │ │ │ ├── warning_cute_re.tsx
│ │ │ │ ├── web_cute_re.tsx
│ │ │ │ ├── webhook_cute_re.tsx
│ │ │ │ ├── weibo_cute_re.tsx
│ │ │ │ ├── wifi_off_cute_re.tsx
│ │ │ │ ├── world_2_cute_fi.tsx
│ │ │ │ ├── world_2_cute_re.tsx
│ │ │ │ └── youtube_cute_fi.tsx
│ │ │ ├── initialize/
│ │ │ │ ├── analytics.ts
│ │ │ │ ├── app-check.ts
│ │ │ │ ├── background.ts
│ │ │ │ ├── dayjs.ts
│ │ │ │ ├── device.ts
│ │ │ │ ├── hydrate.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── migration.ts
│ │ │ │ └── player.ts
│ │ │ ├── interfaces/
│ │ │ │ └── settings/
│ │ │ │ └── data.ts
│ │ │ ├── lib/
│ │ │ │ ├── api-client.ts
│ │ │ │ ├── auth-cookie-migration.ts
│ │ │ │ ├── auth.ts
│ │ │ │ ├── client-session.ts
│ │ │ │ ├── dialog-state.ts
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── e2e-config.ts
│ │ │ │ ├── error-parser.ts
│ │ │ │ ├── event-bus.ts
│ │ │ │ ├── ga4.ts
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── image.ts
│ │ │ │ ├── img-proxy.ts
│ │ │ │ ├── jotai.ts
│ │ │ │ ├── kv.ts
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── native/
│ │ │ │ │ ├── index.ios.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── picker.ts
│ │ │ │ │ └── user-agent.ts
│ │ │ │ ├── navigation/
│ │ │ │ │ ├── AttachNavigationScrollViewContext.tsx
│ │ │ │ │ ├── ChainNavigationContext.tsx
│ │ │ │ │ ├── GroupedNavigationRouteContext.ts
│ │ │ │ │ ├── Navigation.ts
│ │ │ │ │ ├── NavigationInstanceContext.ts
│ │ │ │ │ ├── NavigationLink.tsx
│ │ │ │ │ ├── ScreenItemContext.ts
│ │ │ │ │ ├── ScreenNameContext.tsx
│ │ │ │ │ ├── ScreenOptionsContext.ts
│ │ │ │ │ ├── StackNavigation.tsx
│ │ │ │ │ ├── StackScreenHeaderPortal.tsx
│ │ │ │ │ ├── WrappedScreenItem.tsx
│ │ │ │ │ ├── __internal/
│ │ │ │ │ │ └── hooks.ts
│ │ │ │ │ ├── biz/
│ │ │ │ │ │ └── Destination.ts
│ │ │ │ │ ├── bottom-tab/
│ │ │ │ │ │ ├── BottomTabContext.tsx
│ │ │ │ │ │ ├── TabBarPortal.tsx
│ │ │ │ │ │ ├── TabRoot.tsx
│ │ │ │ │ │ ├── TabScreen.tsx
│ │ │ │ │ │ ├── TabScreenContext.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── native.ios.tsx
│ │ │ │ │ │ ├── native.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── debug/
│ │ │ │ │ │ └── DebugButtonGroup.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── readme.md
│ │ │ │ │ ├── sitemap/
│ │ │ │ │ │ └── registry.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── onboarding.ts
│ │ │ │ ├── parse-api-error.ts
│ │ │ │ ├── payment.ts
│ │ │ │ ├── permission.ts
│ │ │ │ ├── platform.ts
│ │ │ │ ├── player.ts
│ │ │ │ ├── proxy-env.ts
│ │ │ │ ├── query-client.ts
│ │ │ │ ├── responsive.ts
│ │ │ │ ├── secure-store.ts
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── token.ts
│ │ │ │ ├── url-builder.ts
│ │ │ │ └── volume.ts
│ │ │ ├── main.tsx
│ │ │ ├── modules/
│ │ │ │ ├── ai/
│ │ │ │ │ └── summary.tsx
│ │ │ │ ├── context-menu/
│ │ │ │ │ ├── entry.tsx
│ │ │ │ │ ├── feeds.tsx
│ │ │ │ │ ├── inbox.tsx
│ │ │ │ │ ├── lists.tsx
│ │ │ │ │ └── video.tsx
│ │ │ │ ├── debug/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── dialogs/
│ │ │ │ │ ├── ConfirmPasswordDialog.tsx
│ │ │ │ │ ├── ConfirmTOTPCodeDialog.tsx
│ │ │ │ │ ├── MarkAllAsReadDialog.tsx
│ │ │ │ │ └── UpgradeRequiredDialog.tsx
│ │ │ │ ├── discover/
│ │ │ │ │ ├── Category.tsx
│ │ │ │ │ ├── Content.tsx
│ │ │ │ │ ├── DiscoverContent.tsx
│ │ │ │ │ ├── FeedSummary.tsx
│ │ │ │ │ ├── RecommendationListItem.tsx
│ │ │ │ │ ├── Recommendations.tsx
│ │ │ │ │ ├── SearchContent.tsx
│ │ │ │ │ ├── SearchTabBar.tsx
│ │ │ │ │ ├── Trending.tsx
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ ├── search-tabs/
│ │ │ │ │ │ ├── SearchFeed.tsx
│ │ │ │ │ │ ├── SearchFeedCard.tsx
│ │ │ │ │ │ ├── SearchList.tsx
│ │ │ │ │ │ ├── __base.tsx
│ │ │ │ │ │ └── hooks.tsx
│ │ │ │ │ └── search.tsx
│ │ │ │ ├── entry-content/
│ │ │ │ │ ├── EntryAISummary.tsx
│ │ │ │ │ ├── EntryContentHeaderRightActions.tsx
│ │ │ │ │ ├── EntryGridFooter.tsx
│ │ │ │ │ ├── EntryNavigationHeader.tsx
│ │ │ │ │ ├── EntryReadHistory.tsx
│ │ │ │ │ ├── EntryTitle.tsx
│ │ │ │ │ ├── ctx.ts
│ │ │ │ │ └── pull-up-navigation/
│ │ │ │ │ ├── PullUpIndicatorAndroid.tsx
│ │ │ │ │ ├── PullUpIndicatorIos.tsx
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── use-pull-up-navigation.android.tsx
│ │ │ │ │ └── use-pull-up-navigation.tsx
│ │ │ │ ├── entry-list/
│ │ │ │ │ ├── EntryListContentArticle.tsx
│ │ │ │ │ ├── EntryListContentPicture.tsx
│ │ │ │ │ ├── EntryListContentSocial.tsx
│ │ │ │ │ ├── EntryListContentVideo.tsx
│ │ │ │ │ ├── EntryListContext.tsx
│ │ │ │ │ ├── EntryListEmpty.tsx
│ │ │ │ │ ├── EntryListFooter.tsx
│ │ │ │ │ ├── EntryListSelector.tsx
│ │ │ │ │ ├── ItemSeparator.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── templates/
│ │ │ │ │ │ ├── EntryNormalItem.tsx
│ │ │ │ │ │ ├── EntryPictureItem.tsx
│ │ │ │ │ │ ├── EntrySocialItem.tsx
│ │ │ │ │ │ ├── EntryTranslation.tsx
│ │ │ │ │ │ └── EntryVideoItem.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ ├── feed/
│ │ │ │ │ ├── FollowFeed.tsx
│ │ │ │ │ └── view-selector.tsx
│ │ │ │ ├── list/
│ │ │ │ │ └── FollowList.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── email.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── social.tsx
│ │ │ │ ├── onboarding/
│ │ │ │ │ ├── feeds-english.json
│ │ │ │ │ ├── feeds.json
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ └── use-reading-behavior.ts
│ │ │ │ │ ├── preset.ts
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ ├── step-finished.tsx
│ │ │ │ │ ├── step-interests.tsx
│ │ │ │ │ ├── step-preferences.tsx
│ │ │ │ │ └── step-welcome.tsx
│ │ │ │ ├── player/
│ │ │ │ │ ├── GlassPlayerTabBar.tsx
│ │ │ │ │ ├── PlayerTabBar.tsx
│ │ │ │ │ ├── context.ts
│ │ │ │ │ ├── control.tsx
│ │ │ │ │ └── hooks.ts
│ │ │ │ ├── review-prompt/
│ │ │ │ │ ├── debug.ts
│ │ │ │ │ ├── provider.tsx
│ │ │ │ │ ├── use-review-prompt-state.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── rsshub/
│ │ │ │ │ └── preview-url.tsx
│ │ │ │ ├── screen/
│ │ │ │ │ ├── PagerList.ios.tsx
│ │ │ │ │ ├── PagerList.tsx
│ │ │ │ │ ├── PagerListContext.ts
│ │ │ │ │ ├── TimelineSelectorList.tsx
│ │ │ │ │ ├── TimelineSelectorProvider.tsx
│ │ │ │ │ ├── TimelineViewSelector.tsx
│ │ │ │ │ ├── TimelineViewSelectorContextMenu.tsx
│ │ │ │ │ ├── action.tsx
│ │ │ │ │ ├── atoms.ts
│ │ │ │ │ └── hooks/
│ │ │ │ │ └── useHeaderHeight.tsx
│ │ │ │ ├── settings/
│ │ │ │ │ ├── SettingsList.tsx
│ │ │ │ │ ├── UserHeaderBanner.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ └── OTPWindow.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── useShareSubscription.tsx
│ │ │ │ │ │ └── useTOTPModalWrapper.tsx
│ │ │ │ │ ├── routes/
│ │ │ │ │ │ ├── 2FASetting.tsx
│ │ │ │ │ │ ├── About.tsx
│ │ │ │ │ │ ├── Account.tsx
│ │ │ │ │ │ ├── Achievement.tsx
│ │ │ │ │ │ ├── Actions.tsx
│ │ │ │ │ │ ├── Appearance.tsx
│ │ │ │ │ │ ├── Data.tsx
│ │ │ │ │ │ ├── EditCondition.tsx
│ │ │ │ │ │ ├── EditProfile.tsx
│ │ │ │ │ │ ├── EditRewriteRules.tsx
│ │ │ │ │ │ ├── EditRule.tsx
│ │ │ │ │ │ ├── EditWebhooks.tsx
│ │ │ │ │ │ ├── Feeds.tsx
│ │ │ │ │ │ ├── General.tsx
│ │ │ │ │ │ ├── Lists.tsx
│ │ │ │ │ │ ├── ManageList.tsx
│ │ │ │ │ │ ├── Notifications.tsx
│ │ │ │ │ │ ├── Plan.tsx
│ │ │ │ │ │ ├── Privacy.tsx
│ │ │ │ │ │ ├── ResetPassword.tsx
│ │ │ │ │ │ └── navigateToPlanScreen.ts
│ │ │ │ │ ├── sync-queue.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ └── subscription/
│ │ │ │ ├── CategoryGrouped.tsx
│ │ │ │ ├── ItemSeparator.tsx
│ │ │ │ ├── SubscriptionLists.tsx
│ │ │ │ ├── UnGroupedList.tsx
│ │ │ │ ├── atoms.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── ctx.ts
│ │ │ │ ├── header-actions.tsx
│ │ │ │ └── items/
│ │ │ │ ├── InboxItem.tsx
│ │ │ │ ├── ListSubscriptionItem.tsx
│ │ │ │ ├── SubscriptionItem.tsx
│ │ │ │ ├── UnreadCount.tsx
│ │ │ │ └── types.tsx
│ │ │ ├── polyfill/
│ │ │ │ ├── index.ts
│ │ │ │ └── promise-with-resolvers.ts
│ │ │ ├── providers/
│ │ │ │ ├── AppleIAPProvider.tsx
│ │ │ │ ├── FontScalingProvider.tsx
│ │ │ │ ├── ServerConfigsLoader.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── migration.tsx
│ │ │ ├── screens/
│ │ │ │ ├── (headless)/
│ │ │ │ │ ├── (debug)/
│ │ │ │ │ │ ├── markdown.tsx
│ │ │ │ │ │ └── text.tsx
│ │ │ │ │ └── DebugScreen.tsx
│ │ │ │ ├── (modal)/
│ │ │ │ │ ├── DiscoverSettingsScreen.tsx
│ │ │ │ │ ├── EditEmailScreen.tsx
│ │ │ │ │ ├── FollowScreen.tsx
│ │ │ │ │ ├── ForgetPasswordScreen.tsx
│ │ │ │ │ ├── ListScreen.tsx
│ │ │ │ │ ├── LoginScreen.tsx
│ │ │ │ │ ├── ProfileScreen.tsx
│ │ │ │ │ ├── RsshubFormScreen.tsx
│ │ │ │ │ ├── TwoFactorAuthScreen.tsx
│ │ │ │ │ └── onboarding/
│ │ │ │ │ ├── EditProfileScreen.tsx
│ │ │ │ │ └── SelectReadingModeScreen.tsx
│ │ │ │ ├── (stack)/
│ │ │ │ │ ├── (tabs)/
│ │ │ │ │ │ ├── discover.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── settings.tsx
│ │ │ │ │ │ └── subscriptions.tsx
│ │ │ │ │ ├── entries/
│ │ │ │ │ │ └── [entryId]/
│ │ │ │ │ │ └── EntryDetailScreen.tsx
│ │ │ │ │ ├── feeds/
│ │ │ │ │ │ └── [feedId]/
│ │ │ │ │ │ └── FeedScreen.tsx
│ │ │ │ │ └── recommendation/
│ │ │ │ │ └── RecommendationCategoryScreen.tsx
│ │ │ │ ├── +native-intent.tsx
│ │ │ │ ├── OnboardingScreen.tsx
│ │ │ │ └── PlayerScreen.tsx
│ │ │ ├── sitemap.tsx
│ │ │ ├── spec/
│ │ │ │ └── typography.ts
│ │ │ ├── store/
│ │ │ │ └── image/
│ │ │ │ ├── hooks.ts
│ │ │ │ └── store.ts
│ │ │ └── theme/
│ │ │ ├── colors.ts
│ │ │ ├── utils.ts
│ │ │ └── web.ts
│ │ ├── tailwind.config.ts
│ │ ├── tailwind.dom.config.ts
│ │ ├── tsconfig.json
│ │ └── web-app/
│ │ ├── html-renderer/
│ │ │ ├── global.d.ts
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── postcss.config.cjs
│ │ │ ├── src/
│ │ │ │ ├── App.tsx
│ │ │ │ ├── HTML.tsx
│ │ │ │ ├── atoms/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── ProviderComposer.tsx
│ │ │ │ │ └── WrappedElementProvider.tsx
│ │ │ │ ├── components/
│ │ │ │ │ ├── __internal/
│ │ │ │ │ │ ├── calculateDimensions.tsx
│ │ │ │ │ │ └── ctx.ts
│ │ │ │ │ ├── heading.tsx
│ │ │ │ │ ├── image.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── link.tsx
│ │ │ │ │ ├── math.tsx
│ │ │ │ │ ├── p.tsx
│ │ │ │ │ └── shiki/
│ │ │ │ │ ├── Shiki.tsx
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ └── shiki.module.css
│ │ │ │ ├── index.css
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers/
│ │ │ │ │ └── webview-bridge.ts
│ │ │ │ ├── parser.tsx
│ │ │ │ ├── test.txt
│ │ │ │ └── utils.ts
│ │ │ ├── tailwind.config.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── types/
│ │ │ │ └── index.ts
│ │ │ └── vite.config.mts
│ │ └── package.json
│ └── ssr/
│ ├── .env.example
│ ├── api/
│ │ └── index.ts
│ ├── client/
│ │ ├── @types/
│ │ │ ├── constants.ts
│ │ │ ├── default-resource.ts
│ │ │ └── i18next.d.ts
│ │ ├── App.tsx
│ │ ├── atoms/
│ │ │ ├── server-configs.ts
│ │ │ ├── settings/
│ │ │ │ ├── general.ts
│ │ │ │ └── helper.ts
│ │ │ └── user.ts
│ │ ├── components/
│ │ │ ├── common/
│ │ │ │ ├── 404.tsx
│ │ │ │ └── PoweredByFooter.tsx
│ │ │ ├── items/
│ │ │ │ ├── grid.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── normal.tsx
│ │ │ │ └── picture.tsx
│ │ │ ├── layout/
│ │ │ │ └── header/
│ │ │ │ └── index.tsx
│ │ │ └── ui/
│ │ │ ├── feed-certification.tsx
│ │ │ ├── feed-icon.tsx
│ │ │ ├── image.tsx
│ │ │ └── user-avatar.tsx
│ │ ├── configs.ts
│ │ ├── global.d.ts
│ │ ├── hooks/
│ │ │ └── useRecaptchaToken.ts
│ │ ├── i18n.ts
│ │ ├── index.tsx
│ │ ├── initialize/
│ │ │ ├── helper.ts
│ │ │ ├── index.ts
│ │ │ └── sentry.ts
│ │ ├── lib/
│ │ │ ├── api-fetch.ts
│ │ │ ├── auth.ts
│ │ │ ├── helper.ts
│ │ │ ├── query-client.ts
│ │ │ ├── store.ts
│ │ │ └── url-builder.ts
│ │ ├── modules/
│ │ │ └── login/
│ │ │ └── index.tsx
│ │ ├── pages/
│ │ │ ├── (login)/
│ │ │ │ ├── forget-password.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── login/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ ├── register.tsx
│ │ │ │ └── reset-password.tsx
│ │ │ ├── (main)/
│ │ │ │ ├── index.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── share/
│ │ │ │ ├── feeds/
│ │ │ │ │ └── [id]/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ ├── lists/
│ │ │ │ │ └── [id]/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── metadata.ts
│ │ │ │ └── users/
│ │ │ │ └── [id]/
│ │ │ │ ├── index.tsx
│ │ │ │ └── metadata.ts
│ │ │ └── layout.tsx
│ │ ├── providers/
│ │ │ ├── root-providers.tsx
│ │ │ ├── server-configs-provider.tsx
│ │ │ └── user-provider.tsx
│ │ ├── query/
│ │ │ ├── auth.ts
│ │ │ ├── entries.ts
│ │ │ ├── feed.ts
│ │ │ ├── list.ts
│ │ │ └── users.ts
│ │ ├── router.tsx
│ │ └── styles/
│ │ └── index.css
│ ├── global.ts
│ ├── helper/
│ │ └── meta-map.ts
│ ├── index.html
│ ├── index.ts
│ ├── note.md
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── public/
│ │ └── manifest.json
│ ├── scripts/
│ │ ├── check-fonts.ts
│ │ ├── cleanup-vercel-build.ts
│ │ ├── generate-font-data.ts
│ │ ├── patch-worker-build.ts
│ │ ├── prepare-vercel-build.ts
│ │ ├── skip-ssr-app-vercel-build.sh
│ │ └── upload-fonts-to-r2.ts
│ ├── src/
│ │ ├── global.d.ts
│ │ ├── lib/
│ │ │ ├── api-client.ts
│ │ │ ├── dev-vite.ts
│ │ │ ├── load-env.ts
│ │ │ ├── load-env.worker.ts
│ │ │ ├── not-found.ts
│ │ │ ├── og/
│ │ │ │ ├── fonts.ts
│ │ │ │ ├── fonts.worker.ts
│ │ │ │ ├── render-to-image.ts
│ │ │ │ ├── render-to-image.worker.ts
│ │ │ │ └── resvg-wasm-shim.ts
│ │ │ ├── seo.ts
│ │ │ └── worker-request-context.ts
│ │ ├── meta-handler.map.ts
│ │ ├── meta-handler.ts
│ │ └── router/
│ │ ├── global.ts
│ │ └── og/
│ │ ├── __base.tsx
│ │ ├── feed.tsx
│ │ ├── index.ts
│ │ ├── list.tsx
│ │ └── user.tsx
│ ├── tailwind.config.ts
│ ├── tsconfig.json
│ ├── tsdown.config.ts
│ ├── tsdown.worker.config.ts
│ ├── vercel.json
│ ├── vite.config.mts
│ ├── worker-app.ts
│ ├── worker-entry.ts
│ └── wrangler.jsonc
├── changelogithub.config.ts
├── conductor.json
├── eslint.config.mjs
├── locales/
│ ├── ai/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── app/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── common/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── errors/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── external/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── lang/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── mobile/
│ │ └── default/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── native/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ ├── settings/
│ │ ├── en.json
│ │ ├── fr-FR.json
│ │ ├── ja.json
│ │ ├── zh-CN.json
│ │ └── zh-TW.json
│ └── shortcuts/
│ ├── en.json
│ ├── fr-FR.json
│ ├── ja.json
│ ├── zh-CN.json
│ └── zh-TW.json
├── package.json
├── packages/
│ ├── configs/
│ │ ├── package.json
│ │ ├── tailwindcss/
│ │ │ ├── ratio-mixing-plugin.js
│ │ │ ├── tailwind-extend.css
│ │ │ ├── tw-css-plugin.js
│ │ │ └── web.ts
│ │ └── tsconfig.extend.json
│ ├── internal/
│ │ ├── AGENTS.md
│ │ ├── atoms/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── atoms/
│ │ │ │ │ └── user.ts
│ │ │ │ └── helper/
│ │ │ │ └── setting.ts
│ │ │ └── tsconfig.json
│ │ ├── components/
│ │ │ ├── assets/
│ │ │ │ ├── colors-media.css
│ │ │ │ ├── colors.css
│ │ │ │ ├── font.css
│ │ │ │ ├── index.css
│ │ │ │ └── tailwind.css
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── atoms/
│ │ │ │ │ ├── mouse.ts
│ │ │ │ │ ├── route.ts
│ │ │ │ │ └── viewport.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── Focusable/
│ │ │ │ │ │ ├── Focusable.tsx
│ │ │ │ │ │ ├── GlobalFocusableProvider.tsx
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── Fragment.ts
│ │ │ │ │ ├── MemoedDangerousHTMLStyle.tsx
│ │ │ │ │ ├── MotionProvider.tsx
│ │ │ │ │ └── ReparentPortal.tsx
│ │ │ │ ├── constants/
│ │ │ │ │ └── spring.ts
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── useMedia.ts
│ │ │ │ │ ├── useMobile.ts
│ │ │ │ │ ├── useMouse.ts
│ │ │ │ │ └── useViewport.ts
│ │ │ │ ├── icons/
│ │ │ │ │ ├── Database.tsx
│ │ │ │ │ ├── Meditation.tsx
│ │ │ │ │ ├── MynauiInboxArchive.tsx
│ │ │ │ │ ├── OouiUserAnonymous.tsx
│ │ │ │ │ ├── PhCloudCheck.tsx
│ │ │ │ │ ├── PhCloudWarning.tsx
│ │ │ │ │ ├── PhCloudX.tsx
│ │ │ │ │ ├── Progress.tsx
│ │ │ │ │ ├── empty.tsx
│ │ │ │ │ ├── follow.tsx
│ │ │ │ │ ├── folo.tsx
│ │ │ │ │ ├── infinify.tsx
│ │ │ │ │ ├── logo.tsx
│ │ │ │ │ ├── nft.tsx
│ │ │ │ │ ├── resize.tsx
│ │ │ │ │ ├── user.tsx
│ │ │ │ │ └── users.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ ├── event-provider.tsx
│ │ │ │ │ └── stable-router-provider.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── auto-resize-height/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── avatar/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── avatar-group/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── action-button.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── interface.ts
│ │ │ │ │ │ └── variants.tsx
│ │ │ │ │ ├── card/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── collapse/
│ │ │ │ │ │ ├── Collapse.tsx
│ │ │ │ │ │ ├── CollapseCss.tsx
│ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── context-menu/
│ │ │ │ │ │ ├── context-menu.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── datetime/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.tsx
│ │ │ │ │ ├── divider/
│ │ │ │ │ │ ├── Divider.tsx
│ │ │ │ │ │ ├── PanelSplitter.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── drop-zone/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── effect/
│ │ │ │ │ │ └── MagneticHoverEffect.tsx
│ │ │ │ │ ├── form/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── icon/
│ │ │ │ │ │ └── SiteIcon.tsx
│ │ │ │ │ ├── input/
│ │ │ │ │ │ ├── DateTimePicker.tsx
│ │ │ │ │ │ ├── Input.tsx
│ │ │ │ │ │ ├── InputV2.tsx
│ │ │ │ │ │ ├── OTP.tsx
│ │ │ │ │ │ ├── TextArea.tsx
│ │ │ │ │ │ ├── TextAreaWrapper.tsx
│ │ │ │ │ │ ├── TimeSelect.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── json-highlighter/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── katex/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── lazy.tsx
│ │ │ │ │ ├── kbd/
│ │ │ │ │ │ └── Kbd.tsx
│ │ │ │ │ ├── key-value-editor/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── label/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── lexical-rich-editor/
│ │ │ │ │ │ ├── LexicalRichEditor.tsx
│ │ │ │ │ │ ├── LexicalRichEditorTextArea.tsx
│ │ │ │ │ │ ├── editor.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── nodes.ts
│ │ │ │ │ │ ├── plugins/
│ │ │ │ │ │ │ ├── code-highlighting/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── exit-code/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── keyboard/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── string-length-change/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── triple-backtick-toggle/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── theme.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── link/
│ │ │ │ │ │ ├── LinkWithTooltip.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── loading/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── markdown/
│ │ │ │ │ │ └── html.tsx
│ │ │ │ │ ├── marquee/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── masonry/
│ │ │ │ │ │ ├── contexts.tsx
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── navigation-menu/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── style.ts
│ │ │ │ │ ├── platform-icon/
│ │ │ │ │ │ ├── collections/
│ │ │ │ │ │ │ ├── cubox.tsx
│ │ │ │ │ │ │ ├── eagle.tsx
│ │ │ │ │ │ │ ├── instapaper.tsx
│ │ │ │ │ │ │ ├── obsidian.tsx
│ │ │ │ │ │ │ ├── outline.tsx
│ │ │ │ │ │ │ ├── readeck.tsx
│ │ │ │ │ │ │ ├── readwise.tsx
│ │ │ │ │ │ │ ├── rss3.tsx
│ │ │ │ │ │ │ ├── rsshub.tsx
│ │ │ │ │ │ │ └── zotero.tsx
│ │ │ │ │ │ ├── icons.ts
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── utils.tsx
│ │ │ │ │ ├── popover/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── portal/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── provider.tsx
│ │ │ │ │ ├── progress/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── progressive-blur/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── radio-group/
│ │ │ │ │ │ ├── RadioCard.tsx
│ │ │ │ │ │ ├── RadioGroup.tsx
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── motion.tsx
│ │ │ │ │ ├── scroll-area/
│ │ │ │ │ │ ├── ScrollArea.tsx
│ │ │ │ │ │ ├── ctx.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── index.module.css
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── segment/
│ │ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── select/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── responsive.tsx
│ │ │ │ │ ├── sheet/
│ │ │ │ │ │ ├── Sheet.tsx
│ │ │ │ │ │ ├── context.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── shiny-text/
│ │ │ │ │ │ ├── ShinyText.tsx
│ │ │ │ │ │ └── index.module.css
│ │ │ │ │ ├── shrinking-focus-border/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── skeleton/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── slider/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── switch/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── table/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── variants.tsx
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── toast/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── tooltip/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── styles.ts
│ │ │ │ │ ├── typography/
│ │ │ │ │ │ ├── EllipsisWithTooltip.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── z-index/
│ │ │ │ │ ├── ctx.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── dayjs.ts
│ │ │ │ ├── icon.ts
│ │ │ │ ├── parse-markdown.tsx
│ │ │ │ └── selector.tsx
│ │ │ └── tsconfig.json
│ │ ├── constants/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── app.ts
│ │ │ │ ├── auth-providers.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── rsshub.ts
│ │ │ │ ├── social.ts
│ │ │ │ └── tabs.tsx
│ │ │ └── tsconfig.json
│ │ ├── database/
│ │ │ ├── drizzle.config.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── DatabaseSource.js
│ │ │ │ ├── ResourceLock.ts
│ │ │ │ ├── constant.ts
│ │ │ │ ├── db.desktop.ts
│ │ │ │ ├── db.rn.ts
│ │ │ │ ├── db.ts
│ │ │ │ ├── drizzle/
│ │ │ │ │ ├── 0000_harsh_shiva.sql
│ │ │ │ │ ├── 0001_bored_hobgoblin.sql
│ │ │ │ │ ├── 0002_smart_power_man.sql
│ │ │ │ │ ├── 0003_known_roland_deschain.sql
│ │ │ │ │ ├── 0004_majestic_thunderbolt_ross.sql
│ │ │ │ │ ├── 0005_tense_sleepwalker.sql
│ │ │ │ │ ├── 0006_exotic_kid_colt.sql
│ │ │ │ │ ├── 0007_curvy_tarantula.sql
│ │ │ │ │ ├── 0008_last_the_santerians.sql
│ │ │ │ │ ├── 0009_lucky_power_man.sql
│ │ │ │ │ ├── 0010_legal_ben_grimm.sql
│ │ │ │ │ ├── 0011_mysterious_stark_industries.sql
│ │ │ │ │ ├── 0012_magenta_thing.sql
│ │ │ │ │ ├── 0013_chunky_stephen_strange.sql
│ │ │ │ │ ├── 0014_chemical_shocker.sql
│ │ │ │ │ ├── 0015_colorful_warbird.sql
│ │ │ │ │ ├── 0016_curious_carnage.sql
│ │ │ │ │ ├── 0017_talented_captain_cross.sql
│ │ │ │ │ ├── 0018_dashing_the_fury.sql
│ │ │ │ │ ├── 0019_wonderful_shape.sql
│ │ │ │ │ ├── 0020_little_marauders.sql
│ │ │ │ │ ├── 0021_wakeful_onslaught.sql
│ │ │ │ │ ├── 0022_tiny_northstar.sql
│ │ │ │ │ ├── 0023_pink_namor.sql
│ │ │ │ │ ├── 0024_spooky_alex_power.sql
│ │ │ │ │ ├── 0025_colorful_valkyrie.sql
│ │ │ │ │ ├── 0026_numerous_slyde.sql
│ │ │ │ │ ├── 0027_nostalgic_human_torch.sql
│ │ │ │ │ ├── 0028_chief_cyclops.sql
│ │ │ │ │ ├── 0029_flaky_gorgon.sql
│ │ │ │ │ ├── 0030_common_gabe_jones.sql
│ │ │ │ │ ├── 0031_kind_ikaris.sql
│ │ │ │ │ ├── 0032_orange_prima.sql
│ │ │ │ │ ├── 0033_shiny_sebastian_shaw.sql
│ │ │ │ │ ├── 0034_curly_darkstar.sql
│ │ │ │ │ ├── 0035_last_valeria_richards.sql
│ │ │ │ │ ├── 0036_entry_tag_summary.sql
│ │ │ │ │ ├── 0037_bored_the_leader.sql
│ │ │ │ │ ├── meta/
│ │ │ │ │ │ ├── 0000_snapshot.json
│ │ │ │ │ │ ├── 0001_snapshot.json
│ │ │ │ │ │ ├── 0002_snapshot.json
│ │ │ │ │ │ ├── 0003_snapshot.json
│ │ │ │ │ │ ├── 0004_snapshot.json
│ │ │ │ │ │ ├── 0005_snapshot.json
│ │ │ │ │ │ ├── 0006_snapshot.json
│ │ │ │ │ │ ├── 0007_snapshot.json
│ │ │ │ │ │ ├── 0008_snapshot.json
│ │ │ │ │ │ ├── 0009_snapshot.json
│ │ │ │ │ │ ├── 0010_snapshot.json
│ │ │ │ │ │ ├── 0011_snapshot.json
│ │ │ │ │ │ ├── 0012_snapshot.json
│ │ │ │ │ │ ├── 0013_snapshot.json
│ │ │ │ │ │ ├── 0014_snapshot.json
│ │ │ │ │ │ ├── 0015_snapshot.json
│ │ │ │ │ │ ├── 0016_snapshot.json
│ │ │ │ │ │ ├── 0017_snapshot.json
│ │ │ │ │ │ ├── 0018_snapshot.json
│ │ │ │ │ │ ├── 0019_snapshot.json
│ │ │ │ │ │ ├── 0020_snapshot.json
│ │ │ │ │ │ ├── 0021_snapshot.json
│ │ │ │ │ │ ├── 0022_snapshot.json
│ │ │ │ │ │ ├── 0023_snapshot.json
│ │ │ │ │ │ ├── 0024_snapshot.json
│ │ │ │ │ │ ├── 0025_snapshot.json
│ │ │ │ │ │ ├── 0026_snapshot.json
│ │ │ │ │ │ ├── 0027_snapshot.json
│ │ │ │ │ │ ├── 0028_snapshot.json
│ │ │ │ │ │ ├── 0029_snapshot.json
│ │ │ │ │ │ ├── 0030_snapshot.json
│ │ │ │ │ │ ├── 0031_snapshot.json
│ │ │ │ │ │ ├── 0032_snapshot.json
│ │ │ │ │ │ ├── 0033_snapshot.json
│ │ │ │ │ │ ├── 0034_snapshot.json
│ │ │ │ │ │ ├── 0035_snapshot.json
│ │ │ │ │ │ ├── 0036_snapshot.json
│ │ │ │ │ │ ├── 0037_snapshot.json
│ │ │ │ │ │ └── _journal.json
│ │ │ │ │ └── migrations.js
│ │ │ │ ├── migrator.ts
│ │ │ │ ├── schemas/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── services/
│ │ │ │ │ ├── collection.ts
│ │ │ │ │ ├── entry.ts
│ │ │ │ │ ├── feed.ts
│ │ │ │ │ ├── image.ts
│ │ │ │ │ ├── inbox.ts
│ │ │ │ │ ├── internal/
│ │ │ │ │ │ ├── base.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── list.ts
│ │ │ │ │ ├── subscription.ts
│ │ │ │ │ ├── summary.ts
│ │ │ │ │ ├── translation.ts
│ │ │ │ │ ├── unread.ts
│ │ │ │ │ └── user.ts
│ │ │ │ └── types.ts
│ │ │ └── tsconfig.json
│ │ ├── hooks/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── factory/
│ │ │ │ │ └── createHTMLMediaHook.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── internal/
│ │ │ │ │ └── for-theme.ts
│ │ │ │ ├── optimistic/
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── strategies.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── useOptimisticMutation.ts
│ │ │ │ ├── useAnyPointDown.ts
│ │ │ │ ├── useControlled.ts
│ │ │ │ ├── useCountDown.ts
│ │ │ │ ├── useDark.ts
│ │ │ │ ├── useElementWidth.ts
│ │ │ │ ├── useInputComposition.ts
│ │ │ │ ├── useInterval.ts
│ │ │ │ ├── useIsOnline.ts
│ │ │ │ ├── useLongPress.ts
│ │ │ │ ├── useMeasure.ts
│ │ │ │ ├── useOnce.ts
│ │ │ │ ├── usePageVisibility.ts
│ │ │ │ ├── usePrevious.ts
│ │ │ │ ├── useRefValue.ts
│ │ │ │ ├── useSetState.ts
│ │ │ │ ├── useSmoothScroll.ts
│ │ │ │ ├── useSyncTheme.ts
│ │ │ │ ├── useTitle.ts
│ │ │ │ ├── useTriangleMenu.ts
│ │ │ │ ├── useTypescriptHappyCallback.ts
│ │ │ │ └── useVideo.ts
│ │ │ └── tsconfig.json
│ │ ├── logger/
│ │ │ ├── electron.ts
│ │ │ ├── package.json
│ │ │ ├── tsconfig.json
│ │ │ └── web.ts
│ │ ├── models/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ └── rsshub.ts
│ │ │ └── tsconfig.json
│ │ ├── shared/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── auth.ts
│ │ │ │ ├── bridge.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── electron.ts
│ │ │ │ ├── env.common.ts
│ │ │ │ ├── env.desktop.ts
│ │ │ │ ├── env.rn.ts
│ │ │ │ ├── env.ssr.ts
│ │ │ │ ├── event.ts
│ │ │ │ ├── global.d.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── language.ts
│ │ │ │ ├── queue.ts
│ │ │ │ ├── review-prompt.test.ts
│ │ │ │ ├── review-prompt.ts
│ │ │ │ └── settings/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── defaults.ts
│ │ │ │ ├── hook.ts
│ │ │ │ └── interface.ts
│ │ │ └── tsconfig.json
│ │ ├── store/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── @types/
│ │ │ │ │ ├── default-resource.ts
│ │ │ │ │ └── i18next.d.ts
│ │ │ │ ├── constants/
│ │ │ │ │ ├── app.ts
│ │ │ │ │ └── onboarding.ts
│ │ │ │ ├── context.ts
│ │ │ │ ├── hydrate.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── helper.ts
│ │ │ │ │ └── stream.ts
│ │ │ │ ├── modules/
│ │ │ │ │ ├── action/
│ │ │ │ │ │ ├── constant.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── collection/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── entry/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── feed/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── image/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ └── store.ts
│ │ │ │ │ ├── inbox/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── list/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── subscription/
│ │ │ │ │ │ ├── getter.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── summary/
│ │ │ │ │ │ ├── enum.ts
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── translation/
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── unread/
│ │ │ │ │ │ ├── getters.ts
│ │ │ │ │ │ ├── hooks.ts
│ │ │ │ │ │ ├── selectors.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ └── user/
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── getters.ts
│ │ │ │ │ ├── hooks.ts
│ │ │ │ │ ├── store.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── morph/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── db-store.ts
│ │ │ │ │ └── store-db.ts
│ │ │ │ ├── reset.ts
│ │ │ │ └── types.ts
│ │ │ └── tsconfig.json
│ │ ├── tracker/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── adapters/
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── firebase.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── posthog.ts
│ │ │ │ │ └── proxy.ts
│ │ │ │ ├── enums.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── manager.ts
│ │ │ │ ├── track-manager.ts
│ │ │ │ ├── tracker-points.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils.ts
│ │ │ └── tsconfig.json
│ │ ├── types/
│ │ │ ├── global.d.ts
│ │ │ ├── package.json
│ │ │ ├── react-global.d.ts
│ │ │ └── vite-env.d.ts
│ │ └── utils/
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── attribution.ts
│ │ │ ├── bind-this.ts
│ │ │ ├── chain.ts
│ │ │ ├── cjk.ts
│ │ │ ├── color.ts
│ │ │ ├── data-structure/
│ │ │ │ ├── index.ts
│ │ │ │ └── set.ts
│ │ │ ├── dom.ts
│ │ │ ├── duration.ts
│ │ │ ├── environment.ts
│ │ │ ├── event-bus.rn.ts
│ │ │ ├── event-bus.ts
│ │ │ ├── headers.ts
│ │ │ ├── html.ts
│ │ │ ├── img-proxy.ts
│ │ │ ├── index.ts
│ │ │ ├── jotai.ts
│ │ │ ├── json-codec.ts
│ │ │ ├── language.ts
│ │ │ ├── link-parser.ts
│ │ │ ├── lru-cache.test.ts
│ │ │ ├── lru-cache.ts
│ │ │ ├── noop.ts
│ │ │ ├── ns.ts
│ │ │ ├── path-parser.test.ts
│ │ │ ├── path-parser.ts
│ │ │ ├── react.ts
│ │ │ ├── resize.ts
│ │ │ ├── scroller.ts
│ │ │ ├── url-builder.ts
│ │ │ ├── url-for-video.ts
│ │ │ ├── utils.spec.ts
│ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── readability/
│ ├── bump.config.ts
│ ├── package.json
│ ├── src/
│ │ └── index.ts
│ ├── tsconfig.json
│ └── tsdown.config.ts
├── patches/
│ ├── @microflash__remark-callout-directives.patch
│ ├── @mozilla__readability@0.6.0.patch
│ ├── @pengx17__electron-forge-maker-appimage.patch
│ ├── daisyui@4.12.24.patch
│ ├── re-resizable@6.11.2.patch
│ ├── react-native-sheet-transitions.patch
│ ├── react-native-track-player@4.1.1.patch
│ └── workbox-precaching.patch
├── plugins/
│ ├── eslint/
│ │ ├── eslint-check-i18n-json.js
│ │ ├── eslint-no-debug.js
│ │ ├── eslint-package-json.js
│ │ └── eslint-recursive-sort.js
│ └── utils.js
├── pnpm-workspace.yaml
├── scripts/
│ ├── copy-translation.ts
│ ├── increment-build-id.sh
│ ├── lib.ts
│ ├── mitproxy.py
│ ├── run-proxy.sh
│ ├── skip-main-app-vercel-build.sh
│ ├── svg-to-rn.ts
│ └── update-icon.ts
├── tsconfig.json
├── tsslint.config.ts
├── turbo.json
├── vercel.json
├── vitest.workspace.js
├── vitest.workspace.ts
└── wiki/
└── contribute-i18n.md
Showing preview only (400K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (3942 symbols across 1441 files)
FILE: .github/scripts/extract-release-info.mjs
constant RELEASE_PATTERNS (line 12) | const RELEASE_PATTERNS = {
constant EXIT_CODES (line 17) | const EXIT_CODES = {
function setGitHubEnv (line 29) | function setGitHubEnv(key, value) {
function setGitHubOutput (line 46) | function setGitHubOutput(key, value) {
function getLatestCommitMessage (line 62) | function getLatestCommitMessage() {
function extractReleaseInfo (line 76) | function extractReleaseInfo(commitMessage) {
function main (line 97) | function main() {
FILE: api/vercel_webhook.ts
function handler (line 6) | async function handler(request: VercelRequest, response: VercelResponse) {
function sha1 (line 45) | function sha1(data: Buffer, secret: string): string {
function purgeCloudflareCache (line 55) | async function purgeCloudflareCache() {
FILE: apps/cli/src/browser-login.ts
constant LOCAL_CALLBACK_HOST (line 8) | const LOCAL_CALLBACK_HOST = "127.0.0.1"
constant LOCAL_CALLBACK_PATH (line 9) | const LOCAL_CALLBACK_PATH = "/callback"
constant DEFAULT_TIMEOUT_MS (line 10) | const DEFAULT_TIMEOUT_MS = 3 * 60 * 1000
type BrowserLoginOptions (line 100) | interface BrowserLoginOptions {
type BrowserLoginResult (line 106) | interface BrowserLoginResult {
FILE: apps/cli/src/cli.e2e.test.ts
type CLIExecution (line 15) | type CLIExecution = {
FILE: apps/cli/src/client.ts
type GlobalOptions (line 27) | interface GlobalOptions {
type ResolvedGlobalOptions (line 34) | interface ResolvedGlobalOptions extends GlobalOptions {
type CommandContext (line 38) | interface CommandContext {
FILE: apps/cli/src/command.ts
type RunCommandOptions (line 7) | interface RunCommandOptions {
FILE: apps/cli/src/commands/auth.ts
type AuthLoginOptions (line 10) | interface AuthLoginOptions {
FILE: apps/cli/src/commands/collection.ts
type CollectionListOptions (line 7) | interface CollectionListOptions {
type CollectionAddOptions (line 12) | interface CollectionAddOptions {
FILE: apps/cli/src/commands/entry.ts
type MarkAllReadOptions (line 8) | interface MarkAllReadOptions {
FILE: apps/cli/src/commands/list.ts
type ListCreateOptions (line 7) | interface ListCreateOptions {
type ListUpdateOptions (line 15) | interface ListUpdateOptions {
type ListFeedOptions (line 23) | interface ListFeedOptions {
FILE: apps/cli/src/commands/opml.ts
type OpmlExportOptions (line 8) | interface OpmlExportOptions {
type OpmlImportOptions (line 12) | interface OpmlImportOptions {
FILE: apps/cli/src/commands/search.ts
type DiscoverTarget (line 6) | type DiscoverTarget = "feeds" | "lists"
type TrendingRange (line 7) | type TrendingRange = "1d" | "3d" | "7d" | "30d"
type TrendingLanguage (line 8) | type TrendingLanguage = "eng" | "cmn"
type SearchDiscoverOptions (line 31) | interface SearchDiscoverOptions {
type SearchRsshubOptions (line 35) | interface SearchRsshubOptions {
type SearchTrendingOptions (line 39) | interface SearchTrendingOptions {
FILE: apps/cli/src/commands/subscription.ts
type SubscriptionTarget (line 12) | type SubscriptionTarget = "feed" | "list" | "url"
type UpdateTarget (line 13) | type UpdateTarget = "feed" | "list"
type SubscriptionListOptions (line 29) | interface SubscriptionListOptions {
type SubscriptionAddOptions (line 34) | interface SubscriptionAddOptions {
type SubscriptionRemoveOptions (line 43) | interface SubscriptionRemoveOptions {
type SubscriptionUpdateOptions (line 47) | interface SubscriptionUpdateOptions {
FILE: apps/cli/src/commands/timeline.ts
type TimelineQuery (line 8) | type TimelineQuery = EntryListRequest & {
type TimelineOptions (line 12) | interface TimelineOptions {
FILE: apps/cli/src/commands/unread.ts
type UnreadListOptions (line 11) | interface UnreadListOptions {
FILE: apps/cli/src/config.ts
type FoloCLIConfig (line 6) | interface FoloCLIConfig {
FILE: apps/cli/src/output.ts
type OutputFormat (line 5) | type OutputFormat = "json" | "table" | "plain"
type OutputError (line 7) | interface OutputError {
class CLIError (line 12) | class CLIError extends Error {
method constructor (line 15) | constructor(code: string, message: string) {
FILE: apps/desktop/configs/vite.electron-render.config.ts
constant VITE_ROOT (line 13) | const VITE_ROOT = resolve(root, "layer/renderer")
FILE: apps/desktop/configs/vite.render.config.ts
method transform (line 53) | transform(code, id) {
FILE: apps/desktop/e2e/scripts/capture-ui-audit.ts
constant SETTING_TABS (line 17) | const SETTING_TABS = [
constant SUBVIEW_ROUTES (line 33) | const SUBVIEW_ROUTES = ["discover", "power", "action", "rsshub", "ai"] a...
function main (line 60) | async function main() {
FILE: apps/desktop/e2e/support/account.ts
type TestAccount (line 5) | interface TestAccount {
FILE: apps/desktop/e2e/support/app.ts
constant ONBOARDING_FEED_URL (line 8) | const ONBOARDING_FEED_URL = "folo://onboarding"
method get (line 58) | get(currentTarget, property, receiver) {
method set (line 65) | set(currentTarget, property, value, receiver) {
method ownKeys (line 72) | ownKeys(currentTarget) {
method getOwnPropertyDescriptor (line 75) | getOwnPropertyDescriptor(currentTarget, property) {
method get (line 92) | get() {
method set (line 95) | set() {}
FILE: apps/desktop/e2e/support/auth-bootstrap.ts
type AuthBootstrapResponse (line 9) | type AuthBootstrapResponse = {
type ParsedCookie (line 16) | type ParsedCookie = {
FILE: apps/desktop/e2e/support/env.ts
type DesktopE2EProfile (line 5) | type DesktopE2EProfile = "local" | "prod"
constant DESKTOP_E2E_PROFILES (line 7) | const DESKTOP_E2E_PROFILES = {
type DesktopE2EEnv (line 22) | interface DesktopE2EEnv {
FILE: apps/desktop/layer/main/preload/index.d.ts
type Window (line 4) | interface Window {
FILE: apps/desktop/layer/main/src/@types/constants.ts
type MainSupportedLanguages (line 3) | type MainSupportedLanguages = (typeof langs)[number]
FILE: apps/desktop/layer/main/src/@types/i18next.d.ts
type CustomTypeOptions (line 5) | interface CustomTypeOptions {
FILE: apps/desktop/layer/main/src/constants/app.ts
constant UNREAD_BACKGROUND_POLLING_INTERVAL (line 4) | const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5
constant HOTUPDATE_RENDER_ENTRY_DIR (line 6) | const HOTUPDATE_RENDER_ENTRY_DIR = path.resolve(app.getPath("userData"),...
constant GITHUB_OWNER (line 8) | const GITHUB_OWNER = process.env.GITHUB_OWNER || "RSSNext"
constant GITHUB_REPO (line 9) | const GITHUB_REPO = process.env.GITHUB_REPO || "follow"
constant START_IN_TRAY_ARGS (line 12) | const START_IN_TRAY_ARGS = "--start-in-tray"
constant BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN (line 14) | const BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN = "better-auth.session_token"
FILE: apps/desktop/layer/main/src/constants/system.ts
constant DEVICE_ID (line 3) | const DEVICE_ID = machineIdSync()
FILE: apps/desktop/layer/main/src/ipc/index.ts
type IpcServices (line 27) | type IpcServices = MergeIpcService<typeof services>
function initializeIpcServices (line 30) | function initializeIpcServices() {
FILE: apps/desktop/layer/main/src/ipc/services/app.ts
type WindowActionInput (line 24) | interface WindowActionInput {
type SearchInput (line 28) | interface SearchInput {
type Sender (line 33) | interface Sender extends Electron.WebContents {
class AppService (line 37) | class AppService extends IpcService {
method getAppVersion (line 41) | getAppVersion(): string {
method checkForUpdates (line 46) | async checkForUpdates(): Promise<{ hasUpdate: boolean; error?: string ...
method switchAppLocale (line 51) | switchAppLocale(context: IpcContext, input: string): void {
method rendererUpdateReload (line 60) | rendererUpdateReload(): void {
method openExternal (line 85) | async openExternal(_context: IpcContext, url: string): Promise<void> {
method windowAction (line 92) | windowAction(context: IpcContext, input: WindowActionInput): void {
method quitAndInstall (line 119) | quitAndInstall(_context: IpcContext): void {
method readClipboard (line 124) | readClipboard(_context: IpcContext): string {
method search (line 129) | async search(context: IpcContext, input: SearchInput): Promise<Electro...
method clearSearch (line 143) | clearSearch(context: IpcContext): void {
method download (line 148) | async download(context: IpcContext, input: string): Promise<void> {
method getAppPath (line 175) | getAppPath(_context: IpcContext): string {
method resolveAppAsarPath (line 180) | resolveAppAsarPath(context: IpcContext, input: string): string {
method readyToShowMainWindow (line 193) | readyToShowMainWindow(_context: IpcContext) {
method openCacheFolder (line 203) | openCacheFolder(_context: IpcContext): void {
method getCacheLimit (line 209) | getCacheLimit(_context: IpcContext): number {
method clearCache (line 214) | async clearCache(_context: IpcContext): Promise<void> {
method limitCacheSize (line 237) | limitCacheSize(_context: IpcContext, input: number): void {
method revealLogFile (line 246) | revealLogFile(_context: IpcContext) {
method getCacheSize (line 251) | getCacheSize(_context: IpcContext) {
FILE: apps/desktop/layer/main/src/ipc/services/auth.ts
class AuthService (line 14) | class AuthService extends IpcService {
method applySessionToken (line 17) | private async applySessionToken(token: string): Promise<void> {
method clearSessionToken (line 50) | private async clearSessionToken(): Promise<void> {
method requestCredentialAuth (line 65) | private async requestCredentialAuth(
method sessionChanged (line 109) | async sessionChanged(_context: IpcContext): Promise<void> {
method signOut (line 120) | async signOut(_context: IpcContext): Promise<void> {
method signOutRemote (line 130) | async signOutRemote(_context: IpcContext, token?: string): Promise<voi...
method signInWithCredential (line 147) | async signInWithCredential(
method signUpWithCredential (line 162) | async signUpWithCredential(
method setSessionToken (line 185) | async setSessionToken(_context: IpcContext, token: string): Promise<vo...
FILE: apps/desktop/layer/main/src/ipc/services/cli.ts
type CliInstallStatus (line 15) | interface CliInstallStatus {
class CliService (line 25) | class CliService extends IpcService {
method getInstallStatus (line 29) | async getInstallStatus(_context: IpcContext): Promise<CliInstallStatus> {
method installCli (line 48) | async installCli(_context: IpcContext): Promise<{ success: boolean; er...
method uninstallCli (line 70) | async uninstallCli(_context: IpcContext): Promise<{ success: boolean; ...
FILE: apps/desktop/layer/main/src/ipc/services/debug.ts
type InspectElementInput (line 4) | interface InspectElementInput {
class DebugService (line 9) | class DebugService extends IpcService {
method inspectElement (line 13) | inspectElement(context: IpcContext, input: InspectElementInput): void {
FILE: apps/desktop/layer/main/src/ipc/services/dock.ts
class PollingManager (line 8) | class PollingManager {
method startPolling (line 12) | async startPolling(pollingFn: () => Promise<void>, interval: number): ...
method stopPolling (line 37) | stopPolling(): void {
method active (line 43) | get active(): boolean {
method sleepWithAbortSignal (line 47) | private async sleepWithAbortSignal(ms: number, signal: AbortSignal): P...
class DockService (line 59) | class DockService extends IpcService {
method pollingUpdateUnreadCount (line 65) | async pollingUpdateUnreadCount(): Promise<void> {
method cancelPollingUpdateUnreadCount (line 73) | async cancelPollingUpdateUnreadCount(): Promise<void> {
method updateUnreadCount (line 78) | async updateUnreadCount(): Promise<void> {
method setDockBadge (line 85) | setDockBadge(_context: IpcContext, count: number): void {
FILE: apps/desktop/layer/main/src/ipc/services/integration.ts
constant INVALID_CHAR_REGEX (line 15) | const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$%&*+,:;<=>?[\]^`{|}\u007F]/g
constant DRIVE_LETTER_REGEX (line 16) | const DRIVE_LETTER_REGEX = /^[a-z]:/i
function sanitizeFileName (line 18) | function sanitizeFileName(name: string): string {
type SaveToEagleInput (line 28) | interface SaveToEagleInput {
type LoginToQBittorrentInput (line 33) | interface LoginToQBittorrentInput {
type CheckQBittorrentAuthInput (line 39) | interface CheckQBittorrentAuthInput {
type AddMagnetInput (line 43) | interface AddMagnetInput {
type CustomFetchInput (line 48) | interface CustomFetchInput {
class IntegrationService (line 56) | class IntegrationService extends IpcService {
method saveToObsidian (line 60) | async saveToObsidian(
method saveToEagle (line 104) | async saveToEagle(context: IpcContext, input: SaveToEagleInput): Promi...
method loginToQBittorrent (line 128) | async loginToQBittorrent(context: IpcContext, input: LoginToQBittorren...
method checkQBittorrentAuth (line 161) | async checkQBittorrentAuth(context: IpcContext, input: CheckQBittorren...
method addMagnet (line 181) | async addMagnet(context: IpcContext, input: AddMagnetInput) {
method customFetch (line 207) | async customFetch(context: IpcContext, input: CustomFetchInput) {
method openURLScheme (line 350) | async openURLScheme(context: IpcContext, scheme: string) {
FILE: apps/desktop/layer/main/src/ipc/services/menu.ts
type SerializableMenuItem (line 6) | type SerializableMenuItem = Omit<MenuItemConstructorOptions, "click" | "...
type ShowContextMenuInput (line 10) | interface ShowContextMenuInput {
type ShowConfirmDialogInput (line 14) | interface ShowConfirmDialogInput {
class MenuService (line 20) | class MenuService extends IpcService {
method normalizeMenuItems (line 23) | private normalizeMenuItems(
method showContextMenu (line 44) | async showContextMenu(context: IpcContext, input: ShowContextMenuInput...
method showConfirmDialog (line 56) | async showConfirmDialog(_context: IpcContext, input: ShowConfirmDialog...
method showShareMenu (line 67) | async showShareMenu(context: IpcContext, input: string): Promise<void> {
FILE: apps/desktop/layer/main/src/ipc/services/reader.ts
type ReadabilityInput (line 16) | interface ReadabilityInput {
type TtsInput (line 21) | interface TtsInput {
type DetectCodeStringLanguageInput (line 27) | interface DetectCodeStringLanguageInput {
class ReaderService (line 31) | class ReaderService extends IpcService {
method readability (line 35) | async readability(_context: IpcContext, input: ReadabilityInput) {
method tts (line 47) | async tts(context: IpcContext, input: TtsInput): Promise<string | null> {
method getVoices (line 87) | async getVoices(context: IpcContext) {
method detectCodeStringLanguage (line 104) | async detectCodeStringLanguage(
FILE: apps/desktop/layer/main/src/ipc/services/setting.ts
type SetLoginItemSettingsInput (line 15) | interface SetLoginItemSettingsInput {
class SettingService (line 22) | class SettingService extends IpcService {
method getLoginItemSettings (line 26) | getLoginItemSettings(_context: IpcContext): Electron.LoginItemSettings {
method setLoginItemSettings (line 31) | setLoginItemSettings(_context: IpcContext, input: SetLoginItemSettings...
method openSettingWindow (line 36) | openSettingWindow(_context: IpcContext): void {
method getSystemFonts (line 41) | async getSystemFonts(_context: IpcContext): Promise<string[]> {
method getAppearance (line 47) | getAppearance(_context: IpcContext): "light" | "dark" | "system" {
method setAppearance (line 52) | setAppearance(_context: IpcContext, appearance: "light" | "dark" | "sy...
method getMinimizeToTray (line 58) | getMinimizeToTray(_context: IpcContext): boolean {
method setMinimizeToTray (line 63) | setMinimizeToTray(_context: IpcContext, minimize: boolean): void {
method getProxyConfig (line 68) | getProxyConfig(_context: IpcContext) {
method setProxyConfig (line 74) | setProxyConfig(_context: IpcContext, config: string) {
method getMessagingToken (line 81) | getMessagingToken(_context: IpcContext): string | null {
FILE: apps/desktop/layer/main/src/lib/auth-cookie-migration.ts
constant LEGACY_PROD_API_URL (line 7) | const LEGACY_PROD_API_URL = "https://api.follow.is"
constant BETTER_AUTH_SESSION_DATA_COOKIE_NAME (line 8) | const BETTER_AUTH_SESSION_DATA_COOKIE_NAME = "better-auth.session_data"
FILE: apps/desktop/layer/main/src/lib/cli-session-sync.ts
constant CLI_NPM_PACKAGE_NAME (line 15) | const CLI_NPM_PACKAGE_NAME = "folocli"
constant CLI_NPX_PACKAGE_SPEC (line 16) | const CLI_NPX_PACKAGE_SPEC = `${CLI_NPM_PACKAGE_NAME}@latest`
constant CLI_CONFIG_DIR (line 17) | const CLI_CONFIG_DIR = join(homedir(), ".folo")
constant CLI_CONFIG_PATH (line 18) | const CLI_CONFIG_PATH = join(CLI_CONFIG_DIR, "config.json")
type CliConfig (line 21) | interface CliConfig {
FILE: apps/desktop/layer/main/src/lib/download.ts
type DownloadOptions (line 12) | interface DownloadOptions {
function downloadFile (line 20) | async function downloadFile(url: string, dest: string) {
function downloadFileWithProgress (line 33) | async function downloadFileWithProgress(options: DownloadOptions): Promi...
FILE: apps/desktop/layer/main/src/lib/proxy.ts
constant URL_SCHEME (line 37) | const URL_SCHEME = new Set(["http:", "https:", "ftp:", "socks:", "socks4...
constant BYPASS_RULES (line 59) | const BYPASS_RULES = ["<local>"].join(";")
FILE: apps/desktop/layer/main/src/lib/store.ts
type StoreData (line 5) | type StoreData = {
type StoreKey (line 23) | enum StoreKey {
FILE: apps/desktop/layer/main/src/lib/tray.ts
constant DEFAULT_MINIMIZE_TO_TRAY (line 116) | const DEFAULT_MINIMIZE_TO_TRAY = false
FILE: apps/desktop/layer/main/src/lib/utils.ts
function refreshBound (line 6) | function refreshBound(window: BrowserWindow, timeout = 0) {
FILE: apps/desktop/layer/main/src/logger.ts
function getLogFilePath (line 7) | function getLogFilePath() {
function revealLogFile (line 11) | async function revealLogFile() {
FILE: apps/desktop/layer/main/src/manager/app.ts
class AppManagerStatic (line 25) | class AppManagerStatic {
method getInstance (line 28) | public static getInstance(): AppManagerStatic {
method init (line 35) | public init() {
method onReady (line 40) | private onReady() {
method registerProtocols (line 65) | private registerProtocols() {
method setupAppVisuals (line 81) | private setupAppVisuals() {
method setupSystemConfigs (line 87) | private setupSystemConfigs() {
method runCronJobs (line 94) | private runCronJobs() {
method registerPushNotifications (line 99) | private async registerPushNotifications() {
method registerMenuAndContextMenu (line 177) | public registerMenuAndContextMenu() {
FILE: apps/desktop/layer/main/src/manager/bootstrap.ts
class BootstrapManager (line 31) | class BootstrapManager {
method start (line 32) | public static start() {
method registerAppEvents (line 44) | private static registerAppEvents() {
method installDevTools (line 166) | private static installDevTools() {
method handleOpen (line 188) | private static async handleOpen(url: string) {
FILE: apps/desktop/layer/main/src/manager/lifecycle.ts
class LifecycleManagerStatic (line 5) | class LifecycleManagerStatic {
method constructor (line 8) | private constructor() {
method getInstance (line 12) | public static getInstance(): LifecycleManagerStatic {
method registerListeners (line 19) | private registerListeners() {
method onWindowAllClosed (line 24) | private onWindowAllClosed() {
method onActivate (line 30) | private onActivate() {
method onReady (line 36) | public onReady(callback: () => void) {
FILE: apps/desktop/layer/main/src/manager/window.ts
class WindowManagerStatic (line 23) | class WindowManagerStatic {
method mainWindowDefaultSize (line 24) | static get mainWindowDefaultSize() {
method bindEvents (line 77) | private bindEvents(window: BrowserWindow) {
method setupDevToolsFont (line 181) | private setupDevToolsFont(window: BrowserWindow) {
method bindWindowStateEvents (line 218) | private bindWindowStateEvents(window: BrowserWindow) {
method bindMainWindowCloseHandlers (line 241) | private bindMainWindowCloseHandlers(window: BrowserWindow) {
method getPlatformSpecificWindowConfig (line 288) | private getPlatformSpecificWindowConfig(): Partial<BrowserWindowConstr...
method ensureWindowBoundsInScreen (line 379) | private ensureWindowBoundsInScreen(windowState?: {
FILE: apps/desktop/layer/main/src/menu.ts
method click (line 100) | click(_e, window) {
method click (line 214) | click() {
FILE: apps/desktop/layer/main/src/updater/follow-update-provider.ts
type FollowProviderOptions (line 19) | interface FollowProviderOptions {
type FollowProviderContext (line 23) | type FollowProviderContext = {
class FollowUpdateProvider (line 28) | class FollowUpdateProvider extends Provider<UpdateInfo> {
method setContext (line 31) | static setContext(context: FollowProviderContext) {
method clearContext (line 35) | static clearContext() {
method getContext (line 39) | static getContext() {
method constructor (line 43) | constructor(
method getLatestVersion (line 51) | async getLatestVersion(): Promise<UpdateInfo> {
method resolveFiles (line 56) | resolveFiles(updateInfo: UpdateInfo): Array<ResolvedUpdateFileInfo> {
method buildUpdateInfo (line 63) | private buildUpdateInfo(context: FollowProviderContext): UpdateInfo {
method mapFiles (line 104) | private mapFiles(files: PlatformUpdate["files"]): UpdateFileInfo[] {
method mapFile (line 112) | private mapFile(file: PlatformUpdateFile): UpdateFileInfo | null {
method safeParseUrl (line 130) | private safeParseUrl(value: string): URL | null {
method ensureContext (line 139) | private async ensureContext(): Promise<FollowProviderContext> {
method fetchContext (line 148) | private async fetchContext(): Promise<FollowProviderContext> {
method pickPlatform (line 170) | private pickPlatform(appDecision: AppUpdate): PlatformUpdate | null {
method resolvePlatformCandidates (line 185) | private resolvePlatformCandidates(): string[] {
FILE: apps/desktop/layer/main/src/updater/hot-updater.ts
type RendererManifest (line 21) | type RendererManifest = RendererUpdate & {
type RendererEligibilityStatus (line 26) | enum RendererEligibilityStatus {
type RendererEligibilityResult (line 33) | interface RendererEligibilityResult {
class RendererHotUpdater (line 39) | class RendererHotUpdater {
method extractManifest (line 44) | extractManifest(payload: LatestReleasePayload | null): RendererManifes...
method extractManifestFromRendererUpdate (line 55) | extractManifestFromRendererUpdate(renderer: RendererUpdate | null): Re...
method evaluateManifest (line 59) | evaluateManifest(manifest: RendererManifest | null): RendererEligibili...
method toManifest (line 113) | private toManifest(renderer: RendererUpdate | null): RendererManifest ...
method applyManifest (line 140) | async applyManifest(manifest: RendererManifest): Promise<void> {
method getCurrentManifest (line 183) | getCurrentManifest(): RendererManifest | null {
method cleanup (line 201) | async cleanup(): Promise<void> {
method loadDynamicEntry (line 229) | loadDynamicEntry() {
method downloadArchive (line 243) | private async downloadArchive(manifest: RendererManifest) {
method writeManifest (line 264) | private async writeManifest(manifest: RendererManifest) {
FILE: apps/desktop/layer/main/src/updater/index.ts
type UpdateCheckOptions (line 29) | type UpdateCheckOptions = {
type UpdateCheckResult (line 33) | type UpdateCheckResult = {
class FollowUpdater (line 38) | class FollowUpdater {
method constructor (line 45) | constructor(
method register (line 52) | register() {
method checkForUpdates (line 100) | async checkForUpdates(options: UpdateCheckOptions = {}): Promise<Updat...
method handleDirectAppDecision (line 129) | async handleDirectAppDecision(payload: LatestReleasePayload): Promise<...
method downloadAppUpdate (line 151) | async downloadAppUpdate(): Promise<void> {
method quitAndInstall (line 168) | quitAndInstall() {
method resolvePlatformCandidates (line 179) | private resolvePlatformCandidates() {
method pickPlatformUpdate (line 198) | private pickPlatformUpdate(
method handleAppDecision (line 219) | private async handleAppDecision(payload: LatestReleasePayload): Promis...
method handleDistributionAppDecision (line 265) | private async handleDistributionAppDecision(): Promise<UpdateCheckResu...
method tryDistributionRendererUpdate (line 302) | private async tryDistributionRendererUpdate(
method shouldPromptDistributionStoreUpdate (line 372) | private shouldPromptDistributionStoreUpdate(info: DistributionStatusPa...
method notifyDistributionUpdate (line 418) | private async notifyDistributionUpdate(
method handleRendererDecision (line 444) | private async handleRendererDecision(payload: LatestReleasePayload): P...
method registerAutoUpdaterEvents (line 505) | private registerAutoUpdaterEvents() {
FILE: apps/desktop/layer/main/src/updater/logger.ts
function logObject (line 17) | function logObject(logger: typeof updaterLogger, prefix: string, obj: Re...
FILE: apps/desktop/layer/main/src/updater/windows-updater.ts
class WindowsUpdater (line 5) | class WindowsUpdater extends NsisUpdater {
FILE: apps/desktop/layer/renderer/global.d.ts
type Window (line 4) | interface Window {
FILE: apps/desktop/layer/renderer/setup-file.ts
method navigator (line 10) | get navigator() {
FILE: apps/desktop/layer/renderer/src/@types/constants.ts
type RendererSupportedLanguages (line 4) | type RendererSupportedLanguages = (typeof langs)[number]
FILE: apps/desktop/layer/renderer/src/@types/i18next.d.ts
type CustomTypeOptions (line 5) | interface CustomTypeOptions {
FILE: apps/desktop/layer/renderer/src/App.tsx
function App (line 18) | function App() {
FILE: apps/desktop/layer/renderer/src/atoms/context-menu.ts
type ContextMenuState (line 14) | type ContextMenuState =
type FollowMenuItem (line 55) | type FollowMenuItem = MenuItemText | MenuItemSeparator
type MenuItemInput (line 57) | type MenuItemInput = MenuItemText | MenuItemSeparator | NilValue
function sortShortcutsString (line 59) | function sortShortcutsString(shortcut: string) {
function filterNullableMenuItems (line 74) | function filterNullableMenuItems(items: MenuItemInput[]): FollowMenuItem...
function transformMenuItemsForNative (line 94) | function transformMenuItemsForNative(nextItems: FollowMenuItem[]): Elect...
function withDebugMenu (line 115) | function withDebugMenu(menuItems: Array<FollowMenuItem>, e: MouseEvent |...
type MenuItemType (line 133) | enum MenuItemType {
class MenuItemSeparator (line 180) | class MenuItemSeparator {
method constructor (line 182) | constructor(public hide = false) {}
type BaseMenuItemTextConfig (line 187) | type BaseMenuItemTextConfig = {
class BaseMenuItemText (line 199) | class BaseMenuItemText {
method constructor (line 204) | constructor(private configs: BaseMenuItemTextConfig) {
method label (line 210) | public get label() {
method click (line 214) | public get click() {
method onClick (line 218) | public get onClick() {
method icon (line 221) | public get icon() {
method shortcut (line 225) | public get shortcut() {
method disabled (line 229) | public get disabled() {
method checked (line 233) | public get checked() {
method supportMultipleSelection (line 237) | public get supportMultipleSelection() {
method requiresLogin (line 241) | public get requiresLogin() {
type MenuItemTextConfig (line 246) | type MenuItemTextConfig = Prettify<
class MenuItemText (line 253) | class MenuItemText extends BaseMenuItemText {
method constructor (line 255) | constructor(protected config: MenuItemTextConfig) {
method submenu (line 261) | public get submenu() {
method hide (line 265) | public get hide() {
method extend (line 269) | extend(config: Partial<MenuItemTextConfig>) {
constant MENU_ITEM_SEPARATOR (line 276) | const MENU_ITEM_SEPARATOR = MenuItemSeparator.default
FILE: apps/desktop/layer/renderer/src/atoms/corner-player.ts
type CornerPlayerAtomValue (line 6) | type CornerPlayerAtomValue = {
FILE: apps/desktop/layer/renderer/src/atoms/network.ts
type NetworkStatus (line 5) | enum NetworkStatus {
FILE: apps/desktop/layer/renderer/src/atoms/player.ts
type PlayerAtomValue (line 10) | type PlayerAtomValue = {
method get (line 72) | get() {
method mount (line 75) | mount(v: Omit<PlayerAtomValue, "show" | "status" | "playedSeconds" | "du...
method teardown (line 129) | teardown() {
method play (line 133) | play() {
method pause (line 148) | pause() {
method togglePlayAndPause (line 163) | togglePlayAndPause() {
method close (line 182) | close() {
method seek (line 192) | seek(time: number) {
method setPlaybackRate (line 202) | setPlaybackRate(speed: number) {
method back (line 212) | back(time: number) {
method forward (line 218) | forward(time: number) {
method toggleMute (line 224) | toggleMute() {
method setVolume (line 231) | setVolume(volume: number) {
FILE: apps/desktop/layer/renderer/src/atoms/popover.ts
type PopoverProps (line 9) | interface PopoverProps extends Omit<PopoverContentProps, "children"> {
type PopoverState (line 16) | type PopoverState =
FILE: apps/desktop/layer/renderer/src/atoms/readability.ts
type ReadabilityStatus (line 10) | enum ReadabilityStatus {
FILE: apps/desktop/layer/renderer/src/atoms/server-configs.ts
type ServerConfigs (line 19) | type ServerConfigs = ExtractResponseData<GetStatusConfigsResponse>
type PaymentPlan (line 20) | type PaymentPlan = ServerConfigs["PAYMENT_PLAN_LIST"][number]
type PaymentFeature (line 21) | type PaymentFeature = PaymentPlan["limit"]
FILE: apps/desktop/layer/renderer/src/atoms/settings/ai.ts
type WebAISettings (line 17) | interface WebAISettings extends AISettings {
type ServerShortcutConfig (line 22) | type ServerShortcutConfig = ExtractResponseData<GetStatusConfigsResponse...
constant FALLBACK_SHORTCUT_ICON (line 24) | const FALLBACK_SHORTCUT_ICON = "i-mgc-hotkey-cute-re"
constant VALID_SHORTCUT_TARGETS (line 25) | const VALID_SHORTCUT_TARGETS = new Set<AIShortcutTarget>(DEFAULT_SHORTCU...
type AIChatPanelStyle (line 141) | enum AIChatPanelStyle {
type FloatingPanelState (line 153) | interface FloatingPanelState {
constant DEFAULT_FLOATING_PANEL_WIDTH (line 160) | const DEFAULT_FLOATING_PANEL_WIDTH = 500
constant DEFAULT_FLOATING_PANEL_HEIGHT (line 161) | const DEFAULT_FLOATING_PANEL_HEIGHT = clamp(window.innerHeight * 0.9, 60...
constant DEFAULT_FLOATING_PANEL_X (line 162) | const DEFAULT_FLOATING_PANEL_X = window.innerWidth - DEFAULT_FLOATING_PA...
constant DEFAULT_FLOATING_PANEL_Y (line 163) | const DEFAULT_FLOATING_PANEL_Y = window.innerHeight - DEFAULT_FLOATING_P...
FILE: apps/desktop/layer/renderer/src/atoms/settings/general.ts
constant DEFAULT_ACTION_LANGUAGE (line 10) | const DEFAULT_ACTION_LANGUAGE = "default"
function useActionLanguage (line 61) | function useActionLanguage() {
function getActionLanguage (line 69) | function getActionLanguage() {
function useHideAllReadSubscriptions (line 76) | function useHideAllReadSubscriptions() {
FILE: apps/desktop/layer/renderer/src/atoms/updater.ts
type UpdaterStatus (line 6) | type UpdaterStatus = "ready"
type UpdaterStatusKind (line 7) | type UpdaterStatusKind = "app" | "renderer" | "pwa" | "distribution"
type BaseUpdaterStatus (line 9) | type BaseUpdaterStatus<T extends UpdaterStatusKind> = {
type AppUpdaterStatus (line 15) | type AppUpdaterStatus = BaseUpdaterStatus<"app">
type RendererUpdaterStatus (line 17) | type RendererUpdaterStatus = BaseUpdaterStatus<"renderer">
type PwaUpdaterStatus (line 19) | type PwaUpdaterStatus = BaseUpdaterStatus<"pwa">
type DistributionUpdaterStatus (line 21) | type DistributionUpdaterStatus = BaseUpdaterStatus<"distribution"> & {
type UpdaterStatusAtom (line 28) | type UpdaterStatusAtom =
FILE: apps/desktop/layer/renderer/src/components/common/AppErrorBoundary.tsx
type AppErrorBoundaryProps (line 10) | interface AppErrorBoundaryProps extends PropsWithChildren {
type ErrorFallbackProps (line 38) | type ErrorFallbackProps = Parameters<FallbackRender>["0"]
type AppErrorFallbackProps (line 39) | type AppErrorFallbackProps = ErrorFallbackProps & {}
FILE: apps/desktop/layer/renderer/src/components/common/ErrorBoundary.tsx
type ErrorFallbackProps (line 6) | type ErrorFallbackProps = Omit<FallbackProps, "resetErrorBoundary"> &
type FallbackRender (line 10) | type FallbackRender = (props: ErrorFallbackProps) => ReactNode
type ErrorBoundaryProps (line 12) | interface ErrorBoundaryProps extends PropsWithChildren {
FILE: apps/desktop/layer/renderer/src/components/common/ErrorElement.tsx
function ErrorElement (line 14) | function ErrorElement() {
FILE: apps/desktop/layer/renderer/src/components/common/ErrorTooltip.tsx
function ErrorTooltip (line 10) | function ErrorTooltip({
FILE: apps/desktop/layer/renderer/src/components/common/ExPromise.tsx
constant NOT_RESOLVED (line 4) | const NOT_RESOLVED = Symbol("NOT_RESOLVED")
FILE: apps/desktop/layer/renderer/src/components/common/Focusable.tsx
type BizFocusableProps (line 6) | interface BizFocusableProps extends Omit<FocusableProps, "scope"> {
FILE: apps/desktop/layer/renderer/src/components/common/ImpressionTracker.tsx
type ImpressionProps (line 6) | type ImpressionProps<T extends AllTrackers> = {
function ImpressionView (line 14) | function ImpressionView<T extends keyof typeof tracker>(
function ImpressionViewImpl (line 24) | function ImpressionViewImpl<T extends keyof typeof tracker>(props: Impre...
FILE: apps/desktop/layer/renderer/src/components/common/LoadMoreIndicator.tsx
method onChange (line 9) | onChange(inView) {
FILE: apps/desktop/layer/renderer/src/components/common/Motion.tsx
type WithLCPOptimization (line 9) | type WithLCPOptimization<P> = P & { lcpOptimization?: boolean }
type MotionProxy (line 11) | type MotionProxy = {
method get (line 19) | get(target, p: string) {
FILE: apps/desktop/layer/renderer/src/components/common/NotFound.tsx
class AccessNotFoundError (line 13) | class AccessNotFoundError extends Error {
method constructor (line 14) | constructor(
method toString (line 23) | override toString() {
FILE: apps/desktop/layer/renderer/src/components/common/ReloadPrompt.tsx
function ReloadPrompt (line 9) | function ReloadPrompt() {
function registerPeriodicSync (line 45) | function registerPeriodicSync(period: number, swUrl: string, r: ServiceW...
FILE: apps/desktop/layer/renderer/src/components/common/ShadowDOM.tsx
function getLinkedStaticStyleSheets (line 146) | function getLinkedStaticStyleSheets() {
FILE: apps/desktop/layer/renderer/src/components/common/SharePanel.tsx
type SharePanelProps (line 11) | interface SharePanelProps {
type ShareOption (line 15) | interface ShareOption {
type SocialShareOption (line 24) | interface SocialShareOption {
FILE: apps/desktop/layer/renderer/src/components/common/withAppErrorBoundary.tsx
type WithErrorBoundaryOptions (line 7) | interface WithErrorBoundaryOptions {
function withAppErrorBoundary (line 18) | function withAppErrorBoundary<P extends object>(
FILE: apps/desktop/layer/renderer/src/components/errors/EntryNotFound.tsx
class EntryNotFound (line 47) | class EntryNotFound extends CustomSafeError {
method constructor (line 48) | constructor() {
FILE: apps/desktop/layer/renderer/src/components/errors/FeedNotFound.tsx
class FeedNotFound (line 46) | class FeedNotFound extends CustomSafeError {
method constructor (line 47) | constructor() {
FILE: apps/desktop/layer/renderer/src/components/errors/enum.ts
type ErrorComponentType (line 1) | enum ErrorComponentType {
FILE: apps/desktop/layer/renderer/src/components/ui/ai-summary-card/AISummaryCardBase.tsx
type AISummaryCardBaseProps (line 14) | interface AISummaryCardBaseProps {
FILE: apps/desktop/layer/renderer/src/components/ui/auto-completion/AutoCompletion.tsx
type Suggestion (line 11) | type Suggestion = {
type AutocompleteProps (line 15) | interface AutocompleteProps extends React.InputHTMLAttributes<HTMLInputE...
FILE: apps/desktop/layer/renderer/src/components/ui/background/WindowUnderBlur.tsx
type Props (line 8) | type Props<T extends ElementType = "div"> = {
FILE: apps/desktop/layer/renderer/src/components/ui/button/AnimatedCommandButton.tsx
type AnimatedCommandButtonProps (line 31) | interface AnimatedCommandButtonProps extends VariantProps<typeof animate...
FILE: apps/desktop/layer/renderer/src/components/ui/button/CommandActionButton.tsx
type CommandActionButtonProps (line 7) | interface CommandActionButtonProps extends ActionButtonProps {
FILE: apps/desktop/layer/renderer/src/components/ui/button/GlassButton.tsx
type GlassButtonProps (line 13) | interface GlassButtonProps {
FILE: apps/desktop/layer/renderer/src/components/ui/button/HeaderActionButton.tsx
type HeaderActionButtonProps (line 5) | interface HeaderActionButtonProps {
type HeaderActionGroupProps (line 84) | interface HeaderActionGroupProps {
FILE: apps/desktop/layer/renderer/src/components/ui/code-highlighter/shiki/Shiki.tsx
type ShikiProps (line 22) | interface ShikiProps {
function guessLanguage (line 59) | function guessLanguage() {
function loadShikiLanguage (line 82) | async function loadShikiLanguage(language: string, languageModule: any) {
function loadShikiTheme (line 88) | async function loadShikiTheme(theme: string, themeModule: any) {
function register (line 95) | async function register() {
FILE: apps/desktop/layer/renderer/src/components/ui/crop/AvatarUploadModal.tsx
type AvatarUploadModalProps (line 7) | interface AvatarUploadModalProps {
FILE: apps/desktop/layer/renderer/src/components/ui/fab/FABContainer.tsx
type FABConfig (line 16) | interface FABConfig {
FILE: apps/desktop/layer/renderer/src/components/ui/hover-preview/EntryPreviewCard.tsx
type EntryPreviewCardProps (line 16) | interface EntryPreviewCardProps {
FILE: apps/desktop/layer/renderer/src/components/ui/hover-preview/FeedPreviewCard.tsx
type FeedPreviewCardProps (line 14) | interface FeedPreviewCardProps {
FILE: apps/desktop/layer/renderer/src/components/ui/keyboard-recorder/KeyRecorder.tsx
type KeyRecorderProps (line 12) | interface KeyRecorderProps {
function FamiconsArrowUndoCircle (line 83) | function FamiconsArrowUndoCircle(props: SVGProps<SVGSVGElement>) {
constant MODIFIER_KEYS_MAP (line 101) | const MODIFIER_KEYS_MAP = {
constant MODIFIER_KEYS_SET (line 108) | const MODIFIER_KEYS_SET = new Set<string>(Object.values(MODIFIER_KEYS_MAP))
constant F_KEY_REGEX (line 110) | const F_KEY_REGEX = /^F(?:[1-9]|1[0-2])$/
FILE: apps/desktop/layer/renderer/src/components/ui/markdown/HTML.tsx
type HTMLProps (line 23) | type HTMLProps<A extends keyof JSX.IntrinsicElements = "div"> = {
constant HTML (line 98) | const HTML = memo(HTMLImpl)
FILE: apps/desktop/layer/renderer/src/components/ui/markdown/components/Toc.tsx
type ITocItem (line 21) | interface ITocItem {
type TocProps (line 29) | interface TocProps {
type TocRef (line 36) | interface TocRef {
type TocContainerProps (line 248) | interface TocContainerProps {
type TocHoverCardProps (line 256) | interface TocHoverCardProps extends TocContainerProps {}
FILE: apps/desktop/layer/renderer/src/components/ui/markdown/components/TocItem.tsx
type ITocItem (line 6) | interface ITocItem {
type TocItemProps (line 15) | interface TocItemProps {
FILE: apps/desktop/layer/renderer/src/components/ui/markdown/components/hooks.tsx
type DebouncedFuncLeading (line 64) | type DebouncedFuncLeading<T extends (..._args: any[]) => any> = T & {
FILE: apps/desktop/layer/renderer/src/components/ui/markdown/types.ts
type MarkdownImage (line 1) | type MarkdownImage = {
type MarkdownRenderActions (line 9) | interface MarkdownRenderActions {
FILE: apps/desktop/layer/renderer/src/components/ui/media/Media.tsx
type BaseProps (line 19) | type BaseProps = {
type MediaProps (line 31) | type MediaProps = BaseProps &
FILE: apps/desktop/layer/renderer/src/components/ui/media/MediaInfoRecord.tsx
type MediaInfoRecord (line 1) | type MediaInfoRecord = Record<string, { width?: number; height?: number }>
FILE: apps/desktop/layer/renderer/src/components/ui/media/PreviewMediaContent.tsx
constant GLASS_BUTTON_CLASS (line 207) | const GLASS_BUTTON_CLASS = tw`group-hover/left:opacity-100 opacity-0`
type PreviewMediaProps (line 259) | interface PreviewMediaProps extends EntryMedia {
FILE: apps/desktop/layer/renderer/src/components/ui/media/SwipeMedia.tsx
function SwipeMedia (line 16) | function SwipeMedia({
FILE: apps/desktop/layer/renderer/src/components/ui/media/VideoPlayer.tsx
type VideoPlayerProps (line 31) | type VideoPlayerProps = {
type VideoPlayerRef (line 37) | type VideoPlayerRef = {
type VideoPlayerContextValue (line 53) | interface VideoPlayerContextValue {
method onClick (line 102) | onClick(e) {
method onDoubleClick (line 107) | onDoubleClick(e) {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/helper/useAsyncModal.tsx
type AsyncModalOptions (line 13) | type AsyncModalOptions<T> = {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/inspire/PeekModal.tsx
type PeekModalProps (line 14) | interface PeekModalProps {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/AsyncModalContent.tsx
type UseAsyncFetcher (line 12) | interface UseAsyncFetcher<T> {
type AsyncModalContentProps (line 18) | interface AsyncModalContentProps<T> {
function useCountdown (line 24) | function useCountdown(durationInSeconds: number): boolean {
function AsyncModalContent (line 38) | function AsyncModalContent<T>({
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/bus.ts
type ModalDisposeEvent (line 8) | type ModalDisposeEvent = {
type ModalRePresentEvent (line 12) | type ModalRePresentEvent = {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/constants.ts
constant MODAL_STACK_Z_INDEX (line 27) | const MODAL_STACK_Z_INDEX = 1001
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/context.tsx
type CurrentModalContentProps (line 7) | type CurrentModalContentProps = ModalActionsInternal & {
type ModalContentComponent (line 36) | type ModalContentComponent<T = object> = FC<ModalActionsInternal & T>
type ModalActionsInternal (line 37) | type ModalActionsInternal = {
type Disposer (line 43) | type Disposer = () => void
type PresentModalContextInternalFn (line 44) | type PresentModalContextInternalFn = (props: ModalProps & { id?: string ...
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/custom-modal.tsx
type ModalTemplateType (line 29) | type ModalTemplateType = {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/declarative-modal.tsx
type DeclarativeModalProps (line 12) | interface DeclarativeModalProps extends Omit<ModalProps, "content"> {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/helper.tsx
function resizableOnly (line 10) | function resizableOnly(...positions: (keyof Enable)[]) {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/hooks.tsx
method getTopModalStack (line 32) | getTopModalStack() {
method getModalStackById (line 35) | getModalStackById(id: string) {
method dismiss (line 38) | dismiss(id: string) {
method dismissTop (line 43) | dismissTop() {
method dismissAll (line 49) | dismissAll() {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/internal/use-animate.ts
type ModalAnimateControls (line 10) | interface ModalAnimateControls {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/provider.tsx
type Window (line 17) | interface Window {
FILE: apps/desktop/layer/renderer/src/components/ui/modal/stacked/types.tsx
type ModalOverlayOptions (line 5) | interface ModalOverlayOptions {
type ModalProps (line 9) | interface ModalProps {
type DialogInstance (line 37) | interface DialogInstance {
FILE: apps/desktop/layer/renderer/src/components/ux/pull-to-refresh/index.tsx
type PullToRefreshProps (line 7) | interface PullToRefreshProps {
constant THRESHOLD (line 13) | const THRESHOLD = 80
constant MAX_PULL_DISTANCE (line 14) | const MAX_PULL_DISTANCE = 120
function PullToRefresh (line 16) | function PullToRefresh({
FILE: apps/desktop/layer/renderer/src/components/ux/transition/icon.tsx
type TransitionType (line 8) | type TransitionType = {
type IconTransitionProps (line 14) | type IconTransitionProps = {
FILE: apps/desktop/layer/renderer/src/constants/app.tsx
constant FEED_COLLECTION_LIST (line 4) | const FEED_COLLECTION_LIST = "collections"
constant QUERY_PERSIST_KEY (line 6) | const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")
constant I18N_LOCALE_KEY (line 7) | const I18N_LOCALE_KEY = getStorageNS("I18N_LOCALE")
constant ROUTE_VIEW_ALL (line 10) | const ROUTE_VIEW_ALL = "all"
constant ROUTE_FEED_PENDING (line 11) | const ROUTE_FEED_PENDING = "all"
constant ROUTE_ENTRY_PENDING (line 12) | const ROUTE_ENTRY_PENDING = "pending"
constant ROUTE_FEED_IN_FOLDER (line 13) | const ROUTE_FEED_IN_FOLDER = "folder-"
constant ROUTE_FEED_IN_LIST (line 14) | const ROUTE_FEED_IN_LIST = "list-"
constant ROUTE_FEED_IN_INBOX (line 15) | const ROUTE_FEED_IN_INBOX = "inbox-"
constant ROUTE_TIMELINE_OF_VIEW (line 16) | const ROUTE_TIMELINE_OF_VIEW = "view-"
FILE: apps/desktop/layer/renderer/src/constants/copy.ts
constant COPY_MAP (line 8) | const COPY_MAP = {
FILE: apps/desktop/layer/renderer/src/constants/dom.ts
constant ENTRY_CONTENT_RENDER_CONTAINER_ID (line 1) | const ENTRY_CONTENT_RENDER_CONTAINER_ID = "follow-entry-render"
constant LOGO_MOBILE_ID (line 3) | const LOGO_MOBILE_ID = "follow-logo-mobile"
constant ENTRY_COLUMN_LIST_SCROLLER_ID (line 5) | const ENTRY_COLUMN_LIST_SCROLLER_ID = "entry-column-scroller"
constant APP_GRID_CONTAINER_ID (line 7) | const APP_GRID_CONTAINER_ID = "follow-app-grid-container"
constant ROOT_CONTAINER_ID (line 9) | const ROOT_CONTAINER_ID = "follow-root-container"
FILE: apps/desktop/layer/renderer/src/constants/env.ts
constant WEB_URL (line 3) | const WEB_URL = env.VITE_WEB_URL
FILE: apps/desktop/layer/renderer/src/constants/hotkeys.ts
type HotkeyScope (line 1) | enum HotkeyScope {
FILE: apps/desktop/layer/renderer/src/errors/CustomSafeError.ts
class CustomSafeError (line 5) | class CustomSafeError extends Error {
method constructor (line 6) | constructor(message: string, toast?: boolean) {
FILE: apps/desktop/layer/renderer/src/global.d.ts
type Id (line 12) | type Id = string
type FeedId (line 13) | type FeedId = Id
type EntryId (line 14) | type EntryId = Id
type Window (line 21) | interface Window {
type I18nKeys (line 33) | type I18nKeys = OmitStringType<Parameters<typeof t>[0]>
type I18nKeysForSettings (line 34) | type I18nKeysForSettings = OmitStringType<Parameters<typeof settingsT>[0]>
type I18nKeysForShortcuts (line 35) | type I18nKeysForShortcuts = OmitStringType<Parameters<typeof shortcutsT>...
type I18nKeysForAi (line 36) | type I18nKeysForAi = OmitStringType<Parameters<typeof aiT>[0]>
FILE: apps/desktop/layer/renderer/src/hooks/biz/useAsRead.ts
function useEntryIsRead (line 8) | function useEntryIsRead(entryId?: string) {
FILE: apps/desktop/layer/renderer/src/hooks/biz/useDiscoverRSSHubRoute.tsx
type ResponseType (line 15) | type ResponseType = Awaited<ReturnType<ReturnType<typeof useDataFetcher>...
FILE: apps/desktop/layer/renderer/src/hooks/biz/useEntryActions.tsx
type EntryActionMenuItemConfig (line 71) | interface EntryActionMenuItemConfig {
class EntryActionMenuItem (line 83) | class EntryActionMenuItem extends MenuItemText {
method constructor (line 86) | constructor(config: EntryActionMenuItemConfig) {
method id (line 103) | public get id() {
method active (line 107) | public get active() {
method notice (line 111) | public get notice() {
method entryId (line 115) | public get entryId() {
method extend (line 119) | public override extend(config: Partial<EntryActionMenuItemConfig>) {
class EntryActionDropdownItem (line 127) | class EntryActionDropdownItem extends MenuItemText {
method constructor (line 131) | constructor(config: EntryActionMenuItemConfig & { children?: EntryActi...
method id (line 149) | public get id() {
method active (line 153) | public get active() {
method notice (line 157) | public get notice() {
method entryId (line 161) | public get entryId() {
method hasChildren (line 165) | public get hasChildren() {
method enabledChildren (line 169) | public get enabledChildren() {
method addChild (line 173) | public addChild(child: EntryActionMenuItem) {
method removeChild (line 177) | public removeChild(childId: string) {
method extend (line 181) | public override extend(
type EntryActionItem (line 191) | type EntryActionItem = EntryActionMenuItem | EntryActionDropdownItem | M...
constant HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU (line 220) | const HIDE_ACTIONS_IN_ENTRY_CONTEXT_MENU: FollowCommandId[] = [
constant HIDE_ACTIONS_IN_ENTRY_TOOLBAR_ACTIONS (line 234) | const HIDE_ACTIONS_IN_ENTRY_TOOLBAR_ACTIONS: FollowCommandId[] = [
FILE: apps/desktop/layer/renderer/src/hooks/biz/useEntryContextMenu.ts
function useEntryContextMenu (line 16) | function useEntryContextMenu({
FILE: apps/desktop/layer/renderer/src/hooks/biz/useFeature.ts
type DebugFeatureValue (line 6) | interface DebugFeatureValue {
type FeatureKey (line 12) | type FeatureKey = keyof typeof featureConfigMap
FILE: apps/desktop/layer/renderer/src/hooks/biz/useFeedActions.tsx
method click (line 148) | click() {
method click (line 176) | click() {
method click (line 199) | click() {
method click (line 213) | click() {
method onError (line 452) | async onError() {
method onError (line 472) | async onError() {
FILE: apps/desktop/layer/renderer/src/hooks/biz/useFollow.tsx
type FollowOptions (line 15) | interface FollowOptions {
FILE: apps/desktop/layer/renderer/src/hooks/biz/useNavigateEntry.ts
type NavigateEntryOptions (line 25) | type NavigateEntryOptions = Partial<{
type ParsedNavigateEntryOptions (line 52) | type ParsedNavigateEntryOptions = {
function getNavigateEntryPath (line 97) | function getNavigateEntryPath(options: NavigateEntryOptions | ParsedNavi...
FILE: apps/desktop/layer/renderer/src/hooks/biz/useRenderStyle.tsx
function useRenderStyle (line 5) | function useRenderStyle({
FILE: apps/desktop/layer/renderer/src/hooks/biz/useRouteParams.ts
type BizRouteParams (line 33) | interface BizRouteParams {
constant VIEW_SLUG_BY_VIEW (line 46) | const VIEW_SLUG_BY_VIEW: Record<FeedViewType, string> = {
constant VIEW_PARAM_ALIAS_MAP (line 56) | const VIEW_PARAM_ALIAS_MAP: Record<string, FeedViewType> = Object.entrie...
constant FEED_VIEW_VALUES (line 67) | const FEED_VIEW_VALUES = new Set<FeedViewType>(
function parseView (line 77) | function parseView(input: string | undefined): FeedViewType | undefined {
FILE: apps/desktop/layer/renderer/src/hooks/biz/useSubscriptionActions.tsx
method onMutate (line 74) | onMutate(variables) {
FILE: apps/desktop/layer/renderer/src/hooks/biz/useTimelineList.ts
constant ALL_TIMELINE_IDS (line 11) | const ALL_TIMELINE_IDS = getViewList({ includeAll: true }).map((view) =>
FILE: apps/desktop/layer/renderer/src/hooks/common/useBizQuery.ts
type SafeReturnType (line 9) | type SafeReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type CombinedObject (line 11) | type CombinedObject<T, U> = T & U
function useAuthQuery (line 12) | function useAuthQuery<
FILE: apps/desktop/layer/renderer/src/hooks/common/useContextMenu.tsx
type UseContextMenuOptions (line 3) | interface UseContextMenuOptions {
FILE: apps/desktop/layer/renderer/src/hooks/common/useI18n.ts
function useI18n (line 9) | function useI18n() {
FILE: apps/desktop/layer/renderer/src/hooks/common/usePreventOverscrollBounce.ts
constant PREVENT_SPRING_CLASS (line 3) | const PREVENT_SPRING_CLASS = "prevent-spring"
FILE: apps/desktop/layer/renderer/src/hooks/common/useRecaptchaToken.ts
type FoloE2EWindow (line 4) | type FoloE2EWindow = Window &
FILE: apps/desktop/layer/renderer/src/i18n.ts
class LocaleCache (line 20) | class LocaleCache {
method getKey (line 22) | private getKey(lang: string) {
method get (line 25) | get(lang: string) {
method set (line 31) | set(lang: string) {
type CustomEvent (line 97) | interface CustomEvent {
FILE: apps/desktop/layer/renderer/src/initialize/global-shortcuts.ts
type ShortcutDefinition (line 4) | interface ShortcutDefinition {
FILE: apps/desktop/layer/renderer/src/initialize/history.ts
type History (line 10) | interface History {
method get (line 23) | get() {
method set (line 26) | set(value) {
method get (line 55) | get() {
method get (line 83) | get() {
FILE: apps/desktop/layer/renderer/src/initialize/index.ts
type Window (line 24) | interface Window {
method onWindowClose (line 69) | onWindowClose() {
method onWindowShow (line 72) | onWindowShow() {
FILE: apps/desktop/layer/renderer/src/initialize/migrates/helper.ts
type DefineMigrationOptions (line 1) | interface DefineMigrationOptions {
FILE: apps/desktop/layer/renderer/src/initialize/migrates/index.ts
type Window (line 12) | interface Window {
FILE: apps/desktop/layer/renderer/src/initialize/migrates/v/v1.ts
function hasSettingsChanged (line 12) | function hasSettingsChanged(
FILE: apps/desktop/layer/renderer/src/lib/avatar-upload.ts
function uploadAvatarBlob (line 12) | async function uploadAvatarBlob(blob: Blob): Promise<string> {
FILE: apps/desktop/layer/renderer/src/lib/client-session.ts
constant CLIENT_ID_KEY (line 4) | const CLIENT_ID_KEY = getStorageNS("client_id")
constant SESSION_ID_KEY (line 5) | const SESSION_ID_KEY = getStorageNS("session_id")
constant AUTH_SESSION_TOKEN_KEY (line 6) | const AUTH_SESSION_TOKEN_KEY = getStorageNS("auth_session_token")
FILE: apps/desktop/layer/renderer/src/lib/defineQuery.ts
type ValidRecipeReturnDraftType (line 7) | type ValidRecipeReturnDraftType<T> = ReturnType<Producer<T>>
type DefinedQuery (line 9) | type DefinedQuery<TQueryKey extends QueryKey, TData> = Readonly<{
type DefinedQueryOptions (line 49) | type DefinedQueryOptions<TData> = {
function defineQuery (line 72) | function defineQuery<
FILE: apps/desktop/layer/renderer/src/lib/ga4.ts
class Analytics4 (line 7) | class Analytics4 {
method constructor (line 13) | constructor(clientID: string = getClientId(), sessionID = getSessionId...
method setUserId (line 18) | async setUserId(id: string) {
method setUserProperties (line 22) | async setUserProperties(upValue?: Record<string, unknown>) {
method logEvent (line 32) | async logEvent(eventName: string, params?: Record<string, unknown>): P...
FILE: apps/desktop/layer/renderer/src/lib/issues.ts
type IssueOptions (line 4) | interface IssueOptions {
FILE: apps/desktop/layer/renderer/src/lib/native-menu.ts
type ElectronMenuItem (line 5) | type ElectronMenuItem = Omit<MenuItemConstructorOptions, "click" | "subm...
function getMenuItemByPath (line 28) | function getMenuItemByPath(
function removeClick (line 48) | function removeClick(items: Array<ElectronMenuItem>): Array<ElectronMenu...
FILE: apps/desktop/layer/renderer/src/lib/observe-resize.ts
type ObserveResize (line 5) | type ObserveResize = {
FILE: apps/desktop/layer/renderer/src/lib/parse-html.ts
function markInlineImage (line 18) | function markInlineImage(node?: Element) {
function extractCodeFromHtml (line 240) | function extractCodeFromHtml(htmlString: string) {
FILE: apps/desktop/layer/renderer/src/lib/query-client.ts
constant DO_NOT_RETRY_CODES (line 11) | const DO_NOT_RETRY_CODES = new Set([400, 401, 403, 404, 422, 402])
method retry (line 18) | retry(failureCount, error) {
type Meta (line 42) | interface Meta {
type Register (line 46) | interface Register extends Meta {}
FILE: apps/desktop/layer/renderer/src/lib/simple-text-selection.ts
type SelectionRect (line 5) | interface SelectionRect {
type TextSelectionEvent (line 14) | interface TextSelectionEvent {
function addTextSelectionListener (line 23) | function addTextSelectionListener(
function normalizeRect (line 69) | function normalizeRect(rect: DOMRect | DOMRectReadOnly): SelectionRect {
FILE: apps/desktop/layer/renderer/src/lib/url-builder.ts
class WebUrlBuilder (line 6) | class WebUrlBuilder extends UrlBuilderClass {
method constructor (line 7) | constructor() {
method shareEntry (line 11) | shareEntry(
FILE: apps/desktop/layer/renderer/src/lib/utils.ts
function getEntriesParams (line 7) | function getEntriesParams({
FILE: apps/desktop/layer/renderer/src/modules/action/constants.tsx
function AiTargetLanguageSelector (line 29) | function AiTargetLanguageSelector() {
FILE: apps/desktop/layer/renderer/src/modules/action/rule-card.tsx
type RuleCardProps (line 18) | type RuleCardProps = {
FILE: apps/desktop/layer/renderer/src/modules/action/then-section.tsx
type ThenSectionProps (line 23) | type ThenSectionProps = {
FILE: apps/desktop/layer/renderer/src/modules/action/when-section.tsx
type WhenSectionProps (line 21) | type WhenSectionProps = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat-session/service.ts
constant MAX_PAGES (line 17) | const MAX_PAGES = 10
class AIChatSessionServiceStatic (line 22) | class AIChatSessionServiceStatic {
method syncSessionsAndMessagesFromServer (line 31) | async syncSessionsAndMessagesFromServer(filters?: ListSessionsQuery): ...
method loadSessionsFromDb (line 70) | async loadSessionsFromDb() {
method fetchAndPersistMessages (line 82) | async fetchAndPersistMessages(
method syncSessionMessages (line 129) | async syncSessionMessages(chatId: string) {
method fetchUnseenRemoteMessages (line 159) | private async fetchUnseenRemoteMessages(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat-session/store.ts
type AIChatSessionSyncStats (line 6) | interface AIChatSessionSyncStats {
type AIChatSessionViewModelState (line 12) | interface AIChatSessionViewModelState {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/atoms/session.ts
type AIModelState (line 12) | interface AIModelState {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/blocks/ContextBlock.tsx
type MainViewBlock (line 79) | type MainViewBlock = AbstractValueContextBlock<"mainView">
type MainFeedBlock (line 80) | type MainFeedBlock = AbstractValueContextBlock<"mainFeed">
type UnreadOnlyBlock (line 81) | type UnreadOnlyBlock = AbstractValueContextBlock<"unreadOnly">
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/context-bar/menus/ShortcutsMenuContent.tsx
type ShortcutsMenuContentProps (line 13) | interface ShortcutsMenuContentProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIChainOfThought.tsx
type ChainReasoningPart (line 15) | type ChainReasoningPart = ReasoningUIPart | ToolUIPart<BizUITools>
type AIChainOfThoughtProps (line 16) | interface AIChainOfThoughtProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/AIReasoningPart.tsx
type AIReasoningPartProps (line 8) | interface AIReasoningPartProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/share.tsx
type PartWithState (line 7) | interface PartWithState {
function withDisplayStateHandler (line 20) | function withDisplayStateHandler<T>(config: {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/AnalyticsMetrics.tsx
type AnalyticsMetric (line 3) | interface AnalyticsMetric {
type AnalyticsMetricsProps (line 8) | interface AnalyticsMetricsProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/CategoryTag.tsx
type CategoryTagProps (line 3) | interface CategoryTagProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/DisplayHeader.tsx
type DisplayHeaderProps (line 4) | interface DisplayHeaderProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/EmptyState.tsx
type EmptyStateProps (line 3) | interface EmptyStateProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/FeedItemCard.tsx
type FeedItemCardProps (line 11) | interface FeedItemCardProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/GroupedContent.tsx
type GroupedContentProps (line 3) | interface GroupedContentProps<T> {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/StatCard.tsx
type StatCardProps (line 9) | interface StatCardProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/file/GlobalFileDropZone.tsx
type GlobalFileDropZoneProps (line 9) | interface GlobalFileDropZoneProps extends PropsWithChildren {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatContextBar.tsx
constant MAX_VISIBLE_BLOCKS (line 18) | const MAX_VISIBLE_BLOCKS = 4
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatRoot.tsx
type AIChatRootProps (line 21) | interface AIChatRootProps extends PropsWithChildren {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIChatSendButton.tsx
type AIChatSendButtonProps (line 5) | interface AIChatSendButtonProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/AIModelIndicator.tsx
type AIModelIndicatorProps (line 19) | interface AIModelIndicatorProps {
type ProviderType (line 24) | type ProviderType = "openai" | "google" | "auto" | "deepseek" | "anthrop...
constant MODEL_PAID_LEVELS (line 35) | const MODEL_PAID_LEVELS = ["basic", "plus", "pro"] as const
type ModelPaidLevel (line 36) | type ModelPaidLevel = (typeof MODEL_PAID_LEVELS)[number]
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatBottomPanel.tsx
type ChatBottomPanelProps (line 14) | interface ChatBottomPanelProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatHistoryDropdown.tsx
type ChatHistoryDropdownProps (line 30) | interface ChatHistoryDropdownProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInput.tsx
type ChatInputProps (line 44) | interface ChatInputProps extends VariantProps<typeof chatInputVariants> {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatInterface.tsx
type ChatInterfaceProps (line 305) | interface ChatInterfaceProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatMessageContainer.tsx
constant SCROLL_BOTTOM_THRESHOLD (line 13) | const SCROLL_BOTTOM_THRESHOLD = 100
type ChatMessageContainerProps (line 15) | interface ChatMessageContainerProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatShortcutsRow.tsx
type ChatShortcutsRowProps (line 24) | interface ChatShortcutsRowProps {
type ShortcutMenuButtonProps (line 123) | interface ShortcutMenuButtonProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ChatTitle.tsx
type AIHeaderTitleProps (line 4) | interface AIHeaderTitleProps extends Omit<ButtonHTMLAttributes<HTMLButto...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/Messages.tsx
type MessagesProps (line 10) | interface MessagesProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/RateLimitNotice.tsx
type RateLimitNoticeProps (line 8) | interface RateLimitNoticeProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/ScrollToBottomButton.tsx
type ScrollToBottomButtonProps (line 3) | interface ScrollToBottomButtonProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/TaskReportDropdown.tsx
type TaskReportDropdownProps (line 28) | interface TaskReportDropdownProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/WelcomeScreen.tsx
type WelcomeScreenProps (line 13) | interface WelcomeScreenProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/shared/ChatSessionComponents.tsx
type SessionItemProps (line 11) | interface SessionItemProps {
type EmptyStateProps (line 19) | interface EmptyStateProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/shared/useChatSessionHandlers.tsx
type UseChatSessionHandlersProps (line 19) | interface UseChatSessionHandlersProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIChatMessage.tsx
type ChatMessage (line 17) | interface ChatMessage {
type AIChatMessageProps (line 24) | interface AIChatMessageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIDataBlockPart.tsx
type AIDataBlockPartProps (line 7) | interface AIDataBlockPartProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/AIMessageParts.tsx
type AIMessagePartsProps (line 20) | interface AIMessagePartsProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/BlockTitleComponents.tsx
type TitleProps (line 5) | interface TitleProps {
type EntryTitleProps (line 9) | interface EntryTitleProps extends TitleProps {
type FeedTitleProps (line 13) | interface FeedTitleProps extends TitleProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/EditableMessage.tsx
type EditableMessageProps (line 17) | interface EditableMessageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ErrorMessage.tsx
type ErrorMessageProps (line 9) | interface ErrorMessageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ImageThumbnail.tsx
type AttachmentImageProps (line 11) | type AttachmentImageProps = {
type ImageThumbnailProps (line 17) | type ImageThumbnailProps = AttachmentImageProps
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/TokenUsagePill.tsx
type TokenUsagePillProps (line 15) | interface TokenUsagePillProps {
type ModelInfoSectionProps (line 26) | interface ModelInfoSectionProps {
type FreeUserTokenUsageProps (line 65) | interface FreeUserTokenUsageProps {
type NormalUserTokenUsageProps (line 94) | interface NormalUserTokenUsageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ToolInvocationComponent.tsx
type ToolInvocationComponentProps (line 9) | interface ToolInvocationComponentProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserChatMessage.tsx
type UserChatMessageProps (line 18) | interface UserChatMessageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserMessageParts.tsx
type UserMessagePartsProps (line 8) | interface UserMessagePartsProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/UserRichTextMessage.tsx
function onError (line 16) | function onError(error: Error) {
function replaceShortcutTagsWithMarkdown (line 20) | function replaceShortcutTagsWithMarkdown(state: string): string {
type UserRichTextMessageProps (line 50) | interface UserRichTextMessageProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/ai-block-constants.ts
constant BLOCK_STYLES (line 12) | const BLOCK_STYLES = {
constant DEFAULT_BLOCK_STYLES (line 38) | const DEFAULT_BLOCK_STYLES = {
constant BLOCK_ICONS (line 47) | const BLOCK_ICONS = {
constant BLOCK_LABELS (line 57) | const BLOCK_LABELS = {
constant FILE_STATUS_LABELS (line 68) | const FILE_STATUS_LABELS = {
function getBlockStyles (line 78) | function getBlockStyles(type: AIChatContextBlock["type"]) {
function getBlockIcon (line 85) | function getBlockIcon(block: AIChatContextBlock): string {
function getBlockLabel (line 102) | function getBlockLabel(type: AIChatContextBlock["type"]): string {
function getImageUrl (line 109) | function getImageUrl(attachment: FileAttachment): string | null {
function isImageAttachment (line 116) | function isImageAttachment(block: AIChatContextBlock): boolean {
function getFileDisplayContent (line 127) | function getFileDisplayContent(attachment: FileAttachment): string {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/AnimatedMarkdown.tsx
type MarkdownAnimateTextProps (line 23) | interface MarkdownAnimateTextProps {
type BaseInlineFoloReferenceProps (line 185) | type BaseInlineFoloReferenceProps = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/TokenizedText.tsx
type TokenWithSource (line 10) | interface TokenWithSource {
type TokenType (line 18) | type TokenType = string | TokenWithSource | ReactElement
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/animated/constants.ts
constant DEFAULT_ANIMATION (line 1) | const DEFAULT_ANIMATION = "mask-left-to-right 0.5s ease-in-out"
constant ANIMATION_STYLE (line 2) | const ANIMATION_STYLE = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/message/useContextBlockPresentation.tsx
type ContextBlockPresentation (line 19) | interface ContextBlockPresentation {
function useContextBlockPresentation (line 28) | function useContextBlockPresentation(block: AIChatContextBlock): Context...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/shared/common-states.tsx
type LoadingStateProps (line 1) | interface LoadingStateProps {
type ErrorStateProps (line 5) | interface ErrorStateProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/ui/AIShortcutButton.tsx
type AIShortcutButtonProps (line 40) | interface AIShortcutButtonProps extends VariantProps<typeof aiShortcutBu...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/ui/ShortcutTooltip.tsx
type ShortcutTooltipProps (line 10) | interface ShortcutTooltipProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/ui/UploadProgress.tsx
type UploadProgressProps (line 6) | interface UploadProgressProps {
type CircularProgressProps (line 69) | interface CircularProgressProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/EntrySummaryCard.tsx
type EntrySummaryCardProps (line 10) | interface EntrySummaryCardProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/components/welcome/EntryWelcomeContent.tsx
type EntryWelcomeContentProps (line 5) | interface EntryWelcomeContentProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/constants/index.ts
constant SCROLLED_BEYOND_THRESHOLD (line 1) | const SCROLLED_BEYOND_THRESHOLD = 100
constant AI_CHAT_SPECIAL_ID_PREFIX (line 3) | const AI_CHAT_SPECIAL_ID_PREFIX = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/FileAttachmentNode.tsx
type SerializedFileAttachmentNode (line 19) | type SerializedFileAttachmentNode = Spread<
function convertFileAttachmentElement (line 26) | function convertFileAttachmentElement(domNode: Node): null | DOMConversi...
class FileAttachmentNode (line 36) | class FileAttachmentNode extends DecoratorNode<React.ReactElement> {
method getType (line 39) | static override getType(): string {
method clone (line 43) | static override clone(node: FileAttachmentNode): FileAttachmentNode {
method importJSON (line 47) | static override importJSON(serializedNode: SerializedFileAttachmentNod...
method importDOM (line 53) | static override importDOM(): DOMConversionMap | null {
method constructor (line 62) | constructor(attachmentId: string, key?: NodeKey) {
method exportJSON (line 67) | override exportJSON(): SerializedFileAttachmentNode {
method exportDOM (line 75) | override exportDOM(): DOMExportOutput {
method createDOM (line 82) | override createDOM(): HTMLElement {
method updateDOM (line 89) | override updateDOM(): false {
method getAttachmentId (line 93) | getAttachmentId(): string {
method setAttachmentId (line 97) | setAttachmentId(attachmentId: string): void {
method decorate (line 102) | override decorate(): React.ReactElement {
method isInline (line 106) | override isInline(): boolean {
type FileAttachmentComponentProps (line 111) | interface FileAttachmentComponentProps {
function FileAttachmentPill (line 115) | function FileAttachmentPill({ attachment }: { attachment: FileAttachment...
function MissingFilePill (line 141) | function MissingFilePill() {
function BlockBasedAttachment (line 150) | function BlockBasedAttachment({ attachmentId }: { attachmentId: string }) {
function MessageBasedAttachment (line 164) | function MessageBasedAttachment({
function FileAttachmentComponent (line 184) | function FileAttachmentComponent({ node }: FileAttachmentComponentProps) {
function $createFileAttachmentNode (line 195) | function $createFileAttachmentNode(attachmentId: string): FileAttachment...
function $isFileAttachmentNode (line 199) | function $isFileAttachmentNode(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/components/FileDropZone.tsx
type FileDropZoneProps (line 6) | interface FileDropZoneProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/hooks/useFileAttachmentBlockSync.ts
type FileAttachmentBlockReference (line 11) | interface FileAttachmentBlockReference {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/hooks/useFileUploadIntegration.ts
function useFileUploadIntegration (line 14) | function useFileUploadIntegration(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/types.ts
type FileUploadPluginConfig (line 1) | interface FileUploadPluginConfig {
type FileDropZoneState (line 12) | interface FileDropZoneState {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/utils/file-handling.ts
function dragEventHasFiles (line 4) | function dragEventHasFiles(event: DragEvent): boolean {
function clipboardEventHasFiles (line 11) | function clipboardEventHasFiles(event: ClipboardEvent): boolean {
function preventDefaultDrag (line 18) | function preventDefaultDrag(event: DragEvent): void {
function getFilesFromDrop (line 26) | function getFilesFromDrop(event: DragEvent): FileList | null {
function getFilesFromPaste (line 33) | function getFilesFromPaste(event: ClipboardEvent): FileList | null {
function createDragCounter (line 40) | function createDragCounter() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionNode.tsx
type SerializedMentionNode (line 19) | type SerializedMentionNode = Spread<
class MentionNode (line 26) | class MentionNode extends DecoratorNode<React.JSX.Element> {
method getType (line 29) | static override getType(): string {
method clone (line 33) | static override clone(node: MentionNode): MentionNode {
method constructor (line 37) | constructor(mentionData: MentionData, key?: NodeKey) {
method getMentionData (line 42) | getMentionData(): MentionData {
method setMentionData (line 46) | setMentionData(mentionData: MentionData): void {
method createDOM (line 51) | override createDOM(config: EditorConfig): HTMLElement {
method updateDOM (line 60) | override updateDOM(): false {
method importDOM (line 64) | static override importDOM(): DOMConversionMap | null {
method importJSON (line 72) | static override importJSON(serializedNode: SerializedMentionNode): Men...
method exportDOM (line 78) | override exportDOM(): DOMExportOutput {
method exportJSON (line 88) | override exportJSON(): SerializedMentionNode {
method getTextContent (line 99) | override getTextContent(): string {
method decorate (line 103) | override decorate(editor: LexicalEditor): React.JSX.Element {
method isInline (line 122) | override isInline(): boolean {
method isKeyboardSelectable (line 126) | override isKeyboardSelectable(): boolean {
method canInsertTextBefore (line 130) | canInsertTextBefore(): boolean {
method canInsertTextAfter (line 134) | canInsertTextAfter(): boolean {
method canBeEmpty (line 138) | canBeEmpty(): boolean {
method isSegmented (line 142) | isSegmented(): boolean {
method extractWithChild (line 146) | extractWithChild(): boolean {
function $createMentionNode (line 151) | function $createMentionNode(mentionData: MentionData): MentionNode {
function $isMentionNode (line 156) | function $isMentionNode(node: LexicalNode | null | undefined): node is M...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/MentionPlugin.tsx
function MentionPlugin (line 13) | function MentionPlugin() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionComponent.tsx
type MentionComponentProps (line 28) | interface MentionComponentProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/MentionDropdown.tsx
type MentionDropdownProps (line 13) | interface MentionDropdownProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/components/shared/MentionTypeIcon.tsx
type MentionTypeIconProps (line 6) | interface MentionTypeIconProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/constants.ts
constant MENTION_COMMAND (line 6) | const MENTION_COMMAND = createCommand<MentionData>("MENTION_COMMAND")
constant MENTION_TYPEAHEAD_COMMAND (line 7) | const MENTION_TYPEAHEAD_COMMAND = createCommand<string>("MENTION_TYPEAHE...
constant DEFAULT_MAX_SUGGESTIONS (line 11) | const DEFAULT_MAX_SUGGESTIONS = 10
constant MENTION_TRIGGER_PATTERN (line 15) | const MENTION_TRIGGER_PATTERN =
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionConfig.ts
constant MAX_INLINE_DATE_SUGGESTIONS (line 6) | const MAX_INLINE_DATE_SUGGESTIONS = 2
type DateRangeFactory (line 8) | type DateRangeFactory = (today: Dayjs) => DateRange
type RelativeDateDefinition (line 10) | interface RelativeDateDefinition {
constant RELATIVE_DATE_DEFINITIONS (line 17) | const RELATIVE_DATE_DEFINITIONS: readonly RelativeDateDefinition[] = [
type WeekdayPrefix (line 184) | type WeekdayPrefix = "auto" | "this" | "last"
type WeekdayTranslationDescriptor (line 186) | interface WeekdayTranslationDescriptor {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionParsers.ts
type AiTFunction (line 17) | type AiTFunction = TFunction<"ai">
type DateMentionBuilderContext (line 19) | interface DateMentionBuilderContext {
type RelativeDateCandidate (line 24) | interface RelativeDateCandidate {
constant FUSE_OPTIONS (line 30) | const FUSE_OPTIONS: IFuseOptions<RelativeDateCandidate> = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/dateMentionUtils.ts
type DateRange (line 11) | interface DateRange {
type LabelTranslator (line 42) | type LabelTranslator = TFunction<"ai", undefined>
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionKeyboard.ts
type UseMentionKeyboardOptions (line 6) | interface UseMentionKeyboardOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionSearch.ts
type UseMentionSearchOptions (line 6) | interface UseMentionSearchOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionSelection.ts
type UseMentionSelectionOptions (line 6) | interface UseMentionSelectionOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/hooks/useMentionTrigger.ts
type UseMentionTriggerOptions (line 7) | interface UseMentionTriggerOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/types.ts
type MentionLabelValue (line 3) | type MentionLabelValue = string | number | boolean | MentionLabelDescriptor
type MentionLabelDescriptor (line 5) | interface MentionLabelDescriptor {
type MentionBaseData (line 10) | interface MentionBaseData {
type EntryMentionData (line 17) | interface EntryMentionData extends MentionBaseData {
type FeedMentionData (line 22) | interface FeedMentionData extends MentionBaseData {
type DateMentionData (line 27) | interface DateMentionData extends MentionBaseData {
type CategoryMentionData (line 35) | interface CategoryMentionData extends MentionBaseData {
type ViewMentionData (line 40) | interface ViewMentionData extends MentionBaseData {
type MentionData (line 45) | type MentionData =
type MentionType (line 52) | type MentionType = MentionData["type"]
type MentionMatch (line 54) | interface MentionMatch {
type MentionDropdownPosition (line 60) | interface MentionDropdownPosition {
type MentionSearchState (line 65) | interface MentionSearchState {
type MentionTriggerState (line 71) | interface MentionTriggerState {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/mention/utils/mentionTextValue.ts
function getMentionTextValue (line 13) | function getMentionTextValue(mentionData: {
function getMentionDisplayTextValue (line 32) | function getMentionDisplayTextValue(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/selection/SelectedTextNode.tsx
type SelectedTextNodePayload (line 15) | type SelectedTextNodePayload = {
type SerializedSelectedTextNode (line 21) | type SerializedSelectedTextNode = Spread<SelectedTextNodePayload, Serial...
class SelectedTextNode (line 23) | class SelectedTextNode extends DecoratorNode<React.JSX.Element> {
method getType (line 28) | static override getType(): string {
method clone (line 32) | static override clone(node: SelectedTextNode): SelectedTextNode {
method constructor (line 36) | constructor(text: string, sourceEntryId?: string, timestamp?: number, ...
method getText (line 43) | getText(): string {
method setText (line 47) | setText(text: string): void {
method getSourceEntryId (line 52) | getSourceEntryId(): string | undefined {
method getTimestamp (line 56) | getTimestamp(): number | undefined {
method createDOM (line 60) | override createDOM(): HTMLElement {
method updateDOM (line 66) | override updateDOM(): false {
method importDOM (line 70) | static override importDOM(): DOMConversionMap | null {
method importJSON (line 74) | static override importJSON(serializedNode: SerializedSelectedTextNode)...
method exportJSON (line 79) | override exportJSON(): SerializedSelectedTextNode {
method exportDOM (line 89) | override exportDOM(): DOMExportOutput {
method decorate (line 96) | override decorate(_editor: LexicalEditor): React.JSX.Element {
method isInline (line 106) | override isInline(): boolean {
method isKeyboardSelectable (line 110) | override isKeyboardSelectable(): boolean {
method getTextContent (line 114) | override getTextContent(): string {
function $createSelectedTextNode (line 119) | function $createSelectedTextNode(payload: SelectedTextNodePayload): Sele...
function $isSelectedTextNode (line 123) | function $isSelectedTextNode(
function escapeXML (line 128) | function escapeXML(text: string): string {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/selection/SelectedTextNodeComponent.tsx
type SelectedTextNodeComponentProps (line 3) | interface SelectedTextNodeComponentProps {
function SelectedTextNodeComponent (line 9) | function SelectedTextNodeComponent({ text }: SelectedTextNodeComponentPr...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/selection/SelectedTextPlugin.tsx
function SelectedTextPlugin (line 8) | function SelectedTextPlugin() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/selection/insertSelectedTextNode.ts
function insertSelectedTextNode (line 13) | function insertSelectedTextNode(editor: LexicalEditor, payload: Selected...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/selection/selectedTextBridge.ts
type Listener (line 3) | type Listener = (payload: SelectedTextNodePayload) => void
function queueSelectedTextInsertion (line 8) | function queueSelectedTextInsertion(payload: SelectedTextNodePayload) {
function subscribeSelectedTextInsertion (line 18) | function subscribeSelectedTextInsertion(listener: Listener) {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shared/components/MentionLikePill.tsx
type MentionLikePillProps (line 4) | interface MentionLikePillProps extends React.HTMLAttributes<HTMLSpanElem...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shared/components/TypeaheadDropdown.tsx
type TypeaheadGroup (line 19) | interface TypeaheadGroup<TItem, TGroupKey = string> {
type TypeaheadDropdownProps (line 24) | interface TypeaheadDropdownProps<TItem, TGroupKey = string> {
function useOptionalLexicalEditor (line 51) | function useOptionalLexicalEditor() {
function TypeaheadDropdown (line 60) | function TypeaheadDropdown<TItem, TGroupKey = string>({
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shared/hooks/useListKeyboardNavigation.ts
type UseListKeyboardNavigationOptions (line 13) | interface UseListKeyboardNavigationOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shared/hooks/useTextTrigger.ts
type TriggerMatch (line 6) | interface TriggerMatch {
type UseTextTriggerOptions (line 12) | interface UseTextTriggerOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shared/hooks/useTypeaheadSelection.ts
type ReplaceHandlerResult (line 6) | interface ReplaceHandlerResult {
type UseTypeaheadSelectionOptions (line 11) | interface UseTypeaheadSelectionOptions<TMatch, TItem> {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/ShortcutNode.tsx
type SerializedShortcutNode (line 18) | type SerializedShortcutNode = Spread<
class ShortcutNode (line 25) | class ShortcutNode extends DecoratorNode<React.JSX.Element> {
method getType (line 28) | static override getType(): string {
method clone (line 32) | static override clone(node: ShortcutNode): ShortcutNode {
method constructor (line 36) | constructor(shortcutData: ShortcutData, key?: NodeKey) {
method getShortcutData (line 41) | getShortcutData(): ShortcutData {
method setShortcutData (line 45) | setShortcutData(shortcutData: ShortcutData): void {
method createDOM (line 50) | override createDOM(config: EditorConfig): HTMLElement {
method updateDOM (line 58) | override updateDOM(): false {
method importDOM (line 62) | static override importDOM(): DOMConversionMap | null {
method importJSON (line 70) | static override importJSON(serializedNode: SerializedShortcutNode): Sh...
method exportDOM (line 75) | override exportDOM(): DOMExportOutput {
method exportJSON (line 84) | override exportJSON(): SerializedShortcutNode {
method getTextContent (line 92) | override getTextContent(): string {
method decorate (line 96) | override decorate(_editor: LexicalEditor): React.JSX.Element {
method isInline (line 110) | override isInline(): boolean {
method isKeyboardSelectable (line 114) | override isKeyboardSelectable(): boolean {
method canInsertTextBefore (line 118) | canInsertTextBefore(): boolean {
method canInsertTextAfter (line 122) | canInsertTextAfter(): boolean {
method canBeEmpty (line 126) | canBeEmpty(): boolean {
method isSegmented (line 130) | isSegmented(): boolean {
method extractWithChild (line 134) | extractWithChild(): boolean {
function $createShortcutNode (line 139) | function $createShortcutNode(shortcutData: ShortcutData): ShortcutNode {
function $isShortcutNode (line 144) | function $isShortcutNode(node: LexicalNode | null | undefined): node is ...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/ShortcutPlugin.tsx
function ShortcutPlugin (line 13) | function ShortcutPlugin() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/components/ShortcutComponent.tsx
type ShortcutComponentProps (line 9) | interface ShortcutComponentProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/components/ShortcutDropdown.tsx
type ShortcutDropdownProps (line 10) | interface ShortcutDropdownProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/constants.ts
constant SHORTCUT_COMMAND (line 5) | const SHORTCUT_COMMAND = createCommand<ShortcutData>("SHORTCUT_COMMAND")
constant DEFAULT_MAX_SHORTCUT_SUGGESTIONS (line 7) | const DEFAULT_MAX_SHORTCUT_SUGGESTIONS = 10
constant SHORTCUT_TRIGGER_PATTERN (line 9) | const SHORTCUT_TRIGGER_PATTERN =
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/hooks/useShortcutKeyboard.ts
type UseShortcutKeyboardOptions (line 6) | interface UseShortcutKeyboardOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/hooks/useShortcutSearch.ts
type UseShortcutSearchOptions (line 7) | interface UseShortcutSearchOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/hooks/useShortcutSelection.ts
type UseShortcutSelectionOptions (line 6) | interface UseShortcutSelectionOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/hooks/useShortcutTrigger.ts
type UseShortcutTriggerOptions (line 7) | interface UseShortcutTriggerOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/types.ts
type ShortcutData (line 3) | interface ShortcutData {
type ShortcutMetadata (line 12) | interface ShortcutMetadata {
type ShortcutMatch (line 17) | interface ShortcutMatch {
type ShortcutSearchState (line 23) | interface ShortcutSearchState {
type ShortcutTriggerState (line 29) | interface ShortcutTriggerState {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/utils/shortcutTextValue.ts
function getShortcutTextValue (line 5) | function getShortcutTextValue(shortcutData: ShortcutData): string {
function getShortcutDisplayTextValue (line 14) | function getShortcutDisplayTextValue(shortcutData: ShortcutData): string {
function getShortcutMarkdownValue (line 20) | function getShortcutMarkdownValue(shortcutId: string): string {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoScroll.tsx
constant BOTTOM_THRESHOLD (line 5) | const BOTTOM_THRESHOLD = 50
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAutoTimelineSummaryShortcut.ts
constant ONE_HOUR (line 24) | const ONE_HOUR = 60 * 60 * 1000
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useDisplayBlocks.ts
type ValueBlockOf (line 5) | type ValueBlockOf<Type extends ValueContextBlock["type"]> = Omit<ValueCo...
type DisplayBlockItem (line 9) | type DisplayBlockItem =
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useFeedEntrySearchService.ts
type SearchItem (line 15) | interface SearchItem {
type SearchServiceOptions (line 24) | interface SearchServiceOptions {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useFileUpload.ts
type UseFileUploadOptions (line 10) | interface UseFileUploadOptions extends ProcessFileOptions {
type FileUploadHandlers (line 29) | interface FileUploadHandlers {
function useFileUpload (line 51) | function useFileUpload(
function useFileUploadWithDefaults (line 217) | function useFileUploadWithDefaults(): FileUploadHandlers {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useSendAIShortcut.ts
type ShortcutLike (line 25) | type ShortcutLike = ShortcutData | AIShortcut
type ShortcutResolver (line 27) | type ShortcutResolver =
type SendAIShortcutOptions (line 37) | type SendAIShortcutOptions = ShortcutResolver & {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useTimelineSummaryAutoContext.ts
type TimelineSummaryContextParams (line 5) | type TimelineSummaryContextParams = Pick<BizRouteParams, "entryId">
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/services/index.ts
class AIPersistServiceStatic (line 13) | class AIPersistServiceStatic {
method markSessionExists (line 18) | private markSessionExists(chatId: string, exists: boolean) {
method getSessionExistsFromCache (line 22) | private getSessionExistsFromCache(chatId: string): boolean | undefined {
method clearSessionCache (line 26) | private clearSessionCache(chatId?: string) {
method loadMessages (line 34) | async loadMessages(chatId: string): Promise<AiChatMessagesModel[]> {
method hasPersistedMessages (line 41) | async hasPersistedMessages(chatId: string): Promise<boolean> {
method hasAssistantMessagesMissingMetadata (line 52) | async hasAssistantMessagesMissingMetadata(chatId: string): Promise<boo...
method convertToUIMessage (line 70) | private convertToUIMessage(dbMessage: AiChatMessagesModel): BizUIMessa...
method loadUIMessages (line 89) | async loadUIMessages(chatId: string): Promise<BizUIMessage[]> {
method loadSessionWithMessages (line 98) | async loadSessionWithMessages(chatId: string): Promise<{
method replaceAllMessages (line 142) | async replaceAllMessages(chatId: string, messages: BizUIMessage[]) {
method upsertMessages (line 151) | async upsertMessages(chatId: string, messages: BizUIMessage[]) {
method deleteMessages (line 235) | async deleteMessages(chatId: string, messageIds: string[]) {
method resolveSessionTitle (line 245) | private resolveSessionTitle(
method getDefaultSessionTitle (line 258) | private getDefaultSessionTitle(
method formatDateTime (line 293) | private formatDateTime(date: Date, locale?: string): string {
method ensureSession (line 320) | async ensureSession(
method createSession (line 369) | async createSession(
method findTimelineSummarySession (line 385) | async findTimelineSummarySession(criteria: {
method getChatSession (line 408) | async getChatSession(chatId: string) {
method getChatSessions (line 422) | async getChatSessions(limit = 20) {
method deleteSession (line 472) | async deleteSession(chatId: string) {
method updateSessionTitle (line 482) | async updateSessionTitle(chatId: string, title: string) {
method updateSessionTime (line 492) | async updateSessionTime(chatId: string, date: Date = new Date()) {
method markSessionSynced (line 501) | async markSessionSynced(chatId: string) {
method cleanupEmptySessions (line 505) | async cleanupEmptySessions() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/AIChatContext.ts
type AIPanelRefs (line 9) | type AIPanelRefs = {
type AIRootStateContext (line 27) | type AIRootStateContext = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-actions.ts
class ChatSliceActions (line 15) | class ChatSliceActions {
method getActiveInstance (line 25) | static getActiveInstance(): ChatSliceActions | null {
method setActiveInstance (line 33) | static setActiveInstance(instance: ChatSliceActions | null) {
method constructor (line 38) | constructor(
method set (line 53) | get set() {
method get (line 57) | get get() {
method computeSyncStatus (line 61) | private computeSyncStatus(isLocal: boolean): "local" | "synced" {
method setSyncState (line 65) | private setSyncState(isLocal: boolean) {
method markSessionSynced (line 78) | async markSessionSynced() {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-instance.ts
class ZustandChat (line 9) | class ZustandChat extends AbstractChat<BizUIMessage> {
method constructor (line 13) | constructor(
method chatState (line 34) | get chatState() {
method destroy (line 39) | async destroy(): Promise<void> {
method setStatus (line 48) | protected override setStatus({ status, error }: { status: ChatStatus; ...
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/chat-state.ts
class ZustandChatState (line 13) | class ZustandChatState implements ChatState<BizUIMessage> {
method constructor (line 20) | constructor(
method #setupEventHandlers (line 32) | #setupEventHandlers(): void {
method status (line 103) | get status(): ChatStatus {
method status (line 107) | set status(newStatus: ChatStatus) {
method error (line 114) | get error(): Error | undefined {
method error (line 118) | set error(newError: Error | undefined) {
method messages (line 125) | get messages(): BizUIMessage[] {
method messages (line 129) | set messages(newMessages: BizUIMessage[]) {
method #fillMessageCreatedAt (line 181) | #fillMessageCreatedAt(message: SendingUIMessage | BizUIMessage): BizUI...
method destroy (line 201) | destroy(): void {
method setResumingStream (line 205) | setResumingStream(isResuming: boolean) {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/chat-core/types.ts
type ChatSlice (line 8) | interface ChatSlice {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/event-emitter.ts
class ChatStateEventEmitter (line 5) | class ChatStateEventEmitter {
method on (line 8) | on<T extends ChatStateEventType>(
method emit (line 23) | emit<T extends ChatStateEventType>(event: T, payload: ChatStateEvents<...
method clear (line 33) | clear(): void {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/types.ts
type ChatStateEvents (line 6) | interface ChatStateEvents<UI_MESSAGE extends BizUIMessage> {
type ChatStateEventType (line 12) | type ChatStateEventType = keyof ChatStateEvents<any>
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/block.slice.ts
type BlockSlice (line 9) | interface BlockSlice {
class BlockSliceAction (line 27) | class BlockSliceAction {
method constructor (line 28) | constructor(private params: Parameters<StateCreator<BlockSlice, [], []...
method set (line 38) | get set() {
method get (line 42) | get get() {
method addBlock (line 45) | addBlock(block: AIChatContextBlockInput) {
method removeBlock (line 63) | removeBlock(id: string) {
method toggleBlockDisabled (line 75) | toggleBlockDisabled(id: string, disabled?: boolean) {
method updateBlock (line 91) | updateBlock(id: string, updates: Partial<AIChatContextBlock>) {
method addOrUpdateBlock (line 110) | addOrUpdateBlock(block: AIChatContextBlock) {
method clearBlocks (line 119) | clearBlocks({ keepSpecialTypes = false }: { keepSpecialTypes?: boolean...
method resetContext (line 137) | resetContext() {
method getBlocks (line 151) | getBlocks() {
method addFileAttachment (line 156) | addFileAttachment(fileAttachment: FileAttachment) {
method updateFileAttachment (line 165) | updateFileAttachment(attachmentId: string, updatedAttachment: FileAtta...
method updateFileAttachmentStatus (line 178) | updateFileAttachmentStatus(
method removeFileAttachment (line 196) | removeFileAttachment(fileId: string) {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/store.ts
type AiChatStore (line 9) | type AiChatStore = BlockSlice &
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/transport.ts
type TitleHandlerPersistOption (line 9) | type TitleHandlerPersistOption = boolean | ((title: string) => void | Pr...
type TitleHandlerOptions (line 11) | interface TitleHandlerOptions {
type CreateChatTransportOptions (line 18) | interface CreateChatTransportOptions {
type CreateChatTitleHandlerOptions (line 23) | interface CreateChatTitleHandlerOptions {
function createChatTitleHandler (line 30) | function createChatTitleHandler(
function createChatTransport (line 47) | function createChatTransport({ onValue, titleHandler }: CreateChatTransp...
type UIMessageChunkParseResult (line 64) | type UIMessageChunkParseResult =
class ExtendChatTransport (line 91) | class ExtendChatTransport extends HttpChatTransport<BizUIMessage> {
method constructor (line 92) | constructor(
method processResponseStream (line 101) | protected processResponseStream(
method handleGeneratedTitle (line 125) | private async handleGeneratedTitle(chunk: UIMessageChunk) {
method reconnectToStream (line 163) | override reconnectToStream(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/store/types.ts
type FileAttachment (line 4) | interface FileAttachment {
type BaseContextBlock (line 18) | interface BaseContextBlock {
type ValueContextBlockType (line 23) | type ValueContextBlockType = "mainView" | "mainEntry" | "mainFeed" | "un...
type AbstractValueContextBlock (line 24) | interface AbstractValueContextBlock<T extends string> extends BaseContex...
type ValueContextBlock (line 29) | type ValueContextBlock = AbstractValueContextBlock<ValueContextBlockType>
type FileAttachmentContextBlock (line 31) | interface FileAttachmentContextBlock extends BaseContextBlock {
type AIChatContextBlock (line 36) | type AIChatContextBlock = ValueContextBlock | FileAttachmentContextBlock
type AIChatContextBlockInput (line 39) | type AIChatContextBlockInput =
type AIChatContextBlockType (line 43) | type AIChatContextBlockType = AIChatContextBlock["type"]
type AIChatStoreInitial (line 45) | interface AIChatStoreInitial {
type AIChatContextBlocks (line 53) | interface AIChatContextBlocks {
type AIDisplayFlowTool (line 57) | type AIDisplayFlowTool = ToolWithState<BizUITools["display_flow_chart"]>
type BizUIDataTypes (line 60) | type BizUIDataTypes = {
type BizUIMessage (line 67) | type BizUIMessage = UIMessage<BizUIMetadata, BizUIDataTypes, BizUITools>...
type BizUIMessagePart (line 71) | type BizUIMessagePart = UIMessagePart<BizUIDataTypes, BizUITools>
type SendingUIMessage (line 73) | type SendingUIMessage = Omit<BizUIMessage, "createdAt">
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts
type ChatSession (line 3) | interface ChatSession {
type RichTextPart (line 12) | type RichTextPart = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/error.ts
type ParsedErrorData (line 5) | interface ParsedErrorData {
type ParsedError (line 12) | interface ParsedError {
function parseAIError (line 25) | function parseAIError(error: Error | string | undefined): ParsedError {
function isRateLimitError (line 70) | function isRateLimitError(error: Error | string | undefined): boolean {
function getErrorMessage (line 79) | function getErrorMessage(error: ParsedError): string {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/extractor.ts
type AIMessageDataBlockPart (line 3) | type AIMessageDataBlockPart = {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/file-processing.ts
type ProcessFileOptions (line 7) | interface ProcessFileOptions {
type ProcessFileResult (line 15) | interface ProcessFileResult {
function processFile (line 21) | async function processFile(
type ProcessImageResult (line 78) | interface ProcessImageResult {
function processImage (line 83) | async function processImage(
function fileToDataUrl (line 144) | function fileToDataUrl(file: File): Promise<string> {
function cleanupFileAttachment (line 161) | function cleanupFileAttachment(fileAttachment: FileAttachment) {
function uploadFileAttachment (line 167) | async function uploadFileAttachment(
function processAndUploadFile (line 244) | async function processAndUploadFile(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/file-validation.ts
constant MAX_IMAGE_ALLOWED_SIZE (line 1) | const MAX_IMAGE_ALLOWED_SIZE = 3 * 1024 * 1024
constant MAX_DOCUMENT_ALLOWED_SIZE (line 2) | const MAX_DOCUMENT_ALLOWED_SIZE = 1 * 1024 * 1024
constant SUPPORTED_MIME_ACCEPT (line 3) | const SUPPORTED_MIME_ACCEPT = "image/*,.pdf,.txt,.md"
constant SUPPORTED_FILE_TYPES (line 4) | const SUPPORTED_FILE_TYPES = {
type SupportedFileType (line 18) | type SupportedFileType = keyof typeof SUPPORTED_FILE_TYPES
type FileCategory (line 19) | type FileCategory = (typeof SUPPORTED_FILE_TYPES)[SupportedFileType]["ca...
type FileValidationError (line 21) | interface FileValidationError {
type FileValidationResult (line 26) | interface FileValidationResult {
function validateFile (line 37) | function validateFile(file: File): FileValidationResult {
function formatFileSize (line 84) | function formatFileSize(bytes: number): string {
function getFileCategoryFromMimeType (line 94) | function getFileCategoryFromMimeType(mimeType: string): FileCategory {
function getFileIconName (line 114) | function getFileIconName(category: FileCategory): string {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/mentionDate.ts
constant MENTION_DATE_VALUE_FORMAT (line 3) | const MENTION_DATE_VALUE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ"
constant LEGACY_MENTION_DATE_VALUE_FORMAT (line 5) | const LEGACY_MENTION_DATE_VALUE_FORMAT = "YYYY-MM-DD"
type MentionDateDisplay (line 7) | interface MentionDateDisplay {
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/rate-limit.ts
type AIConfigLike (line 7) | interface AIConfigLike {
type RateLimitMessageOptions (line 12) | interface RateLimitMessageOptions {
function computeIsRateLimited (line 41) | function computeIsRateLimited(
function computeRateLimitMessage (line 60) | function computeRateLimitMessage(
FILE: apps/desktop/layer/renderer/src/modules/ai-chat/utils/shortcut.ts
type SerializedNodeWithChildren (line 6) | type SerializedNodeWithChildren = SerializedLexicalNode & {
FILE: apps/desktop/layer/renderer/src/modules/ai-onboarding/ai-chat-pane.tsx
constant SUGGESTION_KEYS (line 50) | const SUGGESTION_KEYS = [
constant SUGGESTION_SAMPLE_SIZE (line 67) | const SUGGESTION_SAMPLE_SIZE = 5
type SuggestionKey (line 69) | type SuggestionKey = (typeof SUGGESTION_KEYS)[number]
function pickSuggestionKeys (line 71) | function pickSuggestionKeys(previous?: readonly SuggestionKey[]): Sugges...
function AIChatPane (line 104) | function AIChatPane() {
function AIChatPaneImpl (line 112) | function AIChatPaneImpl() {
type WelcomeProps (line 173) | interface WelcomeProps {
function Welcome (line 177) | function Welcome({ onSuggestionClick }: WelcomeProps) {
function FinishListener (line 246) | function FinishListener() {
constant SCROLL_BOTTOM_THRESHOLD (line 261) | const SCROLL_BOTTOM_THRESHOLD = 100
type AIChatInterfaceProps (line 263) | interface AIChatInterfaceProps {
function AIChatInterface (line 267) | function AIChatInterface({ inputRef }: AIChatInterfaceProps) {
constant GRADIENT_COLORS (line 552) | const GRADIENT_COLORS = [
function gradientByIndex (line 575) | function gradientByIndex(index: number, isDark: boolean) {
FILE: apps/desktop/layer/renderer/src/modules/ai-onboarding/ai-onboarding-modal-content.tsx
function AiOnboardingModalContent (line 12) | function AiOnboardingModalContent({ onClose }: { onClose: () => void }) {
FILE: apps/desktop/layer/renderer/src/modules/ai-onboarding/feeds-selection-list.tsx
type FeedToSelect (line 25) | type FeedToSelect = Omit<FeedSelection, "selected">
function FeedsSelectionList (line 27) | function FeedsSelectionList() {
function FeedSelectionOperationScreen (line 50) | function FeedSelectionOperationScreen() {
function FeedSelectionItem (line 140) | function FeedSelectionItem({ feedAtom }: { feedAtom: PrimitiveAtom<FeedS...
function FeedSelectionFirstScreen (line 201) | function FeedSelectionFirstScreen() {
FILE: apps/desktop/layer/renderer/src/modules/ai-onboarding/store.ts
type FeedSelection (line 8) | type FeedSelection = {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/components/ai-item-actions.tsx
type ActionButton (line 5) | interface ActionButton {
type ItemActionsProps (line 13) | interface ItemActionsProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/components/ai-task-modal.tsx
type AITaskModalProps (line 33) | interface AITaskModalProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/components/notify-channels-config.tsx
type NotifyChannelsConfigProps (line 8) | interface NotifyChannelsConfigProps {
constant EMAIL_CHANNEL (line 14) | const EMAIL_CHANNEL = {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/components/schedule-config.tsx
type ScheduleConfigProps (line 18) | interface ScheduleConfigProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/components/task-list.tsx
type TaskListProps (line 8) | interface TaskListProps {
FILE: apps/desktop/layer/renderer/src/modules/ai-task/query.ts
constant MAX_AI_TASKS (line 13) | const MAX_AI_TASKS = 10
type OptimisticAITask (line 16) | type OptimisticAITask = WithOptimistic<AITask>
FILE: apps/desktop/layer/renderer/src/modules/ai-task/types.ts
constant MAX_PROMPT_LENGTH (line 4) | const MAX_PROMPT_LENGTH = 2000
type ScheduleType (line 28) | type ScheduleType = z.infer<typeof scheduleSchema>
type AITaskOptions (line 38) | type AITaskOptions = z.infer<typeof aiTaskOptionsSchema>
type TaskFormData (line 66) | type TaskFormData = z.infer<typeof taskSchema>
FILE: apps/desktop/layer/renderer/src/modules/app-layout/MainDestopLayout.tsx
function MainDestopLayout (line 139) | function MainDestopLayout() {
FILE: apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx
constant MIN_ENTRY_WIDTH (line 31) | const MIN_ENTRY_WIDTH = isSafari() ? 356 : 300
method onResizeStart (line 82) | onResizeStart({ position }) {
method onResizeEnd (line 85) | onResizeEnd({ position }) {
method onResizeStart (line 122) | onResizeStart({ position }) {
method onResizeEnd (line 125) | onResizeEnd({ position }) {
FILE: apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/MobileTimelineLayout.tsx
type MobileView (line 14) | type MobileView = "list" | "entry"
type MobileTimelineLayoutProps (line 16) | interface MobileTimelineLayoutProps {
FILE: apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFixedPanel.tsx
type AIChatFixedPanelProps (line 9) | interface AIChatFixedPanelProps extends React.DetailedHTMLProps<
FILE: apps/desktop/layer/renderer/src/modules/app-layout/ai/AIChatFloatingPanel.tsx
type AIChatFloatingPanelProps (line 20) | interface AIChatFloatingPanelProps extends React.DetailedHTMLProps<
FILE: apps/desktop/layer/renderer/src/modules/app-layout/subscription-column/SubscriptionColumn.tsx
method onResizeEnd (line 99) | onResizeEnd({ position }) {
FILE: apps/desktop/layer/renderer/src/modules/app-layout/subview/SubviewLayout.tsx
function SubviewLayout (line 53) | function SubviewLayout() {
function SubviewLayoutInner (line 74) | function SubviewLayoutInner() {
FILE: apps/desktop/layer/renderer/src/modules/app-layout/subview/hooks.ts
function useSubViewTitle (line 15) | function useSubViewTitle(title: I18nKeys | ReactNode, fallbackTitleStrin...
FILE: apps/desktop/layer/renderer/src/modules/app-tip/AICopilotMedia.tsx
type PreviewMessage (line 14) | type PreviewMessage = {
FILE: apps/desktop/layer/renderer/src/modules/app-tip/AppTipDialog.tsx
type AppTipDialogProps (line 12) | type AppTipDialogProps = {
function AppTipDialog (line 22) | function AppTipDialog({
FILE: apps/desktop/layer/renderer/src/modules/app-tip/AppTipMediaPreview.tsx
type AppTipMediaPreviewProps (line 8) | type AppTipMediaPreviewProps = {
function AppTipMediaPreview (line 12) | function AppTipMediaPreview({ media }: AppTipMediaPreviewProps) {
FILE: apps/desktop/layer/renderer/src/modules/app-tip/AppTipModalContent.tsx
type AppTipModalContentProps (line 21) | type AppTipModalContentProps = {
function AppTipModalContent (line 25) | function AppTipModalContent({ initialStep = 0 }: AppTipModalContentProps) {
FILE: apps/desktop/layer/renderer/src/modules/app-tip/constants.ts
constant APP_TIP_STORAGE_PREFIX (line 1) | const APP_TIP_STORAGE_PREFIX = "follow:ai-onboarding:dismissed"
constant APP_TIP_DEBUG_EVENT (line 3) | const APP_TIP_DEBUG_EVENT = "follow:ai-onboarding:debug-open"
constant APP_TIP_DISMISS_EVENT (line 5) | const APP_TIP_DISMISS_EVENT = "follow:ai-onboarding:dismiss-change"
FILE: apps/desktop/layer/renderer/src/modules/app-tip/types.ts
type AppTipDebugOpenEventDetail (line 1) | type AppTipDebugOpenEventDetail = {
type AppTipStepMedia (line 6) | type AppTipStepMedia = {
type AppTipStep (line 15) | type AppTipStep = {
FILE: apps/desktop/layer/renderer/src/modules/app-tip/useNewUserGuideState.ts
type AppTipDismissChangeDetail (line 9) | type AppTipDismissChangeDetail = {
function readDismissed (line 76) | function readDismissed(key: string | null) {
FILE: apps/desktop/layer/renderer/src/modules/auth/Form.tsx
type ElectronAuthResult (line 64) | type ElectronAuthResult = {
function LoginWithPassword (line 116) | function LoginWithPassword({
function RegisterForm (line 321) | function RegisterForm({
FILE: apps/desktop/layer/renderer/src/modules/auth/LoginModalContent.tsx
type LoginModalContentProps (line 23) | interface LoginModalContentProps {
FILE: apps/desktop/layer/renderer/src/modules/auth/TokenModal.tsx
function onSubmit (line 31) | async function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/command/command-button.tsx
type CommandButtonProps (line 7) | interface CommandButtonProps<T extends FollowCommand> extends ActionButt...
type CommandIdButtonProps (line 13) | interface CommandIdButtonProps<
FILE: apps/desktop/layer/renderer/src/modules/command/commands/entry-render.tsx
type EventBusMap (line 9) | interface EventBusMap {
type EntryScrollDownCommand (line 70) | type EntryScrollDownCommand = Command<{
type EntryScrollUpCommand (line 75) | type EntryScrollUpCommand = Command<{
type EntryNextEntryCommand (line 80) | type EntryNextEntryCommand = Command<{
type EntryPreviousEntryCommand (line 85) | type EntryPreviousEntryCommand = Command<{
type EntryRenderCommand (line 90) | type EntryRenderCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/entry.tsx
type StarCommand (line 370) | type StarCommand = Command<{
type DeleteCommand (line 375) | type DeleteCommand = Command<{
type CopyLinkCommand (line 380) | type CopyLinkCommand = Command<{
type ExportAsPDFCommand (line 385) | type ExportAsPDFCommand = Command<{
type CopyTitleCommand (line 390) | type CopyTitleCommand = Command<{
type OpenInBrowserCommand (line 395) | type OpenInBrowserCommand = Command<{
type ViewSourceContentCommand (line 400) | type ViewSourceContentCommand = Command<{
type ShareCommand (line 405) | type ShareCommand = Command<{
type ReadCommand (line 410) | type ReadCommand = Command<{
type ReadAboveCommand (line 415) | type ReadAboveCommand = Command<{
type ReadBelowCommand (line 420) | type ReadBelowCommand = Command<{
type ToggleAITranslationCommand (line 425) | type ToggleAITranslationCommand = Command<{
type ImageGalleryCommand (line 430) | type ImageGalleryCommand = Command<{
type TTSCommand (line 435) | type TTSCommand = Command<{
type ReadabilityCommand (line 440) | type ReadabilityCommand = Command<{
type EntryCommand (line 445) | type EntryCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/global.tsx
type EventBusMap (line 13) | interface EventBusMap {
type ShowShortcutsCommand (line 88) | type ShowShortcutsCommand = Command<{
type ToggleCornerPlayCommand (line 93) | type ToggleCornerPlayCommand = Command<{
type QuickAddCommand (line 98) | type QuickAddCommand = Command<{
type ToggleAIChatCommand (line 103) | type ToggleAIChatCommand = Command<{
type QuickSearchCommand (line 108) | type QuickSearchCommand = Command<{
type GlobalCommand (line 113) | type GlobalCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/id.ts
constant COMMAND_ID (line 1) | const COMMAND_ID = {
FILE: apps/desktop/layer/renderer/src/modules/command/commands/integration.tsx
function extractQBittorrentUrls (line 707) | function extractQBittorrentUrls(entry: EntryModel) {
type SaveToEagleCommand (line 845) | type SaveToEagleCommand = Command<{
type SaveToReadwiseCommand (line 850) | type SaveToReadwiseCommand = Command<{
type SaveToInstapaperCommand (line 855) | type SaveToInstapaperCommand = Command<{
type SaveToObsidianCommand (line 860) | type SaveToObsidianCommand = Command<{
type SaveToOutlineCommand (line 865) | type SaveToOutlineCommand = Command<{
type SaveToReadeckCommand (line 870) | type SaveToReadeckCommand = Command<{
type SaveToCuboxCommand (line 875) | type SaveToCuboxCommand = Command<{
type SaveToZoteroCommand (line 880) | type SaveToZoteroCommand = Command<{
type SaveToQBittorrentCommand (line 885) | type SaveToQBittorrentCommand = Command<{
type CustomIntegrationCommand (line 890) | type CustomIntegrationCommand = Command<{
type IntegrationCommand (line 895) | type IntegrationCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/layout.tsx
type FocusEvent (line 10) | interface FocusEvent {
type EventBusMap (line 14) | interface EventBusMap {
type FocusToSubscriptionCommand (line 63) | type FocusToSubscriptionCommand = Command<{
type ToggleTimelineColumnCommand (line 68) | type ToggleTimelineColumnCommand = Command<{
type FocusToTimelineCommand (line 73) | type FocusToTimelineCommand = Command<{
type FocusToEntryRenderCommand (line 77) | type FocusToEntryRenderCommand = Command<{
type LayoutCommand (line 82) | type LayoutCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/settings.tsx
method run (line 26) | run() {
method run (line 45) | run() {
method run (line 55) | run() {
method run (line 65) | run() {
type CustomizeToolbarCommand (line 72) | type CustomizeToolbarCommand = Command<{
type SettingsCommand (line 77) | type SettingsCommand = CustomizeToolbarCommand
FILE: apps/desktop/layer/renderer/src/modules/command/commands/subscription.tsx
type EventBusMap (line 12) | interface EventBusMap {
type SwitchTabToNextCommand (line 190) | type SwitchTabToNextCommand = Command<{
type SwitchTabToPreviousCommand (line 195) | type SwitchTabToPreviousCommand = Command<{
type SwitchTabToArticleCommand (line 200) | type SwitchTabToArticleCommand = Command<{
type SwitchTabToSocialCommand (line 205) | type SwitchTabToSocialCommand = Command<{
type SwitchTabToPictureCommand (line 210) | type SwitchTabToPictureCommand = Command<{
type SwitchTabToVideoCommand (line 215) | type SwitchTabToVideoCommand = Command<{
type SwitchTabToAudioCommand (line 220) | type SwitchTabToAudioCommand = Command<{
type SwitchTabToNotificationCommand (line 225) | type SwitchTabToNotificationCommand = Command<{
type NextSubscriptionCommand (line 230) | type NextSubscriptionCommand = Command<{
type PreviousSubscriptionCommand (line 235) | type PreviousSubscriptionCommand = Command<{
type ToggleFolderCollapseCommand (line 240) | type ToggleFolderCollapseCommand = Command<{
type MarkAllAsReadCommand (line 245) | type MarkAllAsReadCommand = Command<{
type OpenInBrowserCommand (line 250) | type OpenInBrowserCommand = Command<{
type OpenSiteInBrowserCommand (line 255) | type OpenSiteInBrowserCommand = Command<{
type SubscriptionCommand (line 260) | type SubscriptionCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/timeline.tsx
type EventBusMap (line 11) | interface EventBusMap {
type SwitchToNextTimelineCommand (line 71) | type SwitchToNextTimelineCommand = Command<{
type SwitchToPreviousTimelineCommand (line 76) | type SwitchToPreviousTimelineCommand = Command<{
type RefetchTimelineCommand (line 81) | type RefetchTimelineCommand = Command<{
type UnreadOnlyTimelineCommand (line 86) | type UnreadOnlyTimelineCommand = Command<{
type TimelineCommand (line 91) | type TimelineCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/commands/types.ts
type BasicCommand (line 12) | type BasicCommand =
FILE: apps/desktop/layer/renderer/src/modules/command/hooks/use-command-binding.ts
type ExtractSetType (line 105) | type ExtractSetType<T extends Set<unknown>> = T extends Set<infer U> ? U...
type AllowCustomizeCommandId (line 106) | type AllowCustomizeCommandId = ExtractSetType<typeof allowCustomizeComma...
type BindingCommandId (line 107) | type BindingCommandId = keyof typeof defaultCommandShortcuts
FILE: apps/desktop/layer/renderer/src/modules/command/hooks/use-command.ts
function useCommand (line 20) | function useCommand<T extends FollowCommandId>(id: T): FollowCommandMap[...
function useRunCommandFn (line 38) | function useRunCommandFn() {
FILE: apps/desktop/layer/renderer/src/modules/command/hooks/use-register-command.ts
type RegisterOptions (line 7) | type RegisterOptions = {
function useRegisterFollowCommand (line 62) | function useRegisterFollowCommand(
FILE: apps/desktop/layer/renderer/src/modules/command/hooks/use-register-hotkey.ts
type HotkeyOptions (line 9) | interface HotkeyOptions {
type RegisterHotkeyOptions (line 12) | interface RegisterHotkeyOptions<T extends FollowCommandId> {
constant SPECIAL_KEYS_MAPPINGS (line 21) | const SPECIAL_KEYS_MAPPINGS = {
FILE: apps/desktop/layer/renderer/src/modules/command/mutation-command-ids.ts
constant MUTATION_COMMAND_IDS (line 4) | const MUTATION_COMMAND_IDS = new Set<FollowCommandId>([
constant MUTATION_PREFIXES (line 24) | const MUTATION_PREFIXES = ["integration:custom:"]
FILE: apps/desktop/layer/renderer/src/modules/command/registry/command.ts
function createCommand (line 11) | function createCommand<
function createFollowCommand (line 36) | function createFollowCommand<T extends FollowCommand>(
function defineFollowCommand (line 42) | function defineFollowCommand<T extends FollowCommandId>(
FILE: apps/desktop/layer/renderer/src/modules/command/registry/registry.ts
method commands (line 12) | get commands() {
method register (line 28) | register(options: CommandOptions) {
method has (line 41) | has(id: string): boolean {
method get (line 45) | get(id: string): Command | undefined {
method getAll (line 53) | getAll(): Command[] {
method run (line 57) | run(id: string, ...args: unknown[]) {
function registerCommand (line 65) | function registerCommand(options: CommandOptions) {
FILE: apps/desktop/layer/renderer/src/modules/command/types.ts
type ExtractCategory (line 5) | type ExtractCategory<T extends string> = T extends `category.${string}` ...
type CommandCategory (line 6) | type CommandCategory = ExtractCategory<Parameters<typeof tShortcuts>[0]>
type KeybindingOptions (line 8) | interface KeybindingOptions {
type Command (line 16) | interface Command<
type SimpleCommand (line 32) | type SimpleCommand<T extends string> = Command<{ id: T; fn: () => void }>
type CommandOptions (line 34) | interface CommandOptions<
type FollowCommandMap (line 55) | type FollowCommandMap = {
type UnknownCommand (line 63) | type UnknownCommand = Command<{
type FollowCommandId (line 68) | type FollowCommandId = FollowCommand["id"]
type FollowCommand (line 69) | type FollowCommand = BasicCommand | UnknownCommand
FILE: apps/desktop/layer/renderer/src/modules/customize-toolbar/constant.ts
type ToolbarActionOrder (line 5) | interface ToolbarActionOrder {
constant ENTRY_ITEM_HIDE_IN_HEADER (line 10) | const ENTRY_ITEM_HIDE_IN_HEADER = new Set<UniqueIdentifier>([
constant MAIN_ACTIONS (line 16) | const MAIN_ACTIONS = [
constant MAIN_ACTIONS_SET (line 25) | const MAIN_ACTIONS_SET = new Set<UniqueIdentifier>(MAIN_ACTIONS)
constant DEFAULT_ACTION_ORDER (line 27) | const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
FILE: apps/desktop/layer/renderer/src/modules/customize-toolbar/dnd.tsx
function DroppableContainer (line 96) | function DroppableContainer({ children }: { children: ReactNode }) {
FILE: apps/desktop/layer/renderer/src/modules/debug/registry.ts
class Registry (line 1) | class Registry {
method add (line 4) | add(key: string, action: () => void) {
method remove (line 12) | remove(key: string) {
method getAll (line 16) | getAll() {
method get (line 20) | get(key: string) {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedCard.tsx
function FeedCardActions (line 20) | function FeedCardActions<T extends TrendingFeedItem | DiscoveryItem>({
type DiscoverFeedCardProps (line 84) | interface DiscoverFeedCardProps {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverFeedForm.tsx
type RouteParams (line 79) | type RouteParams = Record<
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverForm.tsx
constant FEED_DISCOVERY_INFO (line 36) | const FEED_DISCOVERY_INFO = {
function DiscoverForm (line 100) | function DiscoverForm({ type = "search" }: { type?: string }) {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx
function DiscoverImport (line 36) | function DiscoverImport() {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverInboxList.tsx
function DiscoverInboxList (line 10) | function DiscoverInboxList() {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverTransform.tsx
function DiscoverTransform (line 48) | function DiscoverTransform() {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoverUser.tsx
function DiscoverUser (line 8) | function DiscoverUser() {
FILE: apps/desktop/layer/renderer/src/modules/discover/DiscoveryContent.tsx
type Language (line 31) | type Language = "all" | "eng" | "cmn" | "fra"
type DiscoveryView (line 32) | type DiscoveryView = "trending" | "categories"
function DiscoveryContent (line 34) | function DiscoveryContent() {
FILE: apps/desktop/layer/renderer/src/modules/discover/FeedForm.tsx
type FeedFormDataValuesType (line 59) | type FeedFormDataValuesType = z.infer<typeof formSchema>
method onError (line 310) | onError(err) {
function onSubmit (line 315) | function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/discover/FeedSummary.tsx
type FeedSummaryProps (line 13) | interface FeedSummaryProps {
FILE: apps/desktop/layer/renderer/src/modules/discover/InboxForm.tsx
function onSubmit (line 110) | function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/discover/ListForm.tsx
type ListFormDataValuesType (line 50) | type ListFormDataValuesType = z.infer<typeof formSchema>
method onError (line 241) | async onError(err) {
function onSubmit (line 247) | function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/discover/OpmlAbstractGraphic.tsx
constant RSS_READERS (line 8) | const RSS_READERS = [
function OpmlAbstractGraphic (line 21) | function OpmlAbstractGraphic({ className }: { className?: string }) {
FILE: apps/desktop/layer/renderer/src/modules/discover/OpmlSelectionModal.tsx
method onError (line 84) | async onError(err) {
FILE: apps/desktop/layer/renderer/src/modules/discover/UnifiedDiscoverForm.tsx
function detectInputType (line 49) | function detectInputType(value: string): "rss" | "rsshub" | "search" {
type SearchFormData (line 75) | type SearchFormData = z.infer<typeof searchSchema>
type ToolLinkProps (line 78) | interface ToolLinkProps {
function ToolLink (line 84) | function ToolLink({ icon, label, onClick }: ToolLinkProps) {
function UnifiedDiscoverForm (line 101) | function UnifiedDiscoverForm() {
FILE: apps/desktop/layer/renderer/src/modules/discover/recommendations.tsx
function Recommendations (line 6) | function Recommendations() {
FILE: apps/desktop/layer/renderer/src/modules/discover/types.ts
type ParsedFeedItem (line 1) | type ParsedFeedItem = {
FILE: apps/desktop/layer/renderer/src/modules/download/index.tsx
function DownloadPage (line 8) | function DownloadPage() {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/all-item.tsx
function AllItem (line 70) | function AllItem({ entryId, translation, currentFeedTitle }: UniversalIt...
function AllItemStateLess (line 210) | function AllItemStateLess({ entry, feed }: EntryItemStatelessProps) {
function AudioIcon (line 247) | function AudioIcon({ entryId, src }: { entryId: string; src: string }) {
function VideoIcon (line 289) | function VideoIcon({ src }: { src: string }) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/article-item.tsx
function ArticleItem (line 13) | function ArticleItem({ entryId, translation }: UniversalItemProps) {
function ArticleItemStateLess (line 19) | function ArticleItemStateLess({ entry, feed }: EntryItemStatelessProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/audio-item.tsx
function AudioItem (line 8) | function AudioItem({ entryId, translation }: UniversalItemProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/notification-item.tsx
function NotificationItem (line 11) | function NotificationItem({ entryId, translation }: UniversalItemProps) {
function NotificationItemStateLess (line 17) | function NotificationItemStateLess({ entry, feed }: EntryItemStatelessPr...
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item-stateless.tsx
function PictureItemStateLess (line 14) | function PictureItemStateLess({ entry }: EntryItemStatelessProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-item.tsx
function PictureItem (line 23) | function PictureItem({ entryId, translation }: UniversalItemProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/picture-masonry.tsx
function scrollOutViewMarkRead (line 156) | function scrollOutViewMarkRead(entries: IntersectionObserverEntry[]) {
function renderInViewMarkRead (line 187) | function renderInViewMarkRead(entries: IntersectionObserverEntry[]) {
type MasonryProps (line 305) | interface MasonryProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/social-media-item.tsx
function SocialMediaItemStateLess (line 159) | function SocialMediaItemStateLess({ entry, feed }: EntryItemStatelessPro...
FILE: apps/desktop/layer/renderer/src/modules/entry-column/Items/video-item.tsx
function VideoItem (line 21) | function VideoItem({ entryId, translation }: UniversalItemProps) {
function VideoItemStateLess (line 134) | function VideoItemStateLess({ entry, feed }: EntryItemStatelessProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/DateItem.tsx
type DateItemInnerProps (line 15) | interface DateItemInnerProps {
type DateItemProps (line 22) | type DateItemProps = Pick<DateItemInnerProps, "isSticky"> & {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/FooterMarkItem.tsx
type FooterMarkItemProps (line 28) | interface FooterMarkItemProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/VirtualRowItem.tsx
type VirtualRowItemProps (line 12) | interface VirtualRowItemProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/ai-timeline-loading/AITimelineLoadingOverlay.tsx
type Props (line 7) | type Props = {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/entry-column-wrapper/types.tsx
type EntryColumnWrapperProps (line 1) | interface EntryColumnWrapperProps extends ComponentType {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/components/mark-all-button.tsx
type MarkAllButtonProps (line 24) | interface MarkAllButtonProps {
method onAutoClose (line 68) | onAutoClose() {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/context/EntriesContext.tsx
type EntriesStateContextValue (line 8) | type EntriesStateContextValue = {
type EntriesActionsContextValue (line 21) | type EntriesActionsContextValue = {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useAttachScrollBeyond.tsx
constant DEFAULT_THRESHOLD (line 6) | const DEFAULT_THRESHOLD = 30
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntriesByView.ts
function getEntryIdsFromMultiplePlace (line 112) | function getEntryIdsFromMultiplePlace(...entryIds: Array<string[] | unde...
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntryMarkReadHandler.tsx
function batchMarkRead (line 47) | function batchMarkRead(ids: string[]) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useEntryVirtualization.ts
type UseEntryVirtualizationOptions (line 9) | interface UseEntryVirtualizationOptions {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useLocalEntries.ts
type UseLocalEntriesOptions (line 19) | interface UseLocalEntriesOptions {
function getEntryIdsFromMultiplePlace (line 28) | function getEntryIdsFromMultiplePlace(...entryIds: Array<string[] | unde...
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useMarkAll.ts
type MarkAllFilter (line 7) | type MarkAllFilter =
FILE: apps/desktop/layer/renderer/src/modules/entry-column/hooks/useWheelGestureClose.ts
type UseWheelGestureCloseOptions (line 6) | interface UseWheelGestureCloseOptions {
type UseWheelGestureCloseReturn (line 13) | interface UseWheelGestureCloseReturn {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/index.tsx
function EntryColumnContent (line 38) | function EntryColumnContent() {
function EntryColumnImpl (line 206) | function EntryColumnImpl() {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/item.tsx
type EntryItemProps (line 14) | interface EntryItemProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/list.tsx
type EntryListProps (line 49) | type EntryListProps = {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/store/EntryColumnContext.ts
type EntryRootStateContext (line 4) | type EntryRootStateContext = {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx
type GridItemProps (line 22) | interface GridItemProps extends UniversalItemProps {
function GridItem (line 26) | function GridItem(props: GridItemProps) {
FILE: apps/desktop/layer/renderer/src/modules/entry-column/templates/list-item-template.tsx
function ListItem (line 55) | function ListItem({
function AudioCover (line 291) | function AudioCover({
FILE: apps/desktop/layer/renderer/src/modules/entry-column/types.ts
type UniversalItemProps (line 6) | type UniversalItemProps = {
type EntryListItemFC (line 12) | type EntryListItemFC<P extends object = object> = FC<P & UniversalItemPr...
type EntryItemStatelessProps (line 16) | type EntryItemStatelessProps = {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/AISummary.tsx
function AISummary (line 16) | function AISummary({ entryId }: { entryId: string }) {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/EntryAttachments.tsx
constant SUPPORTED_MIME_TYPES (line 10) | const SUPPORTED_MIME_TYPES = new Set(["application/x-bittorrent"])
function EntryAttachments (line 12) | function EntryAttachments({ entryId }: { entryId: string }) {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTitle.tsx
type EntryLinkProps (line 23) | interface EntryLinkProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/WarnGoToExternalLink.tsx
function open (line 85) | function open() {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContentFallback.tsx
type EntryContentFallbackProps (line 8) | interface EntryContentFallbackProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/accessories/index.tsx
type EntryContentAccessoriesRef (line 7) | type EntryContentAccessoriesRef = {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/types.tsx
type EntryContentProps (line 2) | interface EntryContentProps {
type EntryContentClassNames (line 8) | interface EntryContentClassNames {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx
function EntryHeaderImpl (line 9) | function EntryHeaderImpl({ entryId, className, compact }: EntryHeaderPro...
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/EntryHeader.tsx
function EntryHeaderImpl (line 9) | function EntryHeaderImpl({ entryId, className, compact }: EntryHeaderPro...
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx
function EntryHeaderActionsContainerImpl (line 10) | function EntryHeaderActionsContainerImpl({ isSmallWidth }: { isSmallWidt...
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderBreadcrumb.tsx
function ViewSubscriptionsDropdown (line 39) | function ViewSubscriptionsDropdown({
function FeedEntriesDropdown (line 151) | function FeedEntriesDropdown({
function EntryHeaderBreadcrumb (line 223) | function EntryHeaderBreadcrumb() {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderMeta.tsx
function EntryHeaderMetaImpl (line 6) | function EntryHeaderMetaImpl() {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderReadHistory.tsx
function EntryHeaderReadHistoryImpl (line 12) | function EntryHeaderReadHistoryImpl({ className }: { className?: string ...
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/context.tsx
type EntryHeaderContextValue (line 12) | interface EntryHeaderContextValue {
function useEntryHeaderContext (line 18) | function useEntryHeaderContext() {
type EntryHeaderRootProps (line 24) | interface EntryHeaderRootProps extends EntryHeaderProps {
function EntryHeaderRootImpl (line 29) | function EntryHeaderRootImpl({
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/types.tsx
type EntryHeaderProps (line 1) | interface EntryHeaderProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/factory.ts
type EntryLayoutComponent (line 9) | type EntryLayoutComponent = FC<EntryLayoutProps>
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/AudioPlayer.tsx
type AudioPlayerProps (line 11) | interface AudioPlayerProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/AuthorHeader.tsx
type AuthorHeaderProps (line 11) | interface AuthorHeaderProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/ContentBody.tsx
type ContentBodyProps (line 8) | interface ContentBodyProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/MediaTranscript.tsx
constant MAX_PARAGRAPH_LENGTH (line 7) | const MAX_PARAGRAPH_LENGTH = 300
type SubtitleItem (line 9) | interface SubtitleItem {
type MediaTranscriptProps (line 18) | interface MediaTranscriptProps {
function srtTimeToSeconds (line 32) | function srtTimeToSeconds(timeString: string): number {
function formatTimeString (line 53) | function formatTimeString(seconds: number): string {
function processEnglishContent (line 64) | function processEnglishContent(allText: string): string[] {
function calculateEnglishTiming (line 119) | function calculateEnglishTiming(
function parseSrt (line 243) | function parseSrt(srtText: string): SubtitleItem[] {
function formatTime (line 326) | function formatTime(timeString: string): string {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/TranscriptToggle.tsx
type TranscriptToggleProps (line 3) | interface TranscriptToggleProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/VideoPlayer.tsx
type VideoPlayerProps (line 24) | interface VideoPlayerProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/types.ts
type EntryLayoutProps (line 4) | interface EntryLayoutProps {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/selection/GlassButton.tsx
type GlassButtonProps (line 5) | interface GlassButtonProps extends HTMLMotionProps<"button"> {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/selection/SharePosterModal.tsx
type SharePosterModalProps (line 16) | type SharePosterModalProps = {
type Mode (line 21) | type Mode = "light" | "dark"
function SharePosterModal (line 23) | function SharePosterModal({ selectedText, entryId }: SharePosterModalPro...
function loadImage (line 475) | function loadImage(url: string): Promise<HTMLImageElement | null> {
function wrapText (line 485) | function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth:...
FILE: apps/desktop/layer/renderer/src/modules/entry-content/components/selection/TextSelectionToolbar.tsx
type TextSelectionToolbarProps (line 29) | type TextSelectionToolbarProps = {
constant DEFAULT_DIMENSIONS (line 36) | const DEFAULT_DIMENSIONS = {
constant VIEWPORT_PADDING (line 41) | const VIEWPORT_PADDING = 12
function TextSelectionToolbar (line 43) | function TextSelectionToolbar({
type ToolbarButtonProps (line 181) | type ToolbarButtonProps = {
function ToolbarButton (line 188) | function ToolbarButton({ iconClassName, label, onClick, active }: Toolba...
function getViewport (line 209) | function getViewport() {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/constants/navigation-hints.ts
constant NAVIGATION_HINTS_CONSTANTS (line 4) | const NAVIGATION_HINTS_CONSTANTS = {
constant NAVIGATION_HINTS_TEXT (line 27) | const NAVIGATION_HINTS_TEXT = {
constant NAVIGATION_HINTS_ICONS (line 35) | const NAVIGATION_HINTS_ICONS = {
FILE: apps/desktop/layer/renderer/src/modules/entry-content/hooks/useEntryNavigationHints.ts
type UseEntryNavigationHintsOptions (line 8) | interface UseEntryNavigationHintsOptions {
type UseEntryNavigationHintsReturn (line 17) | interface UseEntryNavigationHintsReturn {
FILE: apps/desktop/layer/renderer/src/modules/feed/feed-icon.tsx
type GetIconPropsProps (line 24) | type GetIconPropsProps = {
function getIconProps (line 34) | function getIconProps(props: GetIconPropsProps) {
type IconTarget (line 171) | type IconTarget = {
type FeedIconEntry (line 184) | type FeedIconEntry = { authorAvatar?: string | null; firstPhotoUrl?: str...
function FeedIcon (line 191) | function FeedIcon({
FILE: apps/desktop/layer/renderer/src/modules/feed/feed-summary.tsx
function FollowSummary (line 19) | function FollowSummary({
FILE: apps/desktop/layer/renderer/src/modules/integration/CustomIntegrationPreview.tsx
type CustomIntegrationPreviewProps (line 9) | interface CustomIntegrationPreviewProps {
FILE: apps/desktop/layer/renderer/src/modules/integration/CustomIntegrationValidator.tsx
type CustomIntegrationValidatorProps (line 7) | interface CustomIntegrationValidatorProps {
FILE: apps/desktop/layer/renderer/src/modules/integration/PlaceholderHelp.tsx
type PlaceholderHelpProps (line 8) | interface PlaceholderHelpProps {
FILE: apps/desktop/layer/renderer/src/modules/integration/URLSchemePreview.tsx
type URLSchemePreviewProps (line 9) | interface URLSchemePreviewProps {
FILE: apps/desktop/layer/renderer/src/modules/integration/custom-integration-manager.ts
type PlaceholderContext (line 21) | interface PlaceholderContext {
class CustomIntegrationManager (line 36) | class CustomIntegrationManager {
method getEntryContentAsMarkdown (line 40) | private static async getEntryContentAsMarkdown(entry: EntryModel): Pro...
method getDescription (line 61) | private static getDescription(entry: EntryModel): string {
method buildPlaceholderContext (line 75) | static async buildPlaceholderContext(entry: EntryModel): Promise<Place...
method getAvailablePlaceholders (line 93) | static getAvailablePlaceholders(): Array<{ key: string; description: s...
method replacePlaceholders (line 113) | static replacePlaceholders(
method processFetchTemplate (line 167) | static async processFetchTemplate(
method executeIntegration (line 206) | static async executeIntegration(
method executeWithToast (line 311) | static async executeWithToast(integration: CustomIntegration, entry: E...
method validateFetchTemplate (line 328) | static validateFetchTemplate(fetchTemplate: FetchTemplate): { valid: b...
method validateURLSchemeTemplate (line 363) | static validateURLSchemeTemplate(template: URLSchemeTemplate): {
method validateCustomIntegration (line 396) | static validateCustomIntegration(integration: Partial<CustomIntegratio...
method getTemplatePreview (line 436) | static async getTemplatePreview(
method getURLSchemePreview (line 463) | static getURLSchemePreview(
FILE: apps/desktop/layer/renderer/src/modules/integration/fetch-adapter.ts
type FetchRequestOptions (line 11) | interface FetchRequestOptions {
type FetchResponse (line 21) | interface FetchResponse {
class BrowserFetchAdapter (line 40) | class BrowserFetchAdapter extends BaseFetchAdapter {
method fetch (line 41) | async fetch(url: string, options?: FetchRequestOptions): Promise<Fetch...
class ElectronFetchAdapter (line 81) | class ElectronFetchAdapter extends BaseFetchAdapter {
method fetch (line 82) | async fetch(url: string, options?: FetchRequestOptions): Promise<Fetch...
class FetchAdapterManager (line 115) | class FetchAdapterManager {
method constructor (line 120) | private constructor() {
method getInstance (line 133) | static getInstance(): FetchAdapterManager {
method preferElectronFetch (line 143) | preferElectronFetch() {
method preferClientFetch (line 150) | preferClientFetch() {
method createAdapter (line 158) | private createAdapter(): BaseFetchAdapter {
method fetch (line 171) | async fetch(url: string, options?: FetchRequestOptions): Promise<Fetch...
FILE: apps/desktop/layer/renderer/src/modules/integration/url-scheme-handler.ts
class URLSchemeHandler (line 6) | class URLSchemeHandler {
method getInstance (line 9) | static getInstance(): URLSchemeHandler {
method replacePlaceholders (line 19) | private replacePlaceholders(template: string, data: Record<string, str...
method executeURLScheme (line 35) | async executeURLScheme(
method openURLScheme (line 69) | private async openURLScheme(scheme: string): Promise<void> {
method canExecuteURLScheme (line 86) | canExecuteURLScheme(): boolean {
method getExamples (line 95) | static getExamples(): { name: string; scheme: string; description: str...
FILE: apps/desktop/layer/renderer/src/modules/new-user-guide/ai-chat-pane.tsx
constant SUGGESTION_KEYS (line 45) | const SUGGESTION_KEYS = [
constant SUGGESTION_SAMPLE_SIZE (line 62) | const SUGGESTION_SAMPLE_SIZE = 5
type SuggestionKey (line 64) | type SuggestionKey = (typeof SUGGESTION_KEYS)[number]
function pickSuggestionKeys (line 66) | function pickSuggestionKeys(previous?: readonly SuggestionKey[]): Sugges...
function AIChatPane (line 99) | function AIChatPane() {
function AIChatPaneImpl (line 107) | function AIChatPaneImpl() {
type WelcomeProps (line 165) | interface WelcomeProps {
function Welcome (line 169) | function Welcome({ onSuggestionClick }: WelcomeProps) {
function FinishListener (line 232) | function FinishListener() {
constant SCROLL_BOTTOM_THRESHOLD (line 247) | const SCROLL_BOTTOM_THRESHOLD = 100
type AIChatInterfaceProps (line 249) | interface AIChatInterfaceProps {
function AIChatInterface (line 253) | function AIChatInterface({ inputRef }: AIChatInterfaceProps) {
constant GRADIENT_COLORS (line 535) | const GRADIENT_COLORS = [
function gradientByIndex (line 558) | function gradientByIndex(index: number, isDark: boolean) {
FILE: apps/desktop/layer/renderer/src/modules/new-user-guide/discover-import-step.tsx
function DiscoverImportStep (line 9) | function DiscoverImportStep() {
FILE: apps/desktop/layer/renderer/src/modules/new-user-guide/feeds-selection-list.tsx
type FeedToSelect (line 25) | type FeedToSelect = Omit<FeedSelection, "selected">
function FeedsSelectionList (line 43) | function FeedsSelectionList() {
function FeedSelectionOperationScreen (line 59) | function FeedSelectionOperationScreen() {
function FeedSelectionItem (line 133) | function FeedSelectionItem({ feedAtom }: { feedAtom: PrimitiveAtom<FeedS...
function FeedSelectionFirstScreen (line 194) | function FeedSelectionFirstScreen() {
FILE: apps/desktop/layer/renderer/src/modules/new-user-guide/pre-finish.tsx
constant WAIT_DURATION_MS (line 9) | const WAIT_DURATION_MS = 5000
function PreFinish (line 13) | function PreFinish() {
FILE: apps/desktop/layer/renderer/src/modules/new-user-guide/store.ts
type FeedSelection (line 10) | type FeedSelection = {
FILE: apps/desktop/layer/renderer/src/modules/panel/cmdk.tsx
type SearchListType (line 231) | type SearchListType = {
FILE: apps/desktop/layer/renderer/src/modules/player/corner-player.tsx
type ControlButtonProps (line 43) | interface ControlButtonProps {
constant ONE_HOUR_IN_SECONDS (line 339) | const ONE_HOUR_IN_SECONDS = 60 * 60
FILE: apps/desktop/layer/renderer/src/modules/player/entry-tts.ts
constant TTS_MIME_FALLBACK (line 5) | const TTS_MIME_FALLBACK = "audio/ogg; codecs=opus"
constant STREAM_PLACEHOLDER_SRC (line 6) | const STREAM_PLACEHOLDER_SRC = "about:blank"
type TtsAudioHandle (line 11) | type TtsAudioHandle = {
type AudioContextConstructor (line 16) | type AudioContextConstructor =
FILE: apps/desktop/layer/renderer/src/modules/power/transaction-section/tx-table/components.tsx
type TxTableProps (line 13) | interface TxTableProps {
FILE: apps/desktop/layer/renderer/src/modules/profile/account-management.tsx
function AuthProviderButton (line 13) | function AuthProviderButton({ provider }: { provider: string }) {
function AccountManagement (line 65) | function AccountManagement() {
FILE: apps/desktop/layer/renderer/src/modules/profile/email-management.tsx
function EmailManagement (line 32) | function EmailManagement() {
function EmailManagementForm (line 106) | function EmailManagementForm() {
FILE: apps/desktop/layer/renderer/src/modules/profile/hooks.ts
type Variant (line 53) | type Variant = "drawer" | "dialog"
type ResponseType (line 71) | type ResponseType = ReturnType<typeof useDataFetcher>["data"]
function useTOTPModalWrapper (line 105) | function useTOTPModalWrapper<T extends { TOTPCode?: string }>(
FILE: apps/desktop/layer/renderer/src/modules/profile/profile-setting-form.tsx
type ExtendedUser (line 58) | type ExtendedUser = ReturnType<typeof useWhoami> & {
function onSubmit (line 126) | function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/profile/two-factor.tsx
type PasswordFormValues (line 37) | type PasswordFormValues = z.infer<typeof passwordFormSchema>
type TOTPFormValues (line 42) | type TOTPFormValues = z.infer<typeof totpFormSchema>
type PasswordFormProps (line 44) | type PasswordFormProps<V> = {
function TOTPForm (line 65) | function TOTPForm({
function PasswordForm (line 143) | function PasswordForm({
function TwoFactor (line 250) | function TwoFactor() {
FILE: apps/desktop/layer/renderer/src/modules/profile/update-password-form.tsx
function onSubmit (line 72) | function onSubmit(values: z.infer<typeof updatePasswordFormSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/profile/user-profile-modal/UserProfileModalContent.tsx
type ItemVariant (line 32) | type ItemVariant = "loose" | "compact"
type PickedUser (line 219) | type PickedUser = ReturnType<typeof pickUserData>
FILE: apps/desktop/layer/renderer/src/modules/profile/user-profile-modal/shared.tsx
type ItemVariant (line 28) | type ItemVariant = "loose" | "compact"
type SubscriptionModalContentProps (line 30) | interface SubscriptionModalContentProps {
FILE: apps/desktop/layer/renderer/src/modules/renderer/components/TimeStamp.tsx
type CircleProgressProps (line 57) | interface CircleProgressProps {
FILE: apps/desktop/layer/renderer/src/modules/renderer/context.tsx
type EntryContentContext (line 5) | interface EntryContentContext {
type EntryInfoContext (line 25) | interface EntryInfoContext {
FILE: apps/desktop/layer/renderer/src/modules/renderer/html.tsx
function EntryContentHTMLRenderer (line 19) | function EntryContentHTMLRenderer<AS extends keyof JSX.IntrinsicElements...
function isValidTimeString (line 103) | function isValidTimeString(time: string): boolean {
FILE: apps/desktop/layer/renderer/src/modules/renderer/markdown.tsx
type MarkdownProps (line 18) | type MarkdownProps = Omit<ComponentProps<typeof Markdown>, "children">
function EntryContentMarkdownRenderer (line 20) | function EntryContentMarkdownRenderer({
function isValidTimeString (line 104) | function isValidTimeString(time: string): boolean {
FILE: apps/desktop/layer/renderer/src/modules/renderer/types.ts
type EntryContentRendererProps (line 3) | type EntryContentRendererProps = {
FILE: apps/desktop/layer/renderer/src/modules/review-prompt/utils.ts
constant REVIEW_PROMPT_QUIET_WINDOW_MS (line 12) | const REVIEW_PROMPT_QUIET_WINDOW_MS = 5000
type DesktopReviewDistribution (line 14) | type DesktopReviewDistribution = "mas" | "microsoft_store" | "unsupported"
type DesktopReviewRateTarget (line 15) | type DesktopReviewRateTarget = "mas" | "microsoft_store" | null
constant APPLE_REVIEW_URL (line 17) | const APPLE_REVIEW_URL =
constant MICROSOFT_PRODUCT_ID (line 19) | const MICROSOFT_PRODUCT_ID = "9NVFZPV0V0HT"
constant MICROSOFT_REVIEW_URI (line 20) | const MICROSOFT_REVIEW_URI = `ms-windows-store://review/?ProductId=${MIC...
constant MICROSOFT_REVIEW_URL (line 21) | const MICROSOFT_REVIEW_URL = "https://apps.microsoft.com/detail/9nvfzpv0...
constant SUPPORT_EMAIL (line 22) | const SUPPORT_EMAIL = "support@folo.is"
constant REVIEW_PROMPT_STORAGE_PREFIX (line 23) | const REVIEW_PROMPT_STORAGE_PREFIX = getStorageNS("review-prompt")
FILE: apps/desktop/layer/renderer/src/modules/rsshub/add-modal-content.tsx
function AddModalContent (line 26) | function AddModalContent({
FILE: apps/desktop/layer/renderer/src/modules/rsshub/set-modal-content.tsx
function SetModalContent (line 27) | function SetModalContent({
FILE: apps/desktop/layer/renderer/src/modules/settings/constants.ts
constant SETTING_MODAL_ID (line 1) | const SETTING_MODAL_ID = "setting-modal"
constant GUEST_ALLOWED_SETTING_TABS (line 3) | const GUEST_ALLOWED_SETTING_TABS = ["general", "appearance", "about", "s...
constant GUEST_ALLOWED_SETTING_TABS_SET (line 5) | const GUEST_ALLOWED_SETTING_TABS_SET = new Set<string>(GUEST_ALLOWED_SET...
FILE: apps/desktop/layer/renderer/src/modules/settings/helper/setting-builder.tsx
type SharedSettingItem (line 14) | type SharedSettingItem = {
type SettingItem (line 18) | type SettingItem<T, K extends keyof T = keyof T> = {
type SectionSettingItem (line 36) | type SectionSettingItem = {
type ActionSettingItem (line 42) | type ActionSettingItem = {
type CustomSettingItem (line 48) | type CustomSettingItem = ReactNode | FC
FILE: apps/desktop/layer/renderer/src/modules/settings/helper/sync-queue.ts
type SettingMapping (line 22) | type SettingMapping = {
type SettingSyncTab (line 84) | type SettingSyncTab = keyof SettingMapping
type SettingSyncQueueItem (line 85) | interface SettingSyncQueueItem<T extends SettingSyncTab = SettingSyncTab> {
type PersistedSettingSyncQueue (line 91) | interface PersistedSettingSyncQueue {
class SettingSyncQueue (line 96) | class SettingSyncQueue {
method getCurrentUserId (line 100) | private getCurrentUserId() {
method bindQueueOwner (line 104) | private bindQueueOwner(currentUserId: string) {
method reportSyncError (line 116) | private reportSyncError(stage: "flush" | "syncLocal", error: unknown) {
method init (line 124) | async init() {
method teardown (line 155) | teardown() {
method persist (line 164) | private persist() {
method load (line 177) | private load() {
method enqueue (line 222) | async enqueue<T extends SettingSyncTab>(tab: T, payload: Partial<Setti...
method flush (line 256) | private async flush() {
method replaceRemote (line 324) | replaceRemote(tab?: SettingSyncTab) {
method syncLocal (line 361) | async syncLocal() {
FILE: apps/desktop/layer/renderer/src/modules/settings/helper/withSettingEnable.tsx
type WithSelect (line 5) | type WithSelect<T> = T & {
FILE: apps/desktop/layer/renderer/src/modules/settings/hooks/useWrapEnhancedSettingItem.ts
type WrapEnhancedSettingTab (line 7) | enum WrapEnhancedSettingTab {
FILE: apps/desktop/layer/renderer/src/modules/settings/modal/hooks.ts
type Ctx (line 5) | interface Ctx {
FILE: apps/desktop/layer/renderer/src/modules/settings/modal/layout.tsx
function SettingModalLayout (line 34) | function SettingModalLayout(props: PropsWithChildren) {
FILE: apps/desktop/layer/renderer/src/modules/settings/modal/useSettingModal.ts
type SettingModalOptions (line 8) | type SettingModalOptions =
FILE: apps/desktop/layer/renderer/src/modules/settings/sections/fonts.tsx
constant FALLBACK_FONT (line 20) | const FALLBACK_FONT = "Default (UI Font)"
constant DEFAULT_FONT (line 21) | const DEFAULT_FONT = "system-ui"
constant CUSTOM_FONT (line 22) | const CUSTOM_FONT = "Custom"
FILE: apps/desktop/layer/renderer/src/modules/settings/settings-glob.ts
function getSettings (line 6) | function getSettings() {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai.tsx
constant AI_SETTING_SECTION_IDS (line 19) | const AI_SETTING_SECTION_IDS = {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/byok/ByokProviderItem.tsx
type ByokProviderItemProps (line 6) | interface ByokProviderItemProps {
constant PROVIDER_LABELS (line 12) | const PROVIDER_LABELS: Record<string, string> = {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/byok/ByokProviderModalContent.tsx
type ByokProviderModalContentProps (line 17) | interface ByokProviderModalContentProps {
constant EMPTY_CONFIGURED_PROVIDERS (line 24) | const EMPTY_CONFIGURED_PROVIDERS: ByokProviderName[] = []
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/byok/constants.ts
constant PROVIDER_OPTIONS (line 3) | const PROVIDER_OPTIONS: { value: ByokProviderName; label: string }[] = [
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPPresetCard.tsx
type MCPPresetCardProps (line 5) | interface MCPPresetCardProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPPresetSelectionModal.tsx
type MCPPresetSelectionModalProps (line 7) | interface MCPPresetSelectionModalProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceItem.tsx
type MCPServiceItemProps (line 7) | interface MCPServiceItemProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServiceModalContent.tsx
type MCPServiceModalContentProps (line 17) | interface MCPServiceModalContentProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/MCPServicesSection.tsx
type OptimisticMCPService (line 29) | type OptimisticMCPService = WithOptimistic<MCPService>
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/mcp/types.ts
type MCPPreset (line 1) | interface MCPPreset {
constant MCP_PRESETS (line 18) | const MCP_PRESETS: MCPPreset[] = [
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutItem.tsx
type ShortcutItemProps (line 9) | interface ShortcutItemProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/shortcuts/ShortcutModalContent.tsx
type ShortcutModalContentProps (line 20) | interface ShortcutModalContentProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/DetailedUsageModal.tsx
function Metric (line 154) | function Metric({ label, value, unit }: { label: string; value: string; ...
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/EfficiencyTab.tsx
type EfficiencyTabProps (line 8) | interface EfficiencyTabProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/HistoryTab.tsx
type HistoryTabProps (line 7) | interface HistoryTabProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/OverviewTab.tsx
type OverviewTabProps (line 8) | interface OverviewTabProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/PatternsTab.tsx
type PatternsTabProps (line 8) | interface PatternsTabProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/UsageProgressRing.tsx
type UsageProgressRingProps (line 3) | interface UsageProgressRingProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/UsageWarningBanner.tsx
type WarningLevel (line 4) | type WarningLevel = "safe" | "moderate" | "high" | "critical" | (string ...
type UsageWarningBannerProps (line 6) | interface UsageWarningBannerProps {
function formatEta (line 78) | function formatEta(ts: number) {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/charts/BarList.tsx
type BarListProps (line 3) | interface BarListProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/charts/Sparkline.tsx
type SparklineProps (line 1) | interface SparklineProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/components/charts/TinyBars.tsx
type TinyBarsProps (line 5) | interface TinyBarsProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/types.ts
type ChartDataPoint (line 2) | interface ChartDataPoint {
type BarListItem (line 7) | interface BarListItem {
type TokenCount (line 14) | interface TokenCount {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/appearance.tsx
constant ACCENT_COLORS (line 571) | const ACCENT_COLORS: AccentColor[] = [
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/cli.tsx
type CliInstallStatus (line 12) | interface CliInstallStatus {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/data-control.tsx
function onSubmit (line 131) | function onSubmit(values: z.infer<typeof exportFeedFormSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/feeds.tsx
type SortField (line 58) | type SortField = "name" | "view" | "date" | "subscriptionCount" | "updat...
type SortDirection (line 59) | type SortDirection = "asc" | "desc"
type FeedFilter (line 60) | type FeedFilter = "all" | "rsshub"
constant GRID_COLS_CLASSNAME (line 72) | const GRID_COLS_CLASSNAME = "grid-cols-[30px_auto_100px_150px_60px_60px]"
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/general.tsx
method onChange (line 87) | onChange(value) {
method onChangeGuard (line 175) | onChangeGuard(value) {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/integration/CustomIntegrationModal.tsx
type FormData (line 77) | type FormData = z.infer<ReturnType<typeof createFormSchema>>
type CustomIntegrationModalProps (line 79) | interface CustomIntegrationModalProps {
constant HTTP_METHODS (line 85) | const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"] as const
constant INTEGRATION_TYPES (line 86) | const INTEGRATION_TYPES = [
constant ICON_OPTIONS (line 91) | const ICON_OPTIONS = [
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/integration/CustomIntegrationSection.tsx
type CustomIntegrationSectionProps (line 21) | interface CustomIntegrationSectionProps {
type CustomIntegrationsSectionProps (line 179) | interface CustomIntegrationsSectionProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/lists/index.tsx
method onError (line 43) | onError() {
method onMutate (line 46) | onMutate() {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/lists/modals.tsx
function onSubmit (line 98) | function onSubmit(values: z.infer<typeof formSchema>) {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/plan.tsx
constant APPLE_SUBSCRIPTION_MANAGEMENT_URL (line 19) | const APPLE_SUBSCRIPTION_MANAGEMENT_URL = "https://apps.apple.com/accoun...
type ActiveSubscription (line 21) | type ActiveSubscription = {
constant AI_MODEL_SELECTION_VALUE_LABELS (line 31) | const AI_MODEL_SELECTION_VALUE_LABELS = {
function SettingPlan (line 141) | function SettingPlan() {
type PlanCardProps (line 213) | interface PlanCardProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/tabs/shortcut.tsx
type ShortcutInputWrapperProps (line 128) | interface ShortcutInputWrapperProps {
FILE: apps/desktop/layer/renderer/src/modules/settings/utils.ts
type SettingPageContext (line 4) | interface SettingPageContext {
type DisableWhy (line 9) | enum DisableWhy {
type SettingPageConfig (line 14) | interface SettingPageConfig {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/CategoryRemoveDialogContent.tsx
function CategoryRemoveDialogContent (line 12) | function CategoryRemoveDialogContent({
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/CategoryUnsubscribeDialogContent.tsx
function CategoryUnsubscribeDialogContent (line 14) | function CategoryUnsubscribeDialogContent({
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx
type FeedId (line 44) | type FeedId = string
type FeedCategoryProps (line 45) | interface FeedCategoryProps {
function FeedCategoryImpl (line 51) | function FeedCategoryImpl({
function FilterReadFeedCategory (line 394) | function FilterReadFeedCategory(props: FeedCategoryProps) {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/FeedItem.tsx
type FeedItemProps (line 46) | interface FeedItemProps {
method click (line 182) | click() {
type ListItemProps (line 307) | interface ListItemProps {
type InboxItemProps (line 431) | interface InboxItemProps {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/RenameCategoryForm.tsx
method onMutate (line 36) | onMutate({ lastCategory, newCategory }) {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/SimpleDiscoverModal.tsx
function SimpleDiscoverModal (line 55) | function SimpleDiscoverModal({ dismiss }: { dismiss: () => void }) {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/SortedFeedItems.tsx
type SortListProps (line 13) | type SortListProps = {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/SubscriptionTabButton.tsx
function SubscriptionTabButton (line 24) | function SubscriptionTabButton({
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/TimelineTabsSettingsModal.tsx
function ContainerDroppable (line 31) | function ContainerDroppable({
function getViewMeta (line 63) | function getViewMeta(timelineId: string) {
function TabItem (line 70) | function TabItem({ id }: { id: UniqueIdentifier }) {
function SortableTabItem (line 83) | function SortableTabItem({ id }: { id: UniqueIdentifier }) {
function useResolvedTimelineTabs (line 113) | function useResolvedTimelineTabs() {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/atom.ts
type FeedListSortBy (line 7) | type FeedListSortBy = "count" | "alphabetical"
type FeedListSortOrder (line 8) | type FeedListSortOrder = "asc" | "desc"
constant SELECT_NOTHING (line 38) | const SELECT_NOTHING = []
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/hook.ts
function useShouldFreeUpSpace (line 5) | function useShouldFreeUpSpace() {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/index.tsx
function SubscriptionColumn (line 44) | function SubscriptionColumn({
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx
type FeedListProps (line 3) | type FeedListProps = {
type SortBy (line 8) | type SortBy = "count" | "alphabetical"
type ListListProps (line 10) | type ListListProps = {
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/subscription-list/SortButton.tsx
constant SORT_LIST (line 14) | const SORT_LIST = [
FILE: apps/desktop/layer/renderer/src/modules/subscription-column/subscription-list/SubscriptionListGuard.tsx
type SubscriptionProps (line 22) | type SubscriptionProps = ComponentType<
FILE: apps/desktop/layer/renderer/src/modules/trending/index.tsx
type Language (line 34) | type Language = "all" | "eng" | "cmn" | "fra"
type View (line 36) | type View = "all" | string
function Trending (line 56) | function Trending({
FILE: apps/desktop/layer/renderer/src/modules/user/LoginButton.tsx
type LoginProps (line 8) | interface LoginProps {
FILE: apps/desktop/layer/renderer/src/modules/user/ProfileButton.tsx
type ProfileButtonProps (line 35) | type ProfileButtonProps = LoginProps & {
FILE: apps/desktop/layer/renderer/src/modules/user/UserGallery.tsx
type UserGalleryProps (line 5) | interface UserGalleryProps {
FILE: apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/action/index.tsx
function Component (line 7) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx
type SectionProps (line 16) | interface SectionProps {
function Section (line 21) | function Section({ children, className }: SectionProps) {
function Component (line 29) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/power/index.tsx
function Component (line 9) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/rsshub/index.tsx
function Component (line 23) | function Component() {
type InstanceItem (line 83) | type InstanceItem = RSSHubListItem | { id: string; isOfficial: true }
function List (line 248) | function List({ data }: { data?: RSSHubListItem[] }) {
function SelectInstanceButton (line 301) | function SelectInstanceButton({ instance }: { instance: RSSHubListItem }) {
FILE: apps/desktop/layer/renderer/src/pages/(main)/index.sync.tsx
function Component (line 9) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/ai.tsx
function Component (line 17) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/appearance.tsx
function Component (line 14) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/cli.tsx
function Component (line 17) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/data-control.tsx
function Component (line 15) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/feeds.tsx
function Component (line 14) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/general.tsx
function Component (line 14) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/integration.tsx
function Component (line 14) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/list.tsx
function Component (line 15) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/notifications.tsx
function Component (line 14) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/plan.tsx
function Component (line 16) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/profile.tsx
function Component (line 24) | function Component() {
FILE: apps/desktop/layer/renderer/src/pages/settings/(settings)/shortcuts.tsx
function Component (line 16) | function Component() {
FILE: apps/desktop/layer/renderer/src/providers/extension-expose-provider.tsx
type CustomRoute (line 28) | interface CustomRoute {
method updateDownloaded (line 39) | updateDownloaded() {
method distributionUpdateAvailable (line 45) | distributionUpdateAvailable(payload: DistributionUpdateNotice) {
method getApiUrl (line 81) | getApiUrl() {
method getWebUrl (line 84) | getWebUrl() {
method clearIfLoginOtherAccount (line 88) | clearIfLoginOtherAccount(newUserId: string) {
method applyOneTimeToken (line 91) | async applyOneTimeToken(token: string) {
method readyToUpdate (line 96) | readyToUpdate() {
method invalidateQuery (line 102) | invalidateQuery(queryKey: string | string[]) {
method profile (line 124) | profile(id, variant) {
method rsshubRoute (line 127) | rsshubRoute(route) {
FILE: apps/desktop/layer/renderer/src/providers/invalidate-query-provider.tsx
class ElectronCloseEvent (line 10) | class ElectronCloseEvent extends Event {
method constructor (line 12) | constructor() {
class ElectronShowEvent (line 16) | class ElectronShowEvent extends Event {
method constructor (line 18) | constructor() {
method predicate (line 55) | predicate(query) {
FILE: apps/desktop/layer/renderer/src/providers/lazy/index.ts
method onClose (line 35) | onClose() {
FILE: apps/desktop/layer/renderer/src/providers/wrapped-element-provider.tsx
type WrappedElementProviderProps (line 46) | interface WrappedElementProviderProps {
FILE: apps/desktop/layer/renderer/src/push-notification.ts
function registerWebPushNotifications (line 12) | async function registerWebPushNotifications() {
type NavigateEntryMessage (line 69) | interface NavigateEntryMessage {
type ServiceWorkerMessage (line 80) | type ServiceWorkerMessage = NavigateEntryMessage
FILE: apps/desktop/layer/renderer/src/queries/auth.ts
method retry (line 44) | retry(failureCount, error) {
FILE: apps/desktop/layer/renderer/src/queries/feed.ts
type FeedQueryParams (line 15) | type FeedQueryParams = { id?: string; url?: string }
method onError (line 63) | async onError(err) {
method onSuccess (line 66) | onSuccess() {
method onError (line 77) | async onError(err) {
FILE: apps/desktop/layer/renderer/src/queries/types.d.ts
type MutationBaseProps (line 1) | interface MutationBaseProps {
FILE: apps/desktop/layer/renderer/src/queries/users.ts
type AuthProvider (line 6) | interface AuthProvider {
FILE: apps/desktop/layer/renderer/src/queries/wallet.tsx
method onError (line 52) | async onError(err) {
method onSuccess (line 55) | onSuccess() {
FILE: apps/desktop/layer/renderer/src/store/feed/hooks.ts
type PreferredTitleTarget (line 18) | type PreferredTitleTarget = {
FILE: apps/desktop/layer/renderer/src/store/image/db.ts
function createStore (line 6) | function createStore(dbName: string, storeName: string): UseStore {
FILE: apps/desktop/layer/renderer/src/store/image/index.ts
type StoreImageType (line 6) | interface StoreImageType {
type State (line 13) | interface State {
class ImageActions (line 23) | class ImageActions {
method getImage (line 24) | getImage(src: string) {
method saveImages (line 28) | saveImages(images: StoreImageType[]) {
method fetchDimensionsFromDb (line 38) | async fetchDimensionsFromDb(images: string[]) {
method getImagesFromEntry (line 45) | getImagesFromEntry(entry: EntryModel) {
FILE: apps/desktop/layer/renderer/src/store/search/constants.ts
type SearchType (line 12) | type SearchType = (typeof SearchType)[keyof typeof SearchType]
FILE: apps/desktop/layer/renderer/src/store/search/index.ts
class SearchActions (line 35) | class SearchActions {
method reset (line 36) | reset() {
method createFuse (line 40) | private createFuse<T extends object>(data: T[], keys: (keyof T)[]) {
method createLocalDbSearch (line 48) | async createLocalDbSearch() {
method setSearchType (line 108) | setSearchType(type: SearchType) {
method getCurrentKeyword (line 112) | getCurrentKeyword() {
FILE: apps/desktop/layer/renderer/src/store/search/types.ts
type SearchResult (line 6) | interface SearchResult<T extends object, A extends object = object> exte...
type SearchState (line 10) | interface SearchState {
type SearchInstance (line 17) | interface SearchInstance {
FILE: apps/desktop/layer/renderer/src/store/utils/helper.ts
method get (line 59) | get(_, prop) {
type FunctionKeys (line 81) | type FunctionKeys<T> = {
type FunctionProps (line 85) | type FunctionProps<T> = Pick<T, FunctionKeys<T>>
function createImmerSetter (line 100) | function createImmerSetter<T>(useStore: UseBoundStore<StoreApi<T>>) {
type MayBeDraft (line 109) | type MayBeDraft<T> = T
type SyncOrAsync (line 113) | type SyncOrAsync<T> = T | Promise<T>
type ExecutorFn (line 114) | type ExecutorFn<S, Ctx> = (snapshot: S, ctx: Ctx) => SyncOrAsync<void>
class Transaction (line 116) | class Transaction<S, Ctx> {
method constructor (line 124) | constructor(snapshot?: S, ctx?: Ctx) {
method rollback (line 129) | rollback(fn: ExecutorFn<S, Ctx>): this {
method execute (line 134) | execute(executor: ExecutorFn<S, Ctx>): this {
method optimistic (line 139) | optimistic(executor: ExecutorFn<S, Ctx>): this {
method persist (line 144) | persist(fn: ExecutorFn<S, Ctx>): this {
method run (line 149) | async run(): Promise<void> {
FILE: apps/desktop/layer/renderer/src/workers/sw/pusher.ts
type NewEntryMessage (line 2) | interface NewEntryMessage {
type Message (line 11) | type Message = NewEntryMessage
FILE: apps/desktop/plugins/vite/ast.ts
method transform (line 10) | transform(node) {
FILE: apps/desktop/plugins/vite/cleanup.ts
function cleanupUnnecessaryFilesPlugin (line 6) | function cleanupUnnecessaryFilesPlugin(files: string[]): Plugin {
FILE: apps/desktop/plugins/vite/compress.ts
function compressDirectory (line 12) | async function compressDirectory(sourceDir: string, outputFile: string) {
function compressAndFingerprintPlugin (line 23) | function compressAndFingerprintPlugin(outDir: string): Plugin {
FILE: apps/desktop/plugins/vite/deps.ts
function createDependencyChunksPlugin (line 3) | function createDependencyChunksPlugin(dependencies: string[][]): Plugin {
FILE: apps/desktop/plugins/vite/generate-main-hash.ts
function calculateMainHash (line 8) | async function calculateMainHash(
function main (line 36) | async function main() {
FILE: apps/desktop/plugins/vite/hmr.ts
function isNodeWithinCircularImports (line 5) | function isNodeWithinCircularImports(
method configureServer (line 45) | configureServer(server) {
method handleHotUpdate (line 50) | handleHotUpdate({ file, server }: HmrContext) {
FILE: apps/desktop/plugins/vite/html-inject.ts
function htmlInjectPlugin (line 4) | function htmlInjectPlugin(env: typeof EnvType): PluginOption {
FILE: apps/desktop/plugins/vite/i18n-hmr.ts
function customI18nHmrPlugin (line 5) | function customI18nHmrPlugin(): Plugin {
FILE: apps/desktop/plugins/vite/locales-json.ts
function localesJsonPlugin (line 10) | function localesJsonPlugin(): Plugin {
FILE: apps/desktop/plugins/vite/locales.ts
function localesPlugin (line 8) | function localesPlugin(): Plugin {
FILE: apps/desktop/plugins/vite/manifest.ts
function manifestPlugin (line 6) | function manifestPlugin(): Plugin {
FILE: apps/desktop/plugins/vite/specific-import.ts
type Platform (line 3) | type Platform = "electron" | "web"
function createPlatformSpecificImportPlugin (line 4) | function createPlatformSpecificImportPlugin(platform: Platform): Plugin {
FILE: apps/desktop/plugins/vite/utils/i18n-completeness.ts
type LanguageCompletion (line 6) | type LanguageCompletion = Record<string, number>
function getLanguageFiles (line 8) | function getLanguageFiles(dir: string): string[] {
function getNamespaces (line 12) | function getNamespaces(localesDir: string): string[] {
function countKeys (line 18) | function countKeys(obj: any): number {
function calculateCompleteness (line 30) | function calculateCompleteness(localesDir: string): LanguageCompletion {
FILE: apps/desktop/scripts/generate-appx-manifest.ts
type AppXManifestConfig (line 7) | interface AppXManifestConfig {
function generateAppXManifest (line 20) | function generateAppXManifest(config: AppXManifestConfig, templatePath: ...
function main (line 50) | async function main() {
FILE: apps/desktop/scripts/merge-yml.ts
function findYmlFiles (line 8) | function findYmlFiles(dir: string): string[] {
FILE: apps/desktop/vite.config.ts
constant ROOT (line 26) | const ROOT = resolve(__dirname, "./layer/renderer")
method configureServer (line 30) | configureServer(server: ViteDevServer) {
function checkBrowserSupport (line 309) | function checkBrowserSupport() {
method configResolved (line 323) | configResolved(resolvedConfig) {
method closeBundle (line 327) | closeBundle() {
method transformIndexHtml (line 346) | transformIndexHtml(html) {
FILE: apps/landing/global.d.ts
type NextErrorProps (line 6) | type NextErrorProps = {
type NextPageParams (line 10) | type NextPageParams<P extends {}, Props = {}> = PropsWithChildren<
type Component (line 16) | type Component<P = {}> = FC<ComponentType & P>
type ComponentType (line 18) | type ComponentType<P = {}> = {
type Document (line 24) | interface Document {
type ViewTransition (line 28) | interface ViewTransition {
type AriaAttributes (line 37) | interface AriaAttributes {
FILE: apps/landing/next.config.mjs
method rewrites (line 34) | async rewrites() {
FILE: apps/landing/plugins/eslint-recursive-sort.mjs
method create (line 27) | create(context) {
FILE: apps/landing/src/app/[locale]/download/page.tsx
type LocaleParams (line 10) | type LocaleParams = { locale?: string }
function generateMetadata (line 14) | async function generateMetadata({
function DownloadPage (line 33) | async function DownloadPage() {
FILE: apps/landing/src/app/[locale]/layout.tsx
type LocaleParams (line 21) | type LocaleParams = { locale?: string }
type MaybeAsyncLocaleParams (line 23) | type MaybeAsyncLocaleParams = LocaleParams | Promise<LocaleParams> | und...
function generateStaticParams (line 37) | function generateStaticParams() {
function generateMetadata (line 41) | async function generateMetadata(props: {
function generateViewport (line 106) | function generateViewport(): Viewport {
function LocaleLayout (line 121) | async function LocaleLayout({
FILE: apps/landing/src/app/[locale]/page.tsx
type LocaleParams (line 14) | type LocaleParams = { locale?: string }
function Home (line 16) | async function Home({
FILE: apps/landing/src/app/[locale]/pricing/page.tsx
type LocaleParams (line 8) | type LocaleParams = { locale?: string }
function generateMetadata (line 12) | async function generateMetadata({
function PricingPage (line 31) | async function PricingPage() {
FILE: apps/landing/src/app/[locale]/privacy-policy/page.tsx
function PrivacyPolicyPage (line 32) | async function PrivacyPolicyPage() {
FILE: apps/landing/src/app/[locale]/terms-of-service/page.tsx
function TermsOfServicePage (line 30) | async function TermsOfServicePage() {
FILE: apps/landing/src/app/apple-app-site-association/route.ts
function GET (line 3) | function GET() {
FILE: apps/landing/src/app/discover-sources/route.ts
constant CACHE_TTL_MS (line 8) | const CACHE_TTL_MS = 12 * 60 * 60 * 1000
function GET (line 17) | async function GET() {
FILE: apps/landing/src/app/layout.tsx
function RootLayout (line 3) | function RootLayout({
FILE: apps/landing/src/app/robots.ts
function robots (line 3) | function robots(): MetadataRoute.Robots {
FILE: apps/landing/src/components/common/GithubTrending.tsx
function GithubTrending (line 1) | function GithubTrending() {
FILE: apps/landing/src/components/common/Lazyload.tsx
type LazyLoadProps (line 9) | type LazyLoadProps = {
FILE: apps/landing/src/components/common/LightRays.tsx
type RaysOrigin (line 10) | type RaysOrigin =
type LightRaysProps (line 20) | interface LightRaysProps {
constant DEFAULT_COLOR (line 41) | const DEFAULT_COLOR = '#ffffff'
FILE: apps/landing/src/components/common/QueryHydrate.tsx
function QueryHydrate (line 6) | function QueryHydrate(props: HydrationBoundaryProps) {
FILE: apps/landing/src/components/layout/footer/Footer.tsx
type LinkItem (line 8) | type LinkItem = { label: string; href: string; external?: boolean }
function LinkColumn (line 49) | function LinkColumn({ title, links }: { title: string; links: LinkItem[]...
type FooterProps (line 75) | interface FooterProps {
FILE: apps/landing/src/components/ui/accordion/Accordion.tsx
type CollapseContextValue (line 14) | interface CollapseContextValue {
type CollapseGroupProps (line 29) | interface CollapseGroupProps {
type CollapseProps (line 66) | interface CollapseProps {
type CollapseContentProps (line 139) | interface CollapseContentProps {
FILE: apps/landing/src/components/ui/border-beam.tsx
type BorderBeamProps (line 6) | interface BorderBeamProps {
FILE: apps/landing/src/components/ui/button/Button.tsx
type ButtonProps (line 104) | interface ButtonProps
FILE: apps/landing/src/components/ui/checkbox/Checkbox.tsx
type CheckboxProps (line 42) | type CheckboxProps = React.ComponentProps<typeof CheckboxPrimitive.Root> &
function Checkbox (line 48) | function Checkbox({
FILE: apps/landing/src/components/ui/collapse/CollapseCss.tsx
type CollapseContextValue (line 12) | interface CollapseContextValue {
type CollapseGroupProps (line 27) | interface CollapseGroupProps {
type CollapseProps (line 64) | interface CollapseProps {
type CollapseCssRef (line 78) | interface CollapseCssRef {
type CollapseContentProps (line 147) | interface CollapseContentProps {
FILE: apps/landing/src/components/ui/collapse/hooks.tsx
type CollapseContextValue (line 4) | interface CollapseContextValue {
FILE: apps/landing/src/components/ui/dialog/Dialog.tsx
type DialogContextType (line 11) | type DialogContextType = {
type DialogProps (line 27) | type DialogProps = React.ComponentProps<typeof DialogPrimitive.Root>
function Dialog (line 29) | function Dialog({ children, ...props }: DialogProps) {
type DialogTriggerProps (line 59) | type DialogTriggerProps = React.ComponentProps<typeof DialogPrimitive.Tr...
function DialogTrigger (line 61) | function DialogTrigger(props: DialogTriggerProps) {
type DialogPortalProps (line 65) | type DialogPortalProps = React.ComponentProps<typeof DialogPrimitive.Por...
function DialogPortal (line 67) | function DialogPortal(props: DialogPortalProps) {
type DialogCloseProps (line 71) | type DialogCloseProps = React.ComponentProps<typeof DialogPrimitive.Close>
function DialogClose (line 73) | function DialogClose(props: DialogCloseProps) {
type DialogOverlayProps (line 83) | type DialogOverlayProps = React.ComponentProps<typeof DialogPrimitive.Ov...
function DialogOverlay (line 85) | function DialogOverlay({ className, ...props }: DialogOverlayProps) {
type DialogContentProps (line 98) | type DialogContentProps = React.ComponentProps<
function DialogContent (line 114) | function DialogContent({
type DialogHeaderProps (line 187) | type DialogHeaderProps = React.ComponentProps<'div'>
function DialogHeader (line 189) | function DialogHeader({ className, ...props }: DialogHeaderProps) {
type DialogFooterProps (line 202) | type DialogFooterProps = React.ComponentProps<'div'>
function DialogFooter (line 204) | function DialogFooter({ className, ...props }: DialogFooterProps) {
type DialogTitleProps (line 217) | type DialogTitleProps = React.ComponentProps<typeof DialogPrimitive.Title>
function DialogTitle (line 219) | function DialogTitle({ className, ...props }: DialogTitleProps) {
type DialogDescriptionProps (line 232) | type DialogDescriptionProps = React.ComponentProps<
function DialogDescription (line 236) | function DialogDescription({ className, ...props }: DialogDescriptionPro...
FILE: apps/landing/src/components/ui/effects/GridGuides.tsx
type GridGuidesProps (line 3) | type GridGuidesProps = React.PropsWithChildren<{
function GridGuides (line 16) | function GridGuides({
FILE: apps/landing/src/components/ui/effects/ParticlesAura.tsx
type ParticlesAuraProps (line 6) | type ParticlesAuraProps = {
function ParticlesAura (line 16) | function ParticlesAura({
FILE: apps/landing/src/components/ui/effects/TiltCard.tsx
type TiltCardProps (line 6) | type TiltCardProps = React.PropsWithChildren<{
function TiltCard (line 16) | function TiltCard({
FILE: apps/landing/src/components/ui/glass/index.tsx
type GlassSurfaceProps (line 8) | interface GlassSurfaceProps {
FILE: apps/landing/src/components/ui/highlighter.tsx
type AnnotationAction (line 7) | type AnnotationAction =
type HighlighterProps (line 16) | interface HighlighterProps {
function Highlighter (line 28) | function Highlighter({
FILE: apps/landing/src/components/ui/hover-card/HoverCard.tsx
type HoverCardProps (line 10) | type HoverCardProps = React.ComponentProps<typeof HoverCardPrimitive.Root>
type HoverCardTriggerProps (line 12) | type HoverCardTriggerProps = React.ComponentProps<
type HoverCardContentProps (line 16) | type HoverCardContentProps = React.ComponentProps<
FILE: apps/landing/src/components/ui/input/Input.tsx
type InputProps (line 67) | interface InputProps
FILE: apps/landing/src/components/ui/input/TextArea.tsx
type TextareaProps (line 9) | interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAre...
FILE: apps/landing/src/components/ui/json-highlighter/index.tsx
type JsonHighlighterProps (line 5) | interface JsonHighlighterProps {
type Token (line 89) | interface Token {
function highlightJson (line 106) | function highlightJson(jsonString: string): string {
function tokenizeJson (line 120) | function tokenizeJson(json: string): Token[] {
function isFollowedByColon (line 346) | function isFollowedByColon(json: string, startIndex: number): boolean {
function renderTokens (line 360) | function renderTokens(tokens: Token[], originalJson: string): string {
FILE: apps/landing/src/components/ui/label/Label.tsx
type LabelProps (line 7) | interface LabelProps extends React.ComponentPropsWithoutRef<
FILE: apps/landing/src/components/ui/light-rays.tsx
type LightRaysProps (line 8) | interface LightRaysProps extends React.HTMLAttributes<HTMLDivElement> {
type LightRay (line 17) | type LightRay = {
function LightRays (line 87) | function LightRays({
FILE: apps/landing/src/components/ui/loading/index.tsx
type LoadingProps (line 5) | type LoadingProps = {
FILE: apps/landing/src/components/ui/magic-card.tsx
type MagicCardProps (line 8) | interface MagicCardProps {
function MagicCard (line 18) | function MagicCard({
FILE: apps/landing/src/components/ui/markdown/index.tsx
type MarkdownContentProps (line 9) | interface MarkdownContentProps {
function MarkdownContent (line 13) | function MarkdownContent({ content }: MarkdownContentProps) {
function getMarkdownContent (line 30) | async function getMarkdownContent(filePath: string) {
FILE: apps/landing/src/components/ui/modal/ModalManager.ts
method present (line 14) | present<P = unknown>(
method dismiss (line 49) | dismiss(id: string): void {
method __registerCloser (line 64) | __registerCloser(id: string, fn: () => void) {
method __unregisterCloser (line 67) | __unregisterCloser(id: string) {
FILE: apps/landing/src/components/ui/modal/types.ts
type ModalComponentProps (line 5) | type ModalComponentProps = {
type ModalComponent (line 10) | type ModalComponent<P = unknown> = FC<ModalComponentProps & P> & {
type ModalContentConfig (line 19) | type ModalContentConfig = Partial<DialogContentProps>
type ModalItem (line 21) | type ModalItem = {
FILE: apps/landing/src/components/ui/prompts/BasePrompt.tsx
type PromptVariant (line 19) | type PromptVariant = 'danger' | 'info'
type PromptOptions (line 21) | type PromptOptions = {
FILE: apps/landing/src/components/ui/prompts/InputPrompt.tsx
type InputPromptVariant (line 20) | type InputPromptVariant = 'danger' | 'info'
type InputPromptOptions (line 22) | type InputPromptOptions = {
FILE: apps/landing/src/components/ui/prompts/Prompt.ts
method prompt (line 8) | prompt(options: PromptOptions) {
method input (line 11) | input(options: InputPromptOptions): Promise<string | null> {
FILE: apps/landing/src/components/ui/segment-tab/SegmentTab.tsx
type SegmentTabItem (line 10) | interface SegmentTabItem<T = string> {
type SegmentTabProps (line 16) | interface SegmentTabProps<T = string> {
function SegmentTab (line 63) | function SegmentTab<T = string>({
FILE: apps/landing/src/components/ui/select/ComboboxSelect.tsx
type ComboboxSelectProps (line 15) | interface ComboboxSelectProps {
constant DEFAULT_OPTIONS (line 30) | const DEFAULT_OPTIONS: string[] = []
FILE: apps/landing/src/components/ui/select/MultiSelect.tsx
type MultiSelectProps (line 13) | interface MultiSelectProps {
constant DEFAULT_VALUE (line 26) | const DEFAULT_VALUE: string[] = []
constant DEFAULT_OPTIONS (line 27) | const DEFAULT_OPTIONS: string[] = []
FILE: apps/landing/src/components/ui/sheet/Sheet.tsx
type PresentSheetProps (line 7) | interface PresentSheetProps {
FILE: apps/landing/src/components/ui/switch/index.tsx
type SwitchProps (line 10) | type SwitchProps = React.ComponentProps<typeof SwitchPrimitives.Root> &
function Switch (line 17) | function Switch({
FILE: apps/landing/src/components/ui/transition/IconTransiton.tsx
type IconTransitionProps (line 10) | interface IconTransitionProps {
FILE: apps/landing/src/components/ui/transition/factor.tsx
type TransitionViewParams (line 18) | interface TransitionViewParams {
FILE: apps/landing/src/components/ui/transition/typings.ts
type BaseTransitionProps (line 3) | interface BaseTransitionProps extends HTMLMotionProps<'div'> {
FILE: apps/landing/src/components/widgets/download/PlatformDownloads.tsx
type PlatformDownloadsProps (line 18) | type PlatformDownloadsProps = {
FILE: apps/landing/src/components/widgets/landing/Features.tsx
type FeaturesProps (line 13) | type FeaturesProps = {
type FGProps (line 69) | type FGProps = {
function FeatureGridItem (line 77) | function FeatureGridItem({
function DiscoverWindow (line 98) | function DiscoverWindow({ sources }: { sources: DiscoverSource[] }) {
FILE: apps/landing/src/components/widgets/landing/Hero.tsx
type LandingHeroProps (line 26) | type LandingHeroProps = {
type AudienceKey (line 30) | type AudienceKey = 'human' | 'agent'
FILE: apps/landing/src/components/widgets/landing/RepoStats.tsx
function RepoStats (line 9) | function RepoStats() {
function StatBox (line 63) | function StatBox({
FILE: apps/landing/src/components/widgets/landing/TrustedBy.tsx
type TrustedByProps (line 7) | type TrustedByProps = {
FILE: apps/landing/src/components/widgets/landing/ViewsShowcase.tsx
type ViewKey (line 7) | type ViewKey = 'list' | 'social' | 'masonry' | 'grid'
FILE: apps/landing/src/components/widgets/landing/WindowChrome.tsx
function WindowChrome (line 7) | function WindowChrome({
FILE: apps/landing/src/components/widgets/pricing/PricingPlans.tsx
type BillingPeriod (line 12) | type BillingPeriod = 'monthly' | 'yearly'
constant AI_MODEL_SELECTION_LABELS (line 14) | const AI_MODEL_SELECTION_LABELS = {
FILE: apps/landing/src/components/widgets/simulators/EntryChatPanel.tsx
type EntryChatPanelProps (line 7) | interface EntryChatPanelProps {
FILE: apps/landing/src/components/widgets/simulators/ListDemo.tsx
constant DEFAULT_ITEMS (line 53) | const DEFAULT_ITEMS: HeroTimelineItem[] = [
FILE: apps/landing/src/components/widgets/simulators/TimelineChatDemo.tsx
constant INITIAL_HEADER_TITLE (line 5) | const INITIAL_HEADER_TITLE = 'Folo AI - Timeline Summary'
FILE: apps/landing/src/components/widgets/simulators/components/ai/AIChainOfThought.tsx
type ChainReasoningPart (line 16) | type ChainReasoningPart = ReasoningUIPart | ToolUIPart
type AIChainOfThoughtProps (line 17) | interface AIChainOfThoughtProps {
FILE: apps/landing/src/components/widgets/simulators/components/ai/AIReasoningPart.tsx
type AIReasoningPartProps (line 4) | interface AIReasoningPartProps {
FILE: apps/landing/src/components/widgets/simulators/components/ai/ToolInvocationComponent.tsx
type ToolInvocationComponentProps (line 7) | interface ToolInvocationComponentProps {
FILE: apps/landing/src/components/widgets/simulators/components/ai/animated/AnimatedMarkdown.tsx
type MarkdownAnimateTextProps (line 13) | interface MarkdownAnimateTextProps {
FILE: apps/landing/src/components/widgets/simulators/components/ai/animated/TokenizedText.tsx
type TokenWithSource (line 10) | interface TokenWithSource {
type TokenType (line 23) | type TokenType = string | TokenWithSource | ReactElement
FILE: apps/landing/src/components/widgets/simulators/components/ai/animated/constants.ts
constant DEFAULT_ANIMATION (line 1) | const DEFAULT_ANIMATION = 'mask-left-to-right 0.5s ease-in-out'
constant ANIMATION_STYLE (line 2) | const ANIMATION_STYLE = {
FILE: apps/landing/src/components/widgets/simulators/components/ai/mocks.ts
constant REASONING_GROUPS (line 3) | const REASONING_GROUPS = ReasoningGroups.groups as any
FILE: apps/landing/src/components/widgets/simulators/components/ai/shiny-text/ShinyText.tsx
type AnimatedShinyTextProps (line 7) | interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<'span'> {
FILE: apps/landing/src/components/widgets/simulators/components/chat/AiMockMessage.tsx
type AiMockMessageProps (line 11) | interface AiMockMessageProps {
FILE: apps/landing/src/components/widgets/simulators/components/chat/ListChatPlayer.tsx
type ListChatPlayerProps (line 52) | interface ListChatPlayerProps {
FILE: apps/landing/src/components/widgets/simulators/components/chat/stream.ts
type StreamOptions (line 1) | interface StreamOptions {
type StreamHandle (line 9) | interface StreamHandle {
function streamText (line 18) | function streamText(text: string, options: StreamOptions): StreamHandle {
FILE: apps/landing/src/components/widgets/simulators/mocks.tsx
type EntryDetail (line 7) | type EntryDetail = {
constant ENTRY_DETAIL (line 22) | const ENTRY_DETAIL: EntryDetail = {
type ToolInvocation (line 37) | type ToolInvocation = {
constant AI_TOOL_INVOCATION (line 43) | const AI_TOOL_INVOCATION: ToolInvocation = {
type UserContext (line 61) | type UserContext = {
type UserPlainTextStep (line 67) | type UserPlainTextStep = {
type UserComponentStep (line 72) | type UserComponentStep = {
type UserStep (line 81) | type UserStep = UserPlainTextStep | UserComponentStep
type AIReasoningPart (line 82) | type AIReason
Condensed preview — 2869 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (9,386K chars).
[
{
"path": ".agents/settings.local.json",
"chars": 660,
"preview": "{\n \"permissions\": {\n \"allow\": [\n \"Bash(gh pr view:*)\",\n \"Bash(pnpm bump:*)\",\n \"Bash(git add:*)\",\n "
},
{
"path": ".agents/skills/desktop-release/SKILL.md",
"chars": 6143,
"preview": "---\nname: desktop-release\ndescription: Perform a regular desktop release from the dev branch. Gathers commits since last"
},
{
"path": ".agents/skills/installing-mobile-preview-builds/SKILL.md",
"chars": 2682,
"preview": "---\nname: installing-mobile-preview-builds\ndescription: Builds and installs the iOS preview build for apps/mobile using "
},
{
"path": ".agents/skills/mobile-e2e/SKILL.md",
"chars": 4048,
"preview": "---\nname: mobile-e2e\ndescription: Run apps/mobile Maestro end-to-end tests in this repo. Use when an agent needs to vali"
},
{
"path": ".agents/skills/mobile-release/SKILL.md",
"chars": 4726,
"preview": "---\nname: mobile-release\ndescription: Perform a regular mobile release from the dev branch. Gathers commits since last r"
},
{
"path": ".agents/skills/mobile-self-test/SKILL.md",
"chars": 14398,
"preview": "---\nname: mobile-self-test\ndescription: Self-test a mobile feature change or bug fix after implementation in `apps/mobil"
},
{
"path": ".agents/skills/update-deps/SKILL.md",
"chars": 6262,
"preview": "---\nname: update-deps\ndescription: Update all dependencies across frontend and backend projects. Reads changelogs for br"
},
{
"path": ".cursorignore",
"chars": 82,
"preview": "# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)\n"
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
},
{
"path": ".gitattributes",
"chars": 68,
"preview": "* text=auto eol=lf\n*.splinecode filter=lfs diff=lfs merge=lfs -text\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2952,
"preview": "name: 🐞 Bug report\ndescription: Report an issue\nlabels: [pending triage, bug]\ntype: Bug\nbody:\n - type: markdown\n att"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 369,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 💬 Follow's Discord Server\n url: https://discord.gg/tUDVZjEr\n "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1559,
"preview": "name: 🚀 New feature proposal\ndescription: Propose a new feature\nlabels: [enhancement]\ntype: Feature\nbody:\n - type: mark"
},
{
"path": ".github/ISSUE_TEMPLATE/i18n.yml",
"chars": 1974,
"preview": "name: 🌐 Internationalization (i18n)\ndescription: Contribute to or report issues with translations\ntitle: \"[i18n]: \"\nlabe"
},
{
"path": ".github/ISSUE_TEMPLATE/typo.yml",
"chars": 723,
"preview": "name: 👀 Typo / Grammar fix\ndescription: You can just go ahead and send a PR! Thank you!\nlabels: []\nbody:\n - type: markd"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1092,
"preview": "<!-- DO NOT IGNORE THE TEMPLATE!\n\nThank you for contributing!\n\nBefore submitting the PR, please make sure you do the fol"
},
{
"path": ".github/actions/setup-version/action.yml",
"chars": 1145,
"preview": "name: Setup Version\ndescription: \"Setup Version\"\ninputs:\n type:\n required: true\n description: \"Type of the app, e"
},
{
"path": ".github/actions/setup-xcode/action.yml",
"chars": 4277,
"preview": "name: \"Setup Xcode\"\ndescription: \"Setup specific Xcode version for iOS builds\"\ninputs:\n xcode-version:\n description:"
},
{
"path": ".github/advanced-issue-labeler.yml",
"chars": 338,
"preview": "policy:\n - section:\n - id: [platform]\n block-list: [\"None\", \"Other\"]\n label:\n - name: \"plat"
},
{
"path": ".github/copilot-instructions.md",
"chars": 73,
"preview": "Before you start, you need to read and follow the rules in @../CLAUDE.md\n"
},
{
"path": ".github/dependabot.yaml",
"chars": 1667,
"preview": "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: weekly\n day: friday\n "
},
{
"path": ".github/prompts/similar_issues.prompt.yml",
"chars": 1733,
"preview": "messages:\n - role: system\n content: |-\n You are a GitHub assistant with access to GitHub Model Context Protocol"
},
{
"path": ".github/scripts/extract-release-info.mjs",
"chars": 3485,
"preview": "#!/usr/bin/env node\n\n/**\n * Extract release version and platform information from git commit messages\n * Used by GitHub "
},
{
"path": ".github/workflows/build-android.yml",
"chars": 4240,
"preview": "name: 🤖 Build Android\n\non:\n push:\n branches:\n - \"**\"\n paths:\n - \"apps/mobile/**\"\n - \"pnpm-lock.yam"
},
{
"path": ".github/workflows/build-desktop.yml",
"chars": 13043,
"preview": "name: 🖥️ Build Desktop\n\non:\n push:\n branches:\n - \"**\"\n paths:\n - \"apps/desktop/**\"\n - \"packages/**"
},
{
"path": ".github/workflows/build-ios-development.yml",
"chars": 4684,
"preview": "name: 📱 Build iOS for development\n\non:\n push:\n branches:\n - \"**\"\n paths:\n - \"apps/mobile/web-app/**\"\n "
},
{
"path": ".github/workflows/build-ios.yml",
"chars": 4075,
"preview": "name: 🍎 Build iOS\n\non:\n push:\n branches:\n - \"**\"\n paths:\n - \"apps/mobile/**\"\n - \"pnpm-lock.yaml\"\n "
},
{
"path": ".github/workflows/build-web.yml",
"chars": 1178,
"preview": "on:\n pull_request:\n push:\n branches: [main, dev]\n\nname: 🌐 CI Build web and SSR server\nconcurrency:\n group: ${{ git"
},
{
"path": ".github/workflows/deploy-cloudflare-desktop.yml",
"chars": 2060,
"preview": "name: \"\\u2601\\ufe0f Deploy Desktop to Cloudflare\"\n\non:\n push:\n branches: [main, dev]\n\nconcurrency:\n group: ${{ gith"
},
{
"path": ".github/workflows/deploy-cloudflare-landing.yml",
"chars": 2664,
"preview": "name: \"\\u2601\\ufe0f Deploy Landing to Cloudflare\"\n\non:\n push:\n branches: [main, dev]\n workflow_dispatch:\n inputs"
},
{
"path": ".github/workflows/deploy-cloudflare-ssr.yml",
"chars": 3560,
"preview": "name: \"\\u2601\\ufe0f Deploy SSR to Cloudflare\"\n\non:\n push:\n branches: [main, dev]\n workflow_dispatch:\n inputs:\n "
},
{
"path": ".github/workflows/issue-labeler.yml",
"chars": 786,
"preview": "name: 🏷️ Issue labeler\non:\n issues:\n types: [opened]\n\npermissions:\n contents: read\n\njobs:\n label-component:\n ru"
},
{
"path": ".github/workflows/lint.yml",
"chars": 1556,
"preview": "on:\n pull_request:\n push:\n branches: [main, dev]\n\n# Do not use secrets or variables to allow for public access\nenv:"
},
{
"path": ".github/workflows/pr-title-check.yml",
"chars": 431,
"preview": "name: ✅ PR Conventional Commit Validation\n\non:\n pull_request:\n types: [opened, synchronize, reopened, edited]\n\njobs:"
},
{
"path": ".github/workflows/similar-issues.yml",
"chars": 4109,
"preview": "name: Similar Issues via AI MCP\n\non:\n issues:\n types: [opened]\n\njobs:\n find-similar:\n permissions:\n content"
},
{
"path": ".github/workflows/sync.yaml",
"chars": 1045,
"preview": "name: 🔄 Sync Release Branches To Dev\n\non:\n push:\n branches:\n - main\n - mobile-main\n\npermissions:\n content"
},
{
"path": ".github/workflows/tag.yml",
"chars": 7481,
"preview": "name: 🏷️ Release Orchestrator\n\non:\n push:\n branches:\n - main\n - mobile-main\n\npermissions:\n contents: writ"
},
{
"path": ".github/workflows/translator.yml",
"chars": 584,
"preview": "name: \"🌍 translator\"\non:\n issues:\n types: [opened, edited]\n issue_comment:\n types: [created, edited]\n discussio"
},
{
"path": ".gitignore",
"chars": 697,
"preview": "node_modules\ndist\nout\n.next\n.open-next\nnext-env.d.ts\n.DS_Store\n*.log*\n.env\n.eslintcache\n.env.*\n!.env.example\n\n# Sentry C"
},
{
"path": ".npmrc",
"chars": 58,
"preview": "shamefully-hoist=true\nnode-linker=hoisted\nsave-exact=true\n"
},
{
"path": ".nvmrc",
"chars": 7,
"preview": "stable\n"
},
{
"path": ".prettierignore",
"chars": 226,
"preview": "pnpm-lock.yaml\n\nCHANGELOG.md\n.context\n\napps/external/postcss.config.cjs\n\napps/mobile/android\napps/mobile/ios\napps/mobile"
},
{
"path": ".prettierrc.mjs",
"chars": 853,
"preview": "/** @type {import(\"prettier\").Config & import(\"prettier-plugin-tailwindcss\").PluginOptions} */\nexport default {\n semi: "
},
{
"path": ".vscode/extensions.json",
"chars": 159,
"preview": "{\n \"recommendations\": [\n \"dbaeumer.vscode-eslint\",\n \"johnsoncodehk.vscode-tsslint\",\n \"esbenp.prettier-vscode\","
},
{
"path": ".vscode/launch.json",
"chars": 915,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Debug Main Process\",\n \"type\": \"node\",\n \"req"
},
{
"path": ".vscode/settings.json",
"chars": 2764,
"preview": "{\n \"editor.formatOnSave\": true,\n \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n \"[javascript][javascriptreact]"
},
{
"path": "AGENTS.md",
"chars": 7681,
"preview": "# AGENTS.md\n\nThis file provides concise, agent-focused guidance for working in this monorepo. It consolidates the reposi"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3349,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 3585,
"preview": "# Contributing to Folo\n\nThank you for considering contributing to Folo! We welcome contributions from the community to h"
},
{
"path": "LICENSE",
"chars": 34769,
"preview": " GNU AFFERO GENERAL PUBLIC LICENSE\n Version 3, 19 November 2007\n\n Copyright (C)"
},
{
"path": "README.md",
"chars": 12991,
"preview": "<div align=\"center\">\n <a href=\"https://github.com/RSSNext/Folo\">\n <img src=\"https://github.com/RSSNext/Folo/raw/refs"
},
{
"path": "SECURITY.md",
"chars": 226,
"preview": "# Security Policy\n\n## Supported Versions\n\nWe always recommend using the latest version of Follow to ensure you get all s"
},
{
"path": "api/vercel_webhook.ts",
"chars": 3206,
"preview": "import crypto from \"node:crypto\"\n\nimport type { VercelRequest, VercelResponse } from \"@vercel/node\"\nimport getRawBody fr"
},
{
"path": "apps/cli/package.json",
"chars": 1062,
"preview": "{\n \"name\": \"folocli\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"description\": \"Folo CLI for terminal workflows and a"
},
{
"path": "apps/cli/skill.md",
"chars": 6357,
"preview": "# Folo CLI Skill\n\n## Trigger Conditions\n\nUse this skill when a user asks to:\n\n- Manage RSS subscriptions\n- Browse timeli"
},
{
"path": "apps/cli/src/args.test.ts",
"chars": 1945,
"preview": "import { describe, expect, it } from \"vitest\"\n\nimport { parseFormat, parseISODate, parseNonNegativeInt, parsePositiveInt"
},
{
"path": "apps/cli/src/args.ts",
"chars": 1753,
"preview": "import type { OutputFormat } from \"./output\"\n\nconst viewMap: Readonly<Record<string, number>> = {\n article: 0,\n articl"
},
{
"path": "apps/cli/src/browser-login.test.ts",
"chars": 1998,
"preview": "import { describe, expect, it } from \"vitest\"\n\nimport { DEFAULT_VALUES } from \"../../../packages/internal/shared/src/env"
},
{
"path": "apps/cli/src/browser-login.ts",
"chars": 6235,
"preview": "import { spawnSync } from \"node:child_process\"\nimport { createServer } from \"node:http\"\nimport type { AddressInfo } from"
},
{
"path": "apps/cli/src/cli.e2e.test.ts",
"chars": 2812,
"preview": "import type { ExecFileException } from \"node:child_process\"\nimport { execFile } from \"node:child_process\"\nimport { mkdte"
},
{
"path": "apps/cli/src/client.ts",
"chars": 2785,
"preview": "import { FollowClient } from \"@follow-app/client-sdk\"\nimport type { Command } from \"commander\"\n\nimport type { FoloCLICon"
},
{
"path": "apps/cli/src/command.ts",
"chars": 995,
"preview": "import type { Command } from \"commander\"\n\nimport type { CommandContext } from \"./client\"\nimport { createCommandContext, "
},
{
"path": "apps/cli/src/commands/auth.ts",
"chars": 3634,
"preview": "import type { Command } from \"commander\"\n\nimport { parsePositiveInt } from \"../args\"\nimport { loginWithBrowser } from \"."
},
{
"path": "apps/cli/src/commands/collection.ts",
"chars": 2249,
"preview": "import type { EntryListRequest } from \"@follow-app/client-sdk\"\nimport type { Command } from \"commander\"\n\nimport { parseI"
},
{
"path": "apps/cli/src/commands/entry.ts",
"chars": 2733,
"preview": "import type { MarkAllAsReadRequest } from \"@follow-app/client-sdk\"\nimport type { Command } from \"commander\"\n\nimport { pa"
},
{
"path": "apps/cli/src/commands/feed.ts",
"chars": 1448,
"preview": "import type { Command } from \"commander\"\n\nimport { runCommand } from \"../command\"\n\nconst isURL = (value: string) => valu"
},
{
"path": "apps/cli/src/commands/list.ts",
"chars": 4552,
"preview": "import type { Command } from \"commander\"\n\nimport { parseNonNegativeInt, parseView, viewHelp } from \"../args\"\nimport { ru"
},
{
"path": "apps/cli/src/commands/opml.ts",
"chars": 2368,
"preview": "import { mkdir, readFile, writeFile } from \"node:fs/promises\"\n\nimport type { Command } from \"commander\"\nimport { basenam"
},
{
"path": "apps/cli/src/commands/search.ts",
"chars": 3655,
"preview": "import type { Command } from \"commander\"\n\nimport { parsePositiveInt, parseView, viewHelp } from \"../args\"\nimport { runCo"
},
{
"path": "apps/cli/src/commands/subscription.ts",
"chars": 6231,
"preview": "import type {\n SubscriptionCreateRequest,\n SubscriptionDeleteRequest,\n SubscriptionUpdateRequest,\n} from \"@follow-app"
},
{
"path": "apps/cli/src/commands/timeline.ts",
"chars": 2852,
"preview": "import type { EntryListRequest } from \"@follow-app/client-sdk\"\nimport type { Command } from \"commander\"\n\nimport { parseI"
},
{
"path": "apps/cli/src/commands/unread.ts",
"chars": 3399,
"preview": "import type {\n InboxSubscriptionResponse,\n ListSubscriptionResponse,\n SubscriptionWithFeed,\n} from \"@follow-app/clien"
},
{
"path": "apps/cli/src/config.ts",
"chars": 1750,
"preview": "import { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\n\nimport { join } from \"p"
},
{
"path": "apps/cli/src/index.ts",
"chars": 2155,
"preview": "import { Command } from \"commander\"\n\nimport packageJSON from \"../package.json\"\nimport { parseFormat } from \"./args\"\nimpo"
},
{
"path": "apps/cli/src/output.test.ts",
"chars": 2073,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\"\n\nimport { CLIError, normalizeError, printFailur"
},
{
"path": "apps/cli/src/output.ts",
"chars": 4412,
"preview": "import { inspect } from \"node:util\"\n\nimport { FollowAPIError, FollowAuthError } from \"@follow-app/client-sdk\"\n\nexport ty"
},
{
"path": "apps/cli/tsconfig.json",
"chars": 280,
"preview": "{\n \"extends\": \"@follow/configs/tsconfig.extend.json\",\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESN"
},
{
"path": "apps/cli/tsup.config.ts",
"chars": 287,
"preview": "import { defineConfig } from \"tsup\"\n\nexport default defineConfig({\n entry: [\"src/index.ts\"],\n format: [\"esm\"],\n targe"
},
{
"path": "apps/cli/vitest.config.ts",
"chars": 121,
"preview": "import { defineProject } from \"vitest/config\"\n\nexport default defineProject({\n test: {\n environment: \"node\",\n },\n})"
},
{
"path": "apps/desktop/.env.example",
"chars": 256,
"preview": "VITE_WEB_URL=http://localhost:5173\nVITE_API_URL=http://localhost:3000\nVITE_IMGPROXY_URL=http://localhost:2873\nVITE_SENTR"
},
{
"path": "apps/desktop/AGENTS.md",
"chars": 9099,
"preview": "# AGENTS.md\n\nThis file provides specific guidance for developing the Electron desktop application.\n\n## Architecture\n\n- *"
},
{
"path": "apps/desktop/build/appxmanifest-template.xml",
"chars": 1741,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Package\n xmlns=\"http://schemas.microsoft.com/appx/manifest/foundation/windows1"
},
{
"path": "apps/desktop/build/entitlements.mac.plist",
"chars": 251,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apps/desktop/build/entitlements.mas.child.plist",
"chars": 304,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apps/desktop/build/entitlements.mas.plist",
"chars": 460,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "apps/desktop/bump.config.ts",
"chars": 845,
"preview": "/* eslint-disable no-template-curly-in-string */\nimport { defineConfig } from \"nbump\"\n\nexport default defineConfig({\n l"
},
{
"path": "apps/desktop/bump.hotfix.config.js",
"chars": 585,
"preview": "/* eslint-disable no-template-curly-in-string */\nimport { execSync } from \"node:child_process\"\n\nimport { defineConfig } "
},
{
"path": "apps/desktop/changelog/0.1.2.md",
"chars": 688,
"preview": "# What's new in v0.1.2\n\n## Features\n\n- Custom CSS is now supported, so you can add any CSS style and apply it to the Ent"
},
{
"path": "apps/desktop/changelog/0.2.0.md",
"chars": 1114,
"preview": "# What's new in v0.2.0\n\n## New Features\n\n- Feed owners can now reset their feeds.\n- Image Gallery: Clicking on the image"
},
{
"path": "apps/desktop/changelog/0.2.1.md",
"chars": 412,
"preview": "# What's new in v0.2.1\n\n## New Features\n\n- Added zoom functionality for viewing pictures.\n- Set `Show Unread Only` as th"
},
{
"path": "apps/desktop/changelog/0.2.2.md",
"chars": 232,
"preview": "# What's new in v0.2.2\n\n## New Features\n\n- And Or conditions for actions\n- Add achievement badge\n\n## Improvements\n\n- ele"
},
{
"path": "apps/desktop/changelog/0.2.3.md",
"chars": 287,
"preview": "# What's new in v0.2.3\n\n## New Features\n\n- Hold shift to quickly select multiple Feeds\n- Collect entry from List\n\n## Imp"
},
{
"path": "apps/desktop/changelog/0.2.4.md",
"chars": 353,
"preview": "# What's new in v0.2.4-web\n\n## New Features\n\n🎉 HUGE NEWS! Follow finally goes mobile!\n\nEver wished you could Follow your"
},
{
"path": "apps/desktop/changelog/0.2.5.md",
"chars": 279,
"preview": "# What's new in v0.2.5\n\n## New Features\n\n- Customizable columns for masonry view\n- Manually trigger AI summary or transl"
},
{
"path": "apps/desktop/changelog/0.2.6.md",
"chars": 349,
"preview": "# What's new in v0.2.6\n\nWe've made some ux optimizations:\n\n- [Mobile]: Now when returning to the list from the entry det"
},
{
"path": "apps/desktop/changelog/0.2.7.md",
"chars": 797,
"preview": "# What's new in v0.2.7\n\n## New Features\n\n1. New `Move to Category` operation in feed subscription context menu\n\n for login and large transac"
},
{
"path": "apps/desktop/changelog/0.3.3.md",
"chars": 284,
"preview": "# What's new in v0.3.3\n\n## New Features\n\n## Improvements\n\n- Merge actions for toggling state\n- Action supports matching "
},
{
"path": "apps/desktop/changelog/0.3.4.md",
"chars": 630,
"preview": "# What's new in v0.3.4\n\n## New Features\n\n- The audio and notification views have been merged into the article view, bein"
},
{
"path": "apps/desktop/changelog/0.3.5.md",
"chars": 111,
"preview": "# What's new in v0.3.5\n\n## New Features\n\n- Restore audio and notification views\n\n## Improvements\n\n## Bug Fixes\n"
},
{
"path": "apps/desktop/changelog/0.3.6.md",
"chars": 285,
"preview": "# What's new in v0.3.6\n\n## New Features\n\n- Added a quick selector to the timeline column.\n\n\n- Display estimate"
},
{
"path": "apps/desktop/changelog/0.4.1.md",
"chars": 987,
"preview": "# What's New in v0.4.1\n\n## New Features\n\n- Added a \"Mark Above as Read\" button at the bottom of the entry list (88963a9)"
},
{
"path": "apps/desktop/changelog/0.4.2.md",
"chars": 1269,
"preview": "# What's New in v0.4.2\n\n## Shiny new things\n\n- Added Cubox integration (#3385)\n- Included document links on the actions "
},
{
"path": "apps/desktop/changelog/0.4.3.md",
"chars": 1060,
"preview": "# What's New in v0.4.3\n\n## Shiny new things\n\n- Added server‑side readability with AI‑powered summaries and translation s"
},
{
"path": "apps/desktop/changelog/0.4.4.md",
"chars": 806,
"preview": "# What's new in v0.4.4\n\n## Shiny new things\n\n- Added status action that allows you to send notifications or trigger webh"
},
{
"path": "apps/desktop/changelog/0.4.5.md",
"chars": 1483,
"preview": "# What's new in v0.4.5\n\n## Shiny new things\n\n- Translation view toggle – choose between “translation-only” or bilingual "
},
{
"path": "apps/desktop/changelog/0.4.6.md",
"chars": 1377,
"preview": "# What's new in v0.4.6\n\n## Shiny new things\n\n- Hide read subscriptions. You can now hide subscriptions with no unread it"
},
{
"path": "apps/desktop/changelog/0.4.8.md",
"chars": 1453,
"preview": "# What's new in v0.4.8\n\n## Shiny new things\n\n- Keyboard Shortcuts – a full-featured command system covering global actio"
},
{
"path": "apps/desktop/changelog/0.5.0.md",
"chars": 1495,
"preview": "# What's New in v0.5.0\n\n## Shiny new things\n\n- Double-clicking the draggable edge on either side of the entry list reset"
},
{
"path": "apps/desktop/changelog/0.6.0.md",
"chars": 57,
"preview": "# What's New in v0.6.0\n\nThis version has been withdrawn.\n"
},
{
"path": "apps/desktop/changelog/0.6.1.md",
"chars": 1260,
"preview": "# What's New in v0.6.1\n\n## Shiny New Things\n\n- Import and export your Actions (394d00f)\n- Add a bio, website, and social"
},
{
"path": "apps/desktop/changelog/0.6.2.md",
"chars": 1521,
"preview": "# What's new in v0.6.2\n\n## Shiny new things\n\n- New “Fade Read Items” option dims read entries in the timeline (eeeea47)\n"
},
{
"path": "apps/desktop/changelog/0.6.3.md",
"chars": 1576,
"preview": "# What's new in v0.6.3\n\n## Shiny new things\n\n- Hide From Timeline option for subscriptions to make your timeline cleaner"
},
{
"path": "apps/desktop/changelog/0.7.0.md",
"chars": 350,
"preview": "# What's new in v0.7.0\n\n## Shiny new things\n\n- Add custom integration configurations to adapt to more apps\n\n## Improveme"
},
{
"path": "apps/desktop/changelog/0.8.0.md",
"chars": 409,
"preview": "# What's new in v0.8.0\n\n## Shiny new things\n\n- Smart onboarding that gets you started in seconds.\n- One place for all yo"
},
{
"path": "apps/desktop/changelog/0.9.0.md",
"chars": 433,
"preview": "# What's new in v0.9.0\n\n## Improvements\n\n- Removed the limit on the maximum number of views (b69a935)\n- Allow hiding the"
},
{
"path": "apps/desktop/changelog/1.0.0.md",
"chars": 566,
"preview": "# What's new in v1.0.0\n\n## Shiny new things\n\n- 🌟 **Folo is now the AI Reader** — a smarter way to follow everything.\n\n##"
},
{
"path": "apps/desktop/changelog/1.1.0.md",
"chars": 1109,
"preview": "# What's new in v1.1.0\n\n## Shiny new things\n\n- Reintroduced the clean, efficient three-column layout for a smoother read"
},
{
"path": "apps/desktop/changelog/1.2.2.md",
"chars": 1435,
"preview": "# What's new in v1.2\n\n## Shiny new things\n\n- **AI**\n - Added **BYOK (Bring Your Own Key)** support — choose your own AI"
},
{
"path": "apps/desktop/changelog/1.2.6.md",
"chars": 421,
"preview": "# What's new in v1.2.6\n\n## Shiny new things\n\n- Optimize Stripe subscription management UI with dynamic billing portal an"
},
{
"path": "apps/desktop/changelog/1.3.0.md",
"chars": 416,
"preview": "# What's new in v1.3.0\n\n## Shiny new things\n\n- Markdown link supports open share feed link directly in the app\n\n## No lo"
},
{
"path": "apps/desktop/changelog/1.3.1.md",
"chars": 476,
"preview": "# What's new in v1.3.1\n\n## Shiny new things\n\n- Supported French localization.\n\n## Improvements\n\n- Migrated error trackin"
},
{
"path": "apps/desktop/changelog/1.4.0.md",
"chars": 432,
"preview": "# What's new in v1.4.0\n\n## Shiny new things\n\n- Added in-app review prompts\n\n## Improvements\n\n- Expanded desktop end-to-e"
},
{
"path": "apps/desktop/changelog/next.md",
"chars": 177,
"preview": "# What's new in vNEXT_VERSION\n\n## Shiny new things\n\n## Improvements\n\n## No longer broken\n\n## Thanks\n\nSpecial thanks to v"
},
{
"path": "apps/desktop/changelog/next.template.md",
"chars": 177,
"preview": "# What's new in vNEXT_VERSION\n\n## Shiny new things\n\n## Improvements\n\n## No longer broken\n\n## Thanks\n\nSpecial thanks to v"
},
{
"path": "apps/desktop/configs/vite.electron-render.config.ts",
"chars": 1648,
"preview": "import { fileURLToPath } from \"node:url\"\n\nimport { dirname, resolve } from \"pathe\"\nimport type { UserConfig } from \"vite"
},
{
"path": "apps/desktop/configs/vite.render.config.ts",
"chars": 2862,
"preview": "import { readFileSync } from \"node:fs\"\nimport { fileURLToPath } from \"node:url\"\n\nimport react from \"@vitejs/plugin-react"
},
{
"path": "apps/desktop/dev-only/dev-app-update.yml",
"chars": 33,
"preview": "provider: custom\nchannel: latest\n"
},
{
"path": "apps/desktop/e2e/playwright.config.ts",
"chars": 1331,
"preview": "import { defineConfig, devices } from \"@playwright/test\"\n\nimport { resolveDesktopE2EEnv } from \"./support/env\"\n\nconst en"
},
{
"path": "apps/desktop/e2e/scripts/capture-ui-audit.ts",
"chars": 4722,
"preview": "import { mkdir } from \"node:fs/promises\"\n\nimport { chromium } from \"@playwright/test\"\nimport { join } from \"pathe\"\n\nimpo"
},
{
"path": "apps/desktop/e2e/support/account.ts",
"chars": 1119,
"preview": "import type { Page } from \"@playwright/test\"\n\nimport type { DesktopE2EEnv } from \"./env\"\n\nexport interface TestAccount {"
},
{
"path": "apps/desktop/e2e/support/app.ts",
"chars": 20302,
"preview": "import type { Locator, Page } from \"@playwright/test\"\nimport { expect } from \"@playwright/test\"\n\nimport type { TestAccou"
},
{
"path": "apps/desktop/e2e/support/auth-bootstrap.ts",
"chars": 5755,
"preview": "import type { BrowserContext, Page } from \"@playwright/test\"\nimport { nanoid } from \"nanoid\"\n\nimport type { TestAccount "
},
{
"path": "apps/desktop/e2e/support/electron.ts",
"chars": 2556,
"preview": "import { execSync } from \"node:child_process\"\nimport { mkdtemp, rm } from \"node:fs/promises\"\nimport { tmpdir } from \"nod"
},
{
"path": "apps/desktop/e2e/support/env.ts",
"chars": 2360,
"preview": "import { fileURLToPath } from \"node:url\"\n\nimport { join } from \"pathe\"\n\nexport type DesktopE2EProfile = \"local\" | \"prod\""
},
{
"path": "apps/desktop/e2e/tests/electron/core.spec.ts",
"chars": 2091,
"preview": "import { expect, test } from \"@playwright/test\"\n\nimport { createTestAccount, tryDeleteCurrentUser } from \"../../support/"
},
{
"path": "apps/desktop/e2e/tests/web/core.spec.ts",
"chars": 2583,
"preview": "import { expect, test } from \"@playwright/test\"\n\nimport { createTestAccount, tryDeleteCurrentUser } from \"../../support/"
},
{
"path": "apps/desktop/e2e/tests/web/settings-sync.spec.ts",
"chars": 2790,
"preview": "import type { BrowserContext } from \"@playwright/test\"\nimport { expect, test } from \"@playwright/test\"\n\nimport { createT"
},
{
"path": "apps/desktop/electron.vite.config.ts",
"chars": 1061,
"preview": "import { defineConfig } from \"electron-vite\"\nimport { resolve } from \"pathe\"\n\nimport { getGitHash } from \"../../scripts/"
},
{
"path": "apps/desktop/forge.config.cts",
"chars": 10199,
"preview": "import crypto from \"node:crypto\"\nimport fs, { readdirSync } from \"node:fs\"\nimport { cp, readdir } from \"node:fs/promises"
},
{
"path": "apps/desktop/layer/main/export.ts",
"chars": 81,
"preview": "// Export types for renderer to use\nexport type { IpcServices } from \"./src/ipc\"\n"
},
{
"path": "apps/desktop/layer/main/global.d.ts",
"chars": 88,
"preview": "import \"../../types/vite\"\n\ndeclare global {\n const GIT_COMMIT_HASH: string\n}\nexport {}\n"
},
{
"path": "apps/desktop/layer/main/package.json",
"chars": 1714,
"preview": "{\n \"name\": \"@follow/electron-main\",\n \"type\": \"module\",\n \"private\": true,\n \"author\": \"Folo Team\",\n \"license\": \"GPL-3"
},
{
"path": "apps/desktop/layer/main/preload/index.d.ts",
"chars": 200,
"preview": "import type { ElectronAPI } from \"@electron-toolkit/preload\"\n\ndeclare global {\n interface Window {\n electron?: Elect"
},
{
"path": "apps/desktop/layer/main/preload/index.ts",
"chars": 1744,
"preview": "import os from \"node:os\"\nimport { platform } from \"node:process\"\n\nimport { electronAPI } from \"@electron-toolkit/preload"
},
{
"path": "apps/desktop/layer/main/src/@types/constants.ts",
"chars": 267,
"preview": "const langs = [\"en\", \"zh-CN\", \"zh-TW\", \"ja\"] as const\nexport const currentSupportedLanguages = [...langs].sort() as stri"
},
{
"path": "apps/desktop/layer/main/src/@types/i18next.d.ts",
"chars": 462,
"preview": "import type { defaultNS, ns } from \"./constants\"\nimport type { resources } from \"./resources\"\n\ndeclare module \"i18next\" "
},
{
"path": "apps/desktop/layer/main/src/@types/resources.ts",
"chars": 492,
"preview": "import en from \"@locales/native/en.json\"\nimport ja from \"@locales/native/ja.json\"\nimport zhCn from \"@locales/native/zh-C"
},
{
"path": "apps/desktop/layer/main/src/before-bootstrap.ts",
"chars": 490,
"preview": "import { app, protocol } from \"electron\"\nimport path from \"pathe\"\n\nconst e2eUserDataDir = process.env.FOLO_E2E_USER_DATA"
},
{
"path": "apps/desktop/layer/main/src/bootstrap.ts",
"chars": 300,
"preview": "import { app } from \"electron\"\nimport squirrelStartup from \"electron-squirrel-startup\"\n\nimport { DEVICE_ID } from \"./con"
},
{
"path": "apps/desktop/layer/main/src/constants/app.ts",
"chars": 530,
"preview": "import { app } from \"electron\"\nimport path from \"pathe\"\n\nexport const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5"
},
{
"path": "apps/desktop/layer/main/src/constants/system.ts",
"chars": 90,
"preview": "import { machineIdSync } from \"node-machine-id\"\n\nexport const DEVICE_ID = machineIdSync()\n"
},
{
"path": "apps/desktop/layer/main/src/env.ts",
"chars": 850,
"preview": "import os from \"node:os\"\n\nimport { DEV } from \"@follow/shared/constants\"\n\nexport const channel: \"development\" | \"beta\" |"
},
{
"path": "apps/desktop/layer/main/src/helper.ts",
"chars": 1107,
"preview": "import { fileURLToPath, pathToFileURL } from \"node:url\"\n\nimport { MODE, ModeEnum } from \"@follow/shared/constants\"\nimpor"
},
{
"path": "apps/desktop/layer/main/src/index.ts",
"chars": 49,
"preview": "import \"./before-bootstrap\"\nimport \"./bootstrap\"\n"
},
{
"path": "apps/desktop/layer/main/src/ipc/index.ts",
"chars": 1115,
"preview": "import type { MergeIpcService } from \"electron-ipc-decorator\"\nimport { createServices } from \"electron-ipc-decorator\"\n\ni"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/app.ts",
"chars": 7019,
"preview": "import fsp from \"node:fs/promises\"\nimport { fileURLToPath } from \"node:url\"\n\nimport { callWindowExpose } from \"@follow/s"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/auth.ts",
"chars": 5689,
"preview": "import { env } from \"@follow/shared/env.desktop\"\nimport { createDesktopAPIHeaders } from \"@follow/utils/headers\"\nimport "
},
{
"path": "apps/desktop/layer/main/src/ipc/services/cli.ts",
"chars": 2218,
"preview": "import type { IpcContext } from \"electron-ipc-decorator\"\nimport { IpcMethod, IpcService } from \"electron-ipc-decorator\"\n"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/debug.ts",
"chars": 422,
"preview": "import type { IpcContext } from \"electron-ipc-decorator\"\nimport { IpcMethod, IpcService } from \"electron-ipc-decorator\"\n"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/dock.ts",
"chars": 2367,
"preview": "import type { IpcContext } from \"electron-ipc-decorator\"\nimport { IpcMethod, IpcService } from \"electron-ipc-decorator\"\n"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/integration.ts",
"chars": 11582,
"preview": "import { existsSync } from \"node:fs\"\nimport fsp from \"node:fs/promises\"\n\nimport { shell } from \"electron\"\nimport type { "
},
{
"path": "apps/desktop/layer/main/src/ipc/services/menu.ts",
"chars": 2141,
"preview": "import type { MenuItemConstructorOptions, MessageBoxOptions } from \"electron\"\nimport { dialog, Menu, ShareMenu } from \"e"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/reader.ts",
"chars": 3367,
"preview": "import fs from \"node:fs\"\n\nimport { callWindowExpose } from \"@follow/shared/bridge\"\nimport { readability } from \"@follow-"
},
{
"path": "apps/desktop/layer/main/src/ipc/services/setting.ts",
"chars": 2239,
"preview": "import { createRequire } from \"node:module\"\n\nimport { app, nativeTheme } from \"electron\"\nimport type { IpcContext } from"
},
{
"path": "apps/desktop/layer/main/src/lib/api-client.ts",
"chars": 2628,
"preview": "import { env } from \"@follow/shared/env.desktop\"\nimport { createDesktopAPIHeaders } from \"@follow/utils/headers\"\nimport "
},
{
"path": "apps/desktop/layer/main/src/lib/auth-cookie-migration.ts",
"chars": 2454,
"preview": "import type { Cookie, CookiesSetDetails, Session } from \"electron\"\n\nimport { BETTER_AUTH_COOKIE_NAME_SESSION_TOKEN } fro"
},
{
"path": "apps/desktop/layer/main/src/lib/cleaner.ts",
"chars": 4874,
"preview": "import { statSync } from \"node:fs\"\nimport fsp from \"node:fs/promises\"\n\nimport { callWindowExpose } from \"@follow/shared/"
},
{
"path": "apps/desktop/layer/main/src/lib/cli-session-sync.ts",
"chars": 3435,
"preview": "import { execFile } from \"node:child_process\"\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\"\nimport { hom"
},
{
"path": "apps/desktop/layer/main/src/lib/dock.ts",
"chars": 488,
"preview": "import { app } from \"electron\"\n\nimport { isWindows } from \"../env\"\n\nexport const setDockCount = (input: number) => {\n /"
},
{
"path": "apps/desktop/layer/main/src/lib/download.ts",
"chars": 3751,
"preview": "import { createHash } from \"node:crypto\"\nimport { createWriteStream } from \"node:fs\"\nimport { mkdir } from \"node:fs/prom"
},
{
"path": "apps/desktop/layer/main/src/lib/i18n.ts",
"chars": 320,
"preview": "import i18next from \"i18next\"\n\nimport { resources } from \"../@types/resources\"\n\nexport const defaultNS = \"native\"\n\nexpor"
},
{
"path": "apps/desktop/layer/main/src/lib/proxy.test.ts",
"chars": 2936,
"preview": "import { session } from \"electron\"\nimport type { Mock } from \"vitest\"\nimport { beforeEach, describe, expect, it, vi } fr"
},
{
"path": "apps/desktop/layer/main/src/lib/proxy.ts",
"chars": 3265,
"preview": "import { session } from \"electron\"\nimport { ProxyAgent, setGlobalDispatcher } from \"undici\"\n\nimport { logger } from \"../"
},
{
"path": "apps/desktop/layer/main/src/lib/router.ts",
"chars": 3496,
"preview": "import { callWindowExpose } from \"@follow/shared/bridge\"\nimport { extractElectronWindowOptions } from \"@follow/shared/el"
},
{
"path": "apps/desktop/layer/main/src/lib/store.ts",
"chars": 655,
"preview": "import type { Credentials } from \"@eneris/push-receiver/dist/types\"\nimport Store from \"electron-store\"\n\n// @keep-sorted\n"
},
{
"path": "apps/desktop/layer/main/src/lib/tray.ts",
"chars": 3110,
"preview": "import { name } from \"@pkg\"\nimport { app, Menu, nativeImage, Tray } from \"electron\"\n\nimport { isMacOS, isMAS, isWindows "
},
{
"path": "apps/desktop/layer/main/src/lib/user.ts",
"chars": 1092,
"preview": "import type { Credentials } from \"@eneris/push-receiver/dist/types\"\n\nimport { isLinux, isMacOS, isWindows } from \"~/env\""
},
{
"path": "apps/desktop/layer/main/src/lib/utils.ts",
"chars": 653,
"preview": "import type { BrowserWindow } from \"electron\"\n\nexport const sleep = (ms: number) => new Promise((resolve) => setTimeout("
},
{
"path": "apps/desktop/layer/main/src/logger.ts",
"chars": 497,
"preview": "import { app, shell } from \"electron\"\nimport log from \"electron-log\"\n\nexport const logger = log.scope(\"main\")\nlog.initia"
},
{
"path": "apps/desktop/layer/main/src/manager/app.ts",
"chars": 7523,
"preview": "import { PushReceiver } from \"@eneris/push-receiver\"\nimport { callWindowExpose } from \"@follow/shared/bridge\"\nimport { A"
},
{
"path": "apps/desktop/layer/main/src/manager/bootstrap.ts",
"chars": 7116,
"preview": "import { rmSync } from \"node:fs\"\n\nimport { electronApp, optimizer } from \"@electron-toolkit/utils\"\nimport { callWindowEx"
},
{
"path": "apps/desktop/layer/main/src/manager/lifecycle.ts",
"chars": 1049,
"preview": "import { app } from \"electron\"\n\nimport { WindowManager } from \"~/manager/window\"\n\nclass LifecycleManagerStatic {\n priva"
},
{
"path": "apps/desktop/layer/main/src/manager/window.ts",
"chars": 14884,
"preview": "import { fileURLToPath } from \"node:url\"\n\nimport { is } from \"@electron-toolkit/utils\"\nimport { LEGACY_APP_PROTOCOL } fr"
},
{
"path": "apps/desktop/layer/main/src/menu.ts",
"chars": 6512,
"preview": "import { callWindowExpose } from \"@follow/shared/bridge\"\nimport { DEV } from \"@follow/shared/constants\"\nimport { dispatc"
},
{
"path": "apps/desktop/layer/main/src/modules/language-detection/index.ts",
"chars": 3784,
"preview": "// @see https://github.dev/microsoft/vscode/blob/main/src/vs/workbench/services/languageDetection/browser/languageDetect"
},
{
"path": "apps/desktop/layer/main/src/shims/utf-8-validate.cjs",
"chars": 294,
"preview": "\"use strict\"\n\nconst { isUtf8 } = require(\"node:buffer\")\n\nmodule.exports = function isValidUTF8(buffer) {\n if (typeof is"
},
{
"path": "apps/desktop/layer/main/src/updater/api.ts",
"chars": 816,
"preview": "import type {\n DistributionStatusPayload,\n GetLatestReleaseQuery,\n LatestReleasePayload,\n} from \"@follow-app/client-s"
}
]
// ... and 2669 more files (download for full content)
About this extraction
This page contains the full source code of the RSSNext/Folo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 2869 files (8.2 MB), approximately 2.3M tokens, and a symbol index with 3942 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.