(undefined)
5 | useEffect(() => {
6 | ref.current = value
7 | })
8 | return ref.current
9 | }
10 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 |
3 | CHANGELOG.md
4 |
5 | apps/external/postcss.config.cjs
6 |
7 | apps/mobile/android
8 | apps/mobile/ios
9 | apps/mobile/native/example
10 |
11 | apps/mobile/native/android
12 | apps/mobile/native/ios
13 | apps/mobile/.expo
14 |
15 | generated-routes.ts
16 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/ui/modal/stacked/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./context"
2 | export * from "./helper"
3 | // NOTE: This one can easily cause a circular dependency
4 | // export * from "./hooks"
5 | export * from "./modal"
6 | export * from "./provider"
7 | export * from "./types"
8 |
--------------------------------------------------------------------------------
/apps/mobile/global.d.ts:
--------------------------------------------------------------------------------
1 | import type { DOMProps } from "expo/dom"
2 | import type { FC } from "react"
3 | import type WebView from "react-native-webview"
4 |
5 | declare global {
6 | export type WebComponent = FC
>
7 | }
8 | export {}
9 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/skeleton/index.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@follow/utils/utils"
2 |
3 | export function Skeleton({ className, ...props }: React.HTMLAttributes) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/packages/internal/hooks/src/useOnce.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react"
2 |
3 | export const useOnce = (fn: () => any) => {
4 | const isDone = useRef(false)
5 | useEffect(() => {
6 | if (isDone.current) return
7 | fn()
8 | isDone.current = true
9 | }, [])
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.3.7.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.3.7
2 |
3 | ## New Features
4 |
5 | - Revert to the previous list display styles.
6 | 
7 | - The entries of the list is now displayed directly in the timeline.
8 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/queries/_.ts:
--------------------------------------------------------------------------------
1 | export { auth } from "./auth"
2 | export { discover } from "./discover"
3 | export { entries } from "./entries"
4 | export { feed } from "./feed"
5 | export { invitations } from "./invitations"
6 | export { rsshub } from "./rsshub"
7 | export { wallet } from "./wallet"
8 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerOption.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum ImageViewerOption {
4 |
5 | case contentMode(UIView.ContentMode)
6 | case onPreview((Int) -> Void)
7 | case onClosePreview(() -> Void)
8 | case onIndexChange((Int) -> Void)
9 | }
10 |
--------------------------------------------------------------------------------
/packages/internal/shared/src/global.d.ts:
--------------------------------------------------------------------------------
1 | import type { ElectronAPI } from "@electron-toolkit/preload"
2 |
3 | declare global {
4 | interface Window {
5 | electron?: ElectronAPI
6 | api?: { canWindowBlur: boolean }
7 | }
8 |
9 | export const ELECTRON: boolean
10 | }
11 |
12 | export {}
13 |
--------------------------------------------------------------------------------
/packages/internal/store/src/@types/i18next.d.ts:
--------------------------------------------------------------------------------
1 | import type { defaultResources as resources } from "./default-resource"
2 |
3 | declare module "i18next" {
4 | interface CustomTypeOptions {
5 | ns: ["settings"]
6 | resources: (typeof resources)["en"]
7 | defaultNS: "settings"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.2.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.2
2 |
3 | ## New Features
4 |
5 | - And Or conditions for actions
6 | - Add achievement badge
7 |
8 | ## Improvements
9 |
10 | - electron: Prompt when opening an external link whether to open the app
11 | - Improve the smoothness of some animations
12 |
--------------------------------------------------------------------------------
/apps/mobile/native/.npmignore:
--------------------------------------------------------------------------------
1 | # Exclude all top-level hidden directories by convention
2 | /.*/
3 |
4 | # Exclude tarballs generated by `npm pack`
5 | /*.tgz
6 |
7 | __mocks__
8 | __tests__
9 |
10 | /babel.config.js
11 | /android/src/androidTest/
12 | /android/src/test/
13 | /android/build/
14 | /example/
15 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Controllers/RNSViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RNSViewController.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/2/27.
6 | //
7 |
8 | import UIKit
9 |
10 | class RNSViewController: UIViewController {
11 | @objc public let screenView: UIView? = nil
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/apps/desktop/build/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AnalyticsMetrics"
2 | export * from "./CategoryTag"
3 | export * from "./DisplayHeader"
4 | export * from "./EmptyState"
5 | export * from "./FeedItemCard"
6 | export * from "./GroupedContent"
7 | export * from "./StatCard"
8 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/z-index/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { ZIndexContext } from "./ctx"
4 |
5 | export const ZIndexProvider: Component<{
6 | zIndex: number
7 | }> = (props) => {
8 | return {props.children}
9 | }
10 |
--------------------------------------------------------------------------------
/packages/readability/bump.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "nbump"
2 |
3 | export default defineConfig({
4 | leading: ["npm run build"],
5 | tag: false,
6 | push: false,
7 | commit: false,
8 | allowDirty: true,
9 | changelog: false,
10 | publish: true,
11 | allowedBranches: ["dev"],
12 | })
13 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./AIEntryHeader"
2 | export * from "./EntryHeader"
3 | export * from "./internal/context"
4 | export * from "./internal/EntryHeaderActionsContainer"
5 | export * from "./internal/EntryHeaderMeta"
6 | export * from "./types"
7 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Folo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/packages/internal/components/src/hooks/useMobile.ts:
--------------------------------------------------------------------------------
1 | import { useViewport } from "./useViewport"
2 |
3 | export const useMobile = () => {
4 | return useViewport((v) => v.w < 1024 && v.w !== 0)
5 | }
6 |
7 | export const isMobile = () => {
8 | const w = window.innerWidth
9 | return w < 1024 && w !== 0
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/main/src/@types/constants.ts:
--------------------------------------------------------------------------------
1 | const langs = ["en", "zh-CN", "zh-TW", "ja"] as const
2 | export const currentSupportedLanguages = [...langs].sort() as string[]
3 | export type MainSupportedLanguages = (typeof langs)[number]
4 |
5 | export const ns = ["native"] as const
6 | export const defaultNS = "native" as const
7 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/pages/settings/index.tsx:
--------------------------------------------------------------------------------
1 | import { nextFrame } from "@follow/utils/dom"
2 | import { useLayoutEffect } from "react"
3 |
4 | export const Component = () => {
5 | useLayoutEffect(() => {
6 | nextFrame(() => window.router.navigate("/settings/general"))
7 | }, [])
8 | return null
9 | }
10 |
--------------------------------------------------------------------------------
/apps/ssr/api/index.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | // eslint-disable-next-line antfu/no-import-dist
3 | import { createApp } from "../dist/server/index.mjs"
4 |
5 | export default async function handler(req: any, res: any) {
6 | const app = await createApp()
7 | await app.ready()
8 | app.server.emit("request", req, res)
9 | }
10 |
--------------------------------------------------------------------------------
/icons/mgc/left_small_sharp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/portal/provider.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, use } from "react"
2 |
3 | export const useRootPortal = () => {
4 | const ctx = use(RootPortalContext)
5 |
6 | return ctx || document.body
7 | }
8 |
9 | export const RootPortalContext = createContext(undefined)
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/ui/markdown/renderers/ctx.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, use } from "react"
2 |
3 | /**
4 | * @internal
5 | */
6 | export const IsInParagraphContext = createContext(false)
7 |
8 | export const useIsInParagraphContext = () => {
9 | return use(IsInParagraphContext)
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/store/slices/index.ts:
--------------------------------------------------------------------------------
1 | export type { ChatSlice } from "../chat-core/types"
2 | export {
3 | type BlockSlice as ContextSlice,
4 | createBlockSlice as createContextSlice,
5 | } from "./block.slice"
6 | export { createChatSlice } from "./chat.slice"
7 | export { type ChatStatus } from "ai"
8 |
--------------------------------------------------------------------------------
/icons/mgc/right_small_sharp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/segment/ctx.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "use-context-selector"
2 |
3 | export interface SegmentGroupContextValue {
4 | value: string
5 | setValue: (value: string) => void
6 | componentId: string
7 | }
8 | export const SegmentGroupContext = createContext(null!)
9 |
--------------------------------------------------------------------------------
/packages/internal/utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "declaration": false,
6 | "types": ["@follow/types/global", "@follow/types/vite"],
7 | "paths": {
8 | "@follow/utils/*": ["./src/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/plugins/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/apps/mobile/src/initialize/device.ts:
--------------------------------------------------------------------------------
1 | import { getDeviceTypeAsync } from "expo-device"
2 |
3 | import { appAtoms } from "../atoms/app"
4 | import { jotaiStore } from "../lib/jotai"
5 |
6 | export async function initDeviceType() {
7 | const type = await getDeviceTypeAsync()
8 | jotaiStore.set(appAtoms.deviceType, type)
9 | }
10 |
--------------------------------------------------------------------------------
/apps/ssr/client/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Outlet } from "react-router"
3 |
4 | import { RootProviders } from "./providers/root-providers"
5 |
6 | function App() {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export { App as Component }
15 |
--------------------------------------------------------------------------------
/apps/ssr/client/atoms/server-configs.ts:
--------------------------------------------------------------------------------
1 | import { createAtomHooks } from "@follow/utils/jotai"
2 | import type { StatusConfigs } from "@follow-app/client-sdk"
3 | import { atom } from "jotai"
4 |
5 | export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = createAtomHooks(
6 | atom>(null),
7 | )
8 |
--------------------------------------------------------------------------------
/apps/ssr/scripts/cleanup-vercel-build.ts:
--------------------------------------------------------------------------------
1 | import { rmSync } from "node:fs"
2 | import { fileURLToPath } from "node:url"
3 |
4 | import { dirname, resolve } from "pathe"
5 |
6 | const __dirname = dirname(fileURLToPath(import.meta.url))
7 | rmSync(resolve(__dirname, "../.generated"), { recursive: true, force: true })
8 | // restore env file
9 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/summary/getters.ts:
--------------------------------------------------------------------------------
1 | import type { SupportedActionLanguage } from "@follow/shared/language"
2 |
3 | import { useSummaryStore } from "./store"
4 |
5 | export const getSummary = (entryId: string, language: SupportedActionLanguage) => {
6 | return useSummaryStore.getState().data[entryId]?.[language]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/hooks/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useBizQuery"
2 | export * from "./useContextMenu"
3 | export * from "./useI18n"
4 | export * from "./useLoginModal"
5 | export * from "./usePreventOverscrollBounce"
6 | export * from "./useRecaptchaToken"
7 | export * from "./useRequireLogin"
8 | export * from "./useSyncTheme"
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/utils/positioning.ts:
--------------------------------------------------------------------------------
1 | import type { LexicalEditor } from "lexical"
2 |
3 | import { calculateDropdownPosition } from "../../shared/utils/positioning"
4 |
5 | export const calculateShortcutDropdownPosition = (editor: LexicalEditor) =>
6 | calculateDropdownPosition(editor)
7 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/index.ts:
--------------------------------------------------------------------------------
1 | export { ArticleLayout } from "./ArticleLayout"
2 | export { getEntryContentLayout } from "./factory"
3 | export { PicturesLayout } from "./PicturesLayout"
4 | export { SocialMediaLayout } from "./SocialMediaLayout"
5 | export { VideosLayout } from "./VideosLayout"
6 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Folo/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "filename": "App-Icon-1024x1024@1x.png",
5 | "idiom": "universal",
6 | "platform": "ios",
7 | "size": "1024x1024"
8 | }
9 | ],
10 | "info": {
11 | "version": 1,
12 | "author": "expo"
13 | }
14 | }
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/tabbar/ReactNativeTab.tsx:
--------------------------------------------------------------------------------
1 | import { TabBarPortal } from "@/src/lib/navigation/bottom-tab/TabBarPortal"
2 |
3 | import { BottomTabs } from "./BottomTabs"
4 |
5 | export const ReactNativeTab = () => {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/tabbar/contexts/BottomTabBarHeightContext.tsx:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from "react"
2 | import { createContext } from "react"
3 |
4 | export const BottomTabBarHeightContext = createContext(0)
5 | export const SetBottomTabBarHeightContext = createContext>>(null!)
6 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/user/types.ts:
--------------------------------------------------------------------------------
1 | import type { UserSchema } from "@follow/database/schemas/types"
2 |
3 | export interface UserProfileEditable {
4 | email: string
5 | name: string
6 | handle: string
7 | image: string
8 | bio?: string
9 | website?: string
10 | socialLinks?: UserSchema["socialLinks"]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/lib/client.ts:
--------------------------------------------------------------------------------
1 | import type { IpcServices } from "@follow/electron-main"
2 | import type { IpcRenderer } from "electron"
3 | import { createIpcProxy } from "electron-ipc-decorator/client"
4 |
5 | export const ipcServices = createIpcProxy(
6 | window.electron?.ipcRenderer as unknown as IpcRenderer,
7 | )
8 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/action/utils.ts:
--------------------------------------------------------------------------------
1 | export const generateExportFilename = () => {
2 | const now = new Date()
3 | const dateStr = now.toISOString().split("T")[0] // YYYY-MM-DD
4 | const timeStr = now.toTimeString().split(" ")[0]?.replaceAll(":", "-") // HH-MM-SS
5 | return `follow-actions-${dateStr}-${timeStr}.json`
6 | }
7 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/typography/MarkdownNative.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react"
2 |
3 | import { renderMarkdown } from "@/src/lib/markdown"
4 |
5 | export const MarkdownNative: WebComponent<{
6 | value: string
7 | }> = ({ value }) => {
8 | return useMemo(() => {
9 | return renderMarkdown(value)
10 | }, [value])
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/entry-list/EntryListContext.tsx:
--------------------------------------------------------------------------------
1 | import type { FeedViewType } from "@follow/constants"
2 | import { createContext, use } from "react"
3 |
4 | export const EntryListContextViewContext = createContext(null!)
5 |
6 | export const useEntryListContextView = () => {
7 | return use(EntryListContextViewContext)
8 | }
9 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/settings/routes/Feeds.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native"
2 |
3 | import { Text } from "@/src/components/ui/typography/Text"
4 |
5 | export const FeedsScreen = () => {
6 | return (
7 |
8 | Feeds Settings
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/packages/internal/logger/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "baseUrl": ".",
6 | "jsx": "preserve",
7 | "declaration": true,
8 | "paths": {
9 | "@pkg": ["../../package.json"]
10 | }
11 | },
12 | "include": ["**/*"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/internal/store/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { ModuleAPIs } from "@follow-app/client-sdk"
2 |
3 | export type GeneralMutationOptions = {
4 | onSuccess?: () => void
5 | onError?: (errorMessage: Error) => void
6 | }
7 |
8 | export type GeneralQueryOptions = {
9 | enabled?: boolean
10 | }
11 |
12 | export type FollowAPI = ModuleAPIs
13 |
--------------------------------------------------------------------------------
/apps/ssr/client/@types/i18next.d.ts:
--------------------------------------------------------------------------------
1 | import type { defaultNS, ns } from "./constants"
2 | import type { defaultResources as resources } from "./default-resource"
3 |
4 | declare module "i18next" {
5 | interface CustomTypeOptions {
6 | ns: typeof ns
7 | resources: (typeof resources)["en"]
8 | defaultNS: typeof defaultNS
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/ssr/client/pages/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { NotFound } from "@client/components/common/404"
2 | import * as React from "react"
3 | import { Outlet } from "react-router"
4 |
5 | export const Component = () => {
6 | if (document.documentElement.dataset.notFound === "true") {
7 | return
8 | }
9 | return
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const isWebBuild = !!process.env.WEB_BUILD || !!process.env.VERCEL
2 |
3 | module.exports = {
4 | plugins: {
5 | tailwindcss: {},
6 | "tailwindcss/nesting": {},
7 |
8 | ...(isWebBuild ? { autoprefixer: {} } : {}),
9 | ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface MediaModel {
2 | url: string
3 | type: "photo" | "video"
4 | preview_image_url?: string
5 | width?: number
6 | height?: number
7 | blurhash?: string
8 | }
9 |
10 | export interface EntryModel {
11 | content?: string
12 | title?: string
13 | media?: MediaModel[]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/internal/components/src/hooks/useMouse.ts:
--------------------------------------------------------------------------------
1 | import { jotaiStore } from "@follow/utils"
2 | import { useAtomValue } from "jotai"
3 |
4 | import { mouseAtom } from "../atoms/mouse"
5 |
6 | export const useMousePosition = () => {
7 | return useAtomValue(mouseAtom)
8 | }
9 |
10 | export const getMousePosition = () => jotaiStore.get(mouseAtom)
11 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/scroll-area/ctx.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react"
2 |
3 | export const ScrollElementContext = createContext(document.documentElement)
4 |
5 | export const ScrollElementEventsContext = createContext<{
6 | onUpdateMaxScroll?: () => void
7 | }>({
8 | onUpdateMaxScroll: undefined,
9 | })
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/errors/enum.ts:
--------------------------------------------------------------------------------
1 | export enum ErrorComponentType {
2 | Modal = "Modal",
3 | Page = "Page",
4 |
5 | // Feed
6 | FeedFoundCanBeFollow = "FeedFoundCanBeFollow",
7 | FeedNotFound = "FeedNotFound",
8 | // Section
9 | RSSHubDiscoverError = "RSSHubDiscoverError",
10 | EntryNotFound = "EntryNotFound",
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-column/Items/list-item.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@follow/components/ui/skeleton/index.js"
2 |
3 | export const ListItemSkeleton = (
4 |
5 |
6 |
7 | )
8 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/index.ts:
--------------------------------------------------------------------------------
1 | export { UserMemorySection } from "./memory/UserMemorySection"
2 | export { PanelStyleSection } from "./PanelStyleSection"
3 | export { PersonalizePromptSection } from "./PersonalizePromptSection"
4 | export { AIShortcutsSection } from "./shortcuts"
5 | export { UsageAnalysisSection } from "./usage"
6 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/views/NavigationHeaderContext.tsx:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from "react"
2 | import { createContext } from "react"
3 |
4 | export const NavigationHeaderHeightContext = createContext(null!)
5 | export const SetNavigationHeaderHeightContext = createContext>>(
6 | null!,
7 | )
8 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/list/types.ts:
--------------------------------------------------------------------------------
1 | import type { ListSchema } from "@follow/database/schemas/types"
2 |
3 | export type CreateListModel = Pick & {
4 | title: string
5 | }
6 |
7 | export type ListModel = Omit & {
8 | feedIds: string[]
9 | type: "list"
10 | }
11 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/translation/types.ts:
--------------------------------------------------------------------------------
1 | export const translationFields = ["title", "description", "content", "readabilityContent"] as const
2 | export type TranslationField = (typeof translationFields)[number]
3 | export type TranslationFieldArray = Array
4 | export type EntryTranslation = Record
5 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/constants/copy.ts:
--------------------------------------------------------------------------------
1 | import { IN_ELECTRON } from "@follow/shared/constants"
2 |
3 | const OpenInBrowser = (_t?: any) =>
4 | IN_ELECTRON
5 | ? tShortcuts("command.subscription.open_in_browser.title")
6 | : tShortcuts("command.subscription.open_in_tab.title")
7 |
8 | export const COPY_MAP = {
9 | OpenInBrowser,
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Shared props interface for all entry content layout components
3 | */
4 | export interface EntryLayoutProps {
5 | entryId: string
6 | compact?: boolean
7 | noMedia?: boolean
8 | translation?: {
9 | content?: string
10 | title?: string
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/home_5_cute_re.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "home_5_cute_re.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/settings/routes/Achievement.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native"
2 |
3 | import { Text } from "@/src/components/ui/typography/Text"
4 |
5 | export const AchievementScreen = () => {
6 | return (
7 |
8 | Achievement Screen
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/ssr/client/initialize/op.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@follow/shared/env.ssr"
2 | import { OpenPanel } from "@openpanel/web"
3 |
4 | export const op = new OpenPanel({
5 | clientId: env.VITE_OPENPANEL_CLIENT_ID ?? "",
6 | trackScreenViews: true,
7 | trackOutgoingLinks: true,
8 | trackAttributes: true,
9 | apiUrl: env.VITE_OPENPANEL_API_URL,
10 | })
11 |
--------------------------------------------------------------------------------
/apps/ssr/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme_color": "#ff5c00",
3 | "name": "Folo",
4 | "icons": [
5 | {
6 | "src": "/icon-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/icon-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/packages/internal/utils/src/ns.ts:
--------------------------------------------------------------------------------
1 | const ns = "follow"
2 | export const getStorageNS = (key: string) => `${ns}:${key}`
3 |
4 | export const clearStorage = () => {
5 | for (let i = 0; i < localStorage.length; i++) {
6 | const key = localStorage.key(i)
7 | if (key && key.startsWith(ns)) {
8 | localStorage.removeItem(key)
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.5.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.5
2 |
3 | ## New Features
4 |
5 | - Customizable columns for masonry view
6 | - Manually trigger AI summary or translation
7 |
8 | 
9 |
10 | ## Improvements
11 |
12 | ## Bug Fixes
13 |
14 | - Fixed some display and operation issues on mobile
15 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.3.6.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.3.6
2 |
3 | ## New Features
4 |
5 | - Added a quick selector to the timeline column.
6 |
7 | 
8 |
9 | ## Improvements
10 |
11 | ## Bug Fixes
12 |
13 | - Resolved the issue of being unable to reset it to empty after using the proxy.
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/components/message/index.ts:
--------------------------------------------------------------------------------
1 | export { AIMarkdownStreamingMessage } from "./AIMarkdownMessage"
2 | export { AIMessageParts } from "./AIMessageParts"
3 | export { ManageMemoryCard } from "./ManageMemoryCard"
4 | export { SaveMemoryCard } from "./SaveMemoryCard"
5 | export { ToolInvocationComponent } from "./ToolInvocationComponent"
6 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/user/utils.ts:
--------------------------------------------------------------------------------
1 | import type { UserModel } from "@follow/store/user/store"
2 |
3 | export const deduplicateUsers = (users: UserModel[]): UserModel[] => {
4 | const userMap = new Map()
5 | users.forEach((user) => {
6 | userMap.set(user.id, user)
7 | })
8 | return Array.from(userMap.values())
9 | }
10 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/search_3_cute_fi.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "search_3_cute_fi.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/search_3_cute_re.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "search_3_cute_re.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/settings/routes/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native"
2 |
3 | import { Text } from "@/src/components/ui/typography/Text"
4 |
5 | export const NotificationsScreen = () => {
6 | return (
7 |
8 | Notifications Settings
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/src/store/image/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query"
2 |
3 | import { imageSyncService } from "./store"
4 |
5 | export const usePrefetchImageColors = (url?: string | null) => {
6 | useQuery({
7 | queryKey: ["image", "colors", url],
8 | queryFn: () => imageSyncService.getColors(url),
9 | enabled: !!url,
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/apps/ssr/client/global.d.ts:
--------------------------------------------------------------------------------
1 | // data hydrate
2 |
3 | // e.g. window.__HYDRATE__['feeds.$get,query:id=41223694984583197']
4 | declare global {
5 | interface Window {
6 | __HYDRATE__: Record
7 | }
8 |
9 | export const GIT_COMMIT_SHA: string
10 | export const APP_VERSION: string
11 | export const APP_NAME: string
12 | }
13 |
14 | export {}
15 |
--------------------------------------------------------------------------------
/packages/internal/constants/src/app.ts:
--------------------------------------------------------------------------------
1 | export const APPLE_APP_STORE_ID = "6739802604"
2 | export const GOOGLE_PLAY_PACKAGE_ID = "is.follow"
3 |
4 | export const APP_STORE_URLS = {
5 | iOS: `https://apps.apple.com/us/app/folo-follow-everything/id${APPLE_APP_STORE_ID}`,
6 | Android: `https://play.google.com/store/apps/details?id=${GOOGLE_PLAY_PACKAGE_ID}`,
7 | } as const
8 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.3.3.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.3.3
2 |
3 | ## New Features
4 |
5 | ## Improvements
6 |
7 | - Merge actions for toggling state
8 | - Action supports matching custom title
9 |
10 | ## Bug Fixes
11 |
12 | - `Failed to set voice` error message appears every time the app starts
13 | - Can not replay TTS
14 | - Login state lost when restarting the app
15 |
--------------------------------------------------------------------------------
/apps/mobile/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true)
3 | return {
4 | presets: [
5 | ["babel-preset-expo", { jsxImportSource: "nativewind", unstable_transformImportMeta: true }],
6 | "nativewind/babel",
7 | ],
8 | plugins: [["inline-import", { extensions: [".sql"] }], "react-native-worklets/plugin"],
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/settings_1_cute_fi.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "settings_1_cute_fi.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/settings_1_cute_re.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "settings_1_cute_re.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/tabbar/contexts/BottomTabBarVisibleContext.tsx:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from "react"
2 | import { createContext } from "react"
3 |
4 | export const SetBottomTabBarVisibleContext = createContext>>(
5 | () => {},
6 | )
7 |
8 | export const BottomTabBarVisibleContext = createContext(true)
9 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/lexical-rich-editor/plugins/index.ts:
--------------------------------------------------------------------------------
1 | export { CodeHighlightingPlugin } from "./code-highlighting"
2 | export { ExitCodeBoundaryPlugin } from "./exit-code"
3 | export { KeyboardPlugin } from "./keyboard"
4 | export { StringLengthChangePlugin } from "./string-length-change"
5 | export { TripleBacktickTogglePlugin } from "./triple-backtick-toggle"
6 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.3.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.3
2 |
3 | ## New Features
4 |
5 | - Hold shift to quickly select multiple Feeds
6 | - Collect entry from List
7 |
8 | ## Improvements
9 |
10 | ## Bug Fixes
11 |
12 | - Action settings are invalid after loading archive entry
13 | - Redundant parameter on the transform form
14 | - Can't play normal video in video view
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/hooks/biz/useReduceMotion.ts:
--------------------------------------------------------------------------------
1 | import { useReducedMotion } from "motion/react"
2 |
3 | import { useUISettingKey } from "~/atoms/settings/ui"
4 |
5 | export const useReduceMotion = () => {
6 | const appReduceMotion = useUISettingKey("reduceMotion")
7 | const reduceMotion = useReducedMotion()
8 | return appReduceMotion || reduceMotion
9 | }
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/components/layouts/shared/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | EmptyState,
3 | type EmptyStateProps,
4 | SessionItem,
5 | type SessionItemProps,
6 | } from "./ChatSessionComponents"
7 | export { useChatSessionHandlers, type UseChatSessionHandlersProps } from "./useChatSessionHandlers"
8 | export { isTaskSession, isUnreadSession } from "./utils"
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/types.tsx:
--------------------------------------------------------------------------------
1 | // Export types that were defined in the original file
2 | export interface EntryContentProps {
3 | entryId: string
4 | noMedia?: boolean
5 | compact?: boolean
6 | classNames?: EntryContentClassNames
7 | }
8 | export interface EntryContentClassNames {
9 | header?: string
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/store/search/constants.ts:
--------------------------------------------------------------------------------
1 | const SearchTypeBase = {
2 | Feed: 1,
3 | Entry: 1 << 1,
4 | Subscription: 1 << 2,
5 | }
6 |
7 | export const SearchType = {
8 | ...SearchTypeBase,
9 | All: Object.values(SearchTypeBase).reduce((acc, cur) => acc | cur, 0),
10 | }
11 |
12 | export type SearchType = (typeof SearchType)[keyof typeof SearchType]
13 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/styles/main.css:
--------------------------------------------------------------------------------
1 | @import "./base.css";
2 | @import "./additional.css";
3 | @import "./scrollbar.css";
4 | @import "./cursor.css";
5 |
6 | @media print {
7 | * {
8 | overflow: visible !important;
9 | page-break-after: avoid;
10 | page-break-before: avoid;
11 | break-inside: avoid;
12 | height: max-content;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/black_board_2_cute_fi.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "black_board_2_cute_fi.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Assets.xcassets/black_board_2_cute_re.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "black_board_2_cute_re.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/list/getters.ts:
--------------------------------------------------------------------------------
1 | import { useListStore } from "./store"
2 |
3 | export const getListById = (id: string) => {
4 | const get = () => useListStore.getState()
5 | return get().lists[id]
6 | }
7 |
8 | export const getListFeedIds = (id: string) => {
9 | const get = () => useListStore.getState()
10 | return get().lists[id]?.feedIds
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/.env.example:
--------------------------------------------------------------------------------
1 | VITE_WEB_URL=http://localhost:5173
2 | VITE_API_URL=http://localhost:3000
3 | VITE_IMGPROXY_URL=http://localhost:2873
4 | VITE_SENTRY_DSN=
5 | VITE_BUILD_TYPE=production
6 | VITE_INBOXES_EMAIL=@follow.re
7 | VITE_OPENPANEL_CLIENT_ID=
8 | VITE_OPENPANEL_API_URL=
9 |
10 | VITE_EDITOR=cursor
11 |
12 | VITE_PUBLIC_POSTHOG_KEY=
13 | VITE_PUBLIC_POSTHOG_HOST=
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/initialize/op.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@follow/shared/env.desktop"
2 | import { OpenPanel } from "@openpanel/web"
3 |
4 | export const op = new OpenPanel({
5 | clientId: env.VITE_OPENPANEL_CLIENT_ID ?? "",
6 | trackScreenViews: true,
7 | trackOutgoingLinks: true,
8 | trackAttributes: true,
9 | apiUrl: env.VITE_OPENPANEL_API_URL,
10 | })
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/queries/server-configs.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query"
2 |
3 | import { followApi } from "~/lib/api-client"
4 |
5 | export const useServerConfigsQuery = () => {
6 | const { data } = useQuery({
7 | queryKey: ["server-configs"],
8 | queryFn: () => followApi.status.getConfigs(),
9 | })
10 | return data?.data
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/styles/cursor.css:
--------------------------------------------------------------------------------
1 | html,
2 | #shadow-html {
3 | --cursor-button: var(--pointer);
4 | --cursor-select: var(--pointer);
5 | --cursor-checkbox: var(--pointer);
6 | --cursor-link: var(--pointer);
7 | --cursor-menu: default;
8 | --cursor-radio: var(--pointer);
9 | --cursor-switch: var(--pointer);
10 | --cursor-card: var(--pointer);
11 | }
12 |
--------------------------------------------------------------------------------
/apps/mobile/native/android/src/main/java/expo/modules/follownative/tabbar/TabScreenView.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.follownative.tabbar
2 |
3 | import android.content.Context
4 | import expo.modules.kotlin.AppContext
5 | import expo.modules.kotlin.views.ExpoView
6 |
7 |
8 | class TabScreenView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/icons/mgc/loading_3_cute_li.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/hooks/src/optimistic/index.ts:
--------------------------------------------------------------------------------
1 | export { createOptimisticConfig } from "./config"
2 | export { optimisticStrategies } from "./strategies"
3 | export type {
4 | OptimisticContext,
5 | OptimisticItem,
6 | OptimisticMutationConfig,
7 | StrategyConfig,
8 | WithOptimistic,
9 | } from "./types"
10 | export { useOptimisticMutation } from "./useOptimisticMutation"
11 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/collection/getter.ts:
--------------------------------------------------------------------------------
1 | import { useCollectionStore } from "./store"
2 |
3 | export const isEntryStarred = (entryId: string): boolean => {
4 | return !!useCollectionStore.getState().collections[entryId]
5 | }
6 |
7 | export const getEntryCollections = (entryId: string) => {
8 | return useCollectionStore.getState().collections[entryId]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/desktop/build/entitlements.mas.child.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.inherit
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/main/src/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron"
2 | import squirrelStartup from "electron-squirrel-startup"
3 |
4 | import { DEVICE_ID } from "./constants/system"
5 | import { BootstrapManager } from "./manager/bootstrap"
6 |
7 | console.info("[main] device id:", DEVICE_ID)
8 | if (squirrelStartup) {
9 | app.quit()
10 | }
11 |
12 | BootstrapManager.start()
13 |
--------------------------------------------------------------------------------
/apps/ssr/client/initialize/index.ts:
--------------------------------------------------------------------------------
1 | import { initI18n } from "@client/i18n"
2 | import { initializeDayjs } from "@follow/components/dayjs"
3 |
4 | import { initAnalytics } from "./analytics"
5 | import { initSentry } from "./sentry"
6 |
7 | export const initialize = async () => {
8 | initAnalytics()
9 | initializeDayjs()
10 | await Promise.all([initI18n(), initSentry()])
11 | }
12 |
--------------------------------------------------------------------------------
/apps/ssr/src/lib/not-found.ts:
--------------------------------------------------------------------------------
1 | import { FetchError } from "ofetch"
2 |
3 | export class NotFoundError extends Error {
4 | constructor(reason: string) {
5 | super(`Page not found: ${reason}`)
6 | }
7 | }
8 | export const callNotFound = (e: any) => {
9 | if (e instanceof FetchError && e.status === 404) {
10 | throw new NotFoundError(e.message)
11 | }
12 | throw e
13 | }
14 |
--------------------------------------------------------------------------------
/packages/internal/tracker/src/adapters/index.ts:
--------------------------------------------------------------------------------
1 | export type { IdentifyPayload, TrackerAdapter, TrackPayload } from "./base"
2 | export { FirebaseAdapter, type FirebaseAdapterConfig } from "./firebase"
3 | export { OpenPanelAdapter, type OpenPanelAdapterConfig } from "./openpanel"
4 | export { PostHogAdapter, type PostHogAdapterConfig } from "./posthog"
5 | export { ProxyAdapter } from "./proxy"
6 |
--------------------------------------------------------------------------------
/packages/internal/types/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | VITE_WEB_URL: string
5 | VITE_API_URL: string
6 | VITE_SENTRY_DSN: string
7 | VITE_OPENPANEL_CLIENT_ID: string
8 | VITE_OPENPANEL_API_URL: string
9 | VITE_FIREBASE_CONFIG: string
10 | }
11 |
12 | interface ImportMeta {
13 | readonly env: ImportMetaEnv
14 | }
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/shared/index.ts:
--------------------------------------------------------------------------------
1 | export { AuthorHeader } from "./AuthorHeader"
2 | export { ContentBody } from "./ContentBody"
3 | export { MediaTranscript } from "./MediaTranscript"
4 | export { TranscriptToggle } from "./TranscriptToggle"
5 | export { useTranscription } from "./useTranscription"
6 | export { VideoPlayer } from "./VideoPlayer"
7 |
--------------------------------------------------------------------------------
/apps/mobile/src/lib/op.ts:
--------------------------------------------------------------------------------
1 | import { OpenPanel } from "@follow/tracker/src/op"
2 |
3 | import { proxyEnv } from "./proxy-env"
4 |
5 | export const op = new OpenPanel({
6 | clientId: proxyEnv.OPENPANEL_CLIENT_ID ?? "",
7 | apiUrl: proxyEnv.OPENPANEL_API_URL,
8 | headers: {
9 | Origin: "https://app.folo.is",
10 | },
11 | sdk: "react-native",
12 | sdkVersion: "1.0.0",
13 | })
14 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/subscription/ctx.ts:
--------------------------------------------------------------------------------
1 | import type { FeedViewType } from "@follow/constants"
2 | import { createContext } from "react"
3 |
4 | // TODO: remove this context
5 | const ViewPageCurrentViewContext = createContext(null!)
6 | export const ViewPageCurrentViewProvider = ViewPageCurrentViewContext.Provider
7 | export const GroupedContext = createContext(null)
8 |
--------------------------------------------------------------------------------
/packages/internal/components/src/icons/resize.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react"
2 |
3 | export function LetsIconsResizeDownRightLight(props: SVGProps) {
4 | return (
5 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/packages/internal/tracker/src/types.ts:
--------------------------------------------------------------------------------
1 | export type Optional = Pick, K> & Omit
2 |
3 | export type IdentifyPayload = {
4 | id: string
5 | name?: string | null
6 | email?: string | null
7 | image?: string | null
8 | handle?: string | null
9 | }
10 |
11 | export type Tracker = (code: number, properties?: Record) => Promise
12 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.4.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.4-web
2 |
3 | ## New Features
4 |
5 | 🎉 HUGE NEWS! Follow finally goes mobile!
6 |
7 | Ever wished you could Follow your favorite feeds while lounging on your couch? Well, now you can! We've made Follow fully responsive and mobile-friendly. Whether you're on your phone during your commute or browsing from bed (we won't judge), Follow's got your back!
8 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/ui/modal/stacked/bus.ts:
--------------------------------------------------------------------------------
1 | import { createEventBus } from "@follow/utils/event-bus"
2 |
3 | export const ModalEventBus = createEventBus<{
4 | DISMISS: ModalDisposeEvent
5 | RE_PRESENT: ModalRePresentEvent
6 | }>()
7 |
8 | export type ModalDisposeEvent = {
9 | id: string
10 | }
11 |
12 | export type ModalRePresentEvent = {
13 | id: string
14 | }
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/constants/dom.ts:
--------------------------------------------------------------------------------
1 | export const ENTRY_CONTENT_RENDER_CONTAINER_ID = "follow-entry-render"
2 |
3 | export const LOGO_MOBILE_ID = "follow-logo-mobile"
4 |
5 | export const ENTRY_COLUMN_LIST_SCROLLER_ID = "entry-column-scroller"
6 |
7 | export const APP_GRID_CONTAINER_ID = "follow-app-grid-container"
8 |
9 | export const ROOT_CONTAINER_ID = "follow-root-container"
10 |
--------------------------------------------------------------------------------
/apps/mobile/src/lib/navigation/bottom-tab/TabScreenContext.tsx:
--------------------------------------------------------------------------------
1 | import type { PrimitiveAtom } from "jotai"
2 | import { createContext } from "react"
3 |
4 | export interface TabScreenContextType {
5 | tabScreenIndex: number
6 |
7 | titleAtom: PrimitiveAtom
8 | identifierAtom: PrimitiveAtom
9 | }
10 | export const TabScreenContext = createContext(null!)
11 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/discover/DiscoverContent.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "react-native"
2 |
3 | import { Category } from "@/src/modules/discover/Category"
4 | import { Trending } from "@/src/modules/discover/Trending"
5 |
6 | export function DiscoverContent() {
7 | return (
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/apps/ssr/client/initialize/helper.ts:
--------------------------------------------------------------------------------
1 | import { tracker } from "@follow/tracker"
2 | import type { AuthUser } from "@follow-app/client-sdk"
3 |
4 | export const setIntegrationIdentify = async (user: AuthUser) => {
5 | tracker.identify(user)
6 |
7 | await import("@sentry/react").then(({ setTag }) => {
8 | setTag("user_id", user.id)
9 | setTag("user_name", user.name)
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/katex/lazy.tsx:
--------------------------------------------------------------------------------
1 | import { lazy, Suspense } from "react"
2 |
3 | import type { KateX } from "./index"
4 |
5 | const LazyKateX_ = lazy(() => import("./index").then((mod) => ({ default: mod.KateX })))
6 | export const LazyKateX: typeof KateX = (props) => {
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/feed/selectors.ts:
--------------------------------------------------------------------------------
1 | import type { FeedModel } from "./types"
2 |
3 | export const feedIconSelector = (feed: FeedModel) => {
4 | return {
5 | type: feed.type,
6 | ownerUserId: feed.ownerUserId,
7 | id: feed.id,
8 | title: feed.title,
9 | url: (feed as any).url || "",
10 | image: feed.image,
11 | siteUrl: feed.siteUrl,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/advanced-issue-labeler.yml:
--------------------------------------------------------------------------------
1 | policy:
2 | - section:
3 | - id: [platform]
4 | block-list: ["None", "Other"]
5 | label:
6 | - name: "platform: desktop"
7 | keys: ["Desktop - macOS", "Desktop - Windows", "Desktop - Linux", "Desktop - Web"]
8 | - name: "platform: mobile"
9 | keys: ["Mobile - iOS", "Mobile - Android", "Mobile - Web"]
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/usage/types.ts:
--------------------------------------------------------------------------------
1 | // Chart Data Types
2 | export interface ChartDataPoint {
3 | label: string
4 | value: number
5 | }
6 |
7 | export interface BarListItem {
8 | label: string
9 | value: number
10 | right?: string
11 | }
12 |
13 | // Component Props Types
14 | export interface TokenCount {
15 | value: string
16 | unit: string
17 | }
18 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/providers/hotkey-provider.tsx:
--------------------------------------------------------------------------------
1 | import { HotkeysProvider } from "react-hotkeys-hook"
2 |
3 | import { GlobalHotkeysProvider } from "./global-hotkeys-provider"
4 |
5 | export const HotkeyProvider: Component = ({ children }) => {
6 | return (
7 |
8 | {children}
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/tabbar/contexts/BottomTabBarBackgroundContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react"
2 | import type { SharedValue } from "react-native-reanimated"
3 |
4 | interface TabBarBackgroundContextType {
5 | opacity: SharedValue
6 | }
7 |
8 | export const BottomTabBarBackgroundContext = createContext({
9 | opacity: null!,
10 | })
11 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/pressable/IosItemPressable.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react"
2 | import type { ViewProps } from "react-native"
3 |
4 | const NativeItemPressable: FC<
5 | ViewProps & {
6 | onItemPress?: () => any
7 | touchHighlight?: boolean
8 | }
9 | > = () => {
10 | throw new Error("NativeItemPressable is not supported on iOS")
11 | }
12 | export { NativeItemPressable }
13 |
--------------------------------------------------------------------------------
/apps/mobile/src/initialize/dayjs.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs"
2 | import duration from "dayjs/plugin/duration"
3 | import localizedFormat from "dayjs/plugin/localizedFormat"
4 | import relativeTime from "dayjs/plugin/relativeTime"
5 | // Initialize dayjs
6 | export const initializeDayjs = () => {
7 | dayjs.extend(duration)
8 | dayjs.extend(relativeTime)
9 | dayjs.extend(localizedFormat)
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/errors/CustomSafeError.ts:
--------------------------------------------------------------------------------
1 | import { nextFrame } from "@follow/utils/dom"
2 |
3 | import { createErrorToaster } from "~/lib/error-parser"
4 |
5 | export class CustomSafeError extends Error {
6 | constructor(message: string, toast?: boolean) {
7 | super(message)
8 | if (toast) {
9 | nextFrame(() => createErrorToaster(message)(this))
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/initialize/helper.ts:
--------------------------------------------------------------------------------
1 | import { tracker } from "@follow/tracker"
2 | import type { AuthUser } from "@follow-app/client-sdk"
3 |
4 | export const setIntegrationIdentify = async (user: AuthUser) => {
5 | tracker.identify(user)
6 | await import("@sentry/react").then(({ setTag }) => {
7 | setTag("user_id", user.id)
8 | setTag("user_name", user.name)
9 | })
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-column/styles.ts:
--------------------------------------------------------------------------------
1 | import { readableContentMaxWidthClassName } from "~/constants/ui"
2 |
3 | export const girdClassNames = tw`grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4 @7xl:grid-cols-5 gap-1.5`
4 |
5 | // Shared max-width styles for readable content
6 | export const readableContentMaxWidth = tw`${readableContentMaxWidthClassName} mx-auto px-3`
7 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/settings/modal/use-setting-modal-hack.ts:
--------------------------------------------------------------------------------
1 | // HACK: Use expose the navigate function in the window object, avoid to import `router` circular issue.
2 | import type { SettingModalOptions } from "./useSettingModal"
3 |
4 | const showSettings = (args?: SettingModalOptions) => window.router.showSettings.call(null, args)
5 |
6 | export const useSettingModal = () => showSettings
7 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/src/common/ProviderComposer.tsx:
--------------------------------------------------------------------------------
1 | import type { JSX } from "react"
2 | import * as React from "react"
3 |
4 | export const ProviderComposer: Component<{
5 | contexts: JSX.Element[]
6 | }> = ({ contexts, children }) => {
7 | return contexts.reduceRight((kids: any, parent: any) => {
8 | return React.cloneElement(parent, { children: kids })
9 | }, children)
10 | }
11 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/sheet/context.tsx:
--------------------------------------------------------------------------------
1 | import { atom } from "jotai"
2 | import { createContext, use } from "react"
3 |
4 | interface SheetContextValue {
5 | dismiss: () => void
6 | }
7 | export const SheetContext = createContext(null)
8 |
9 | export const useSheetContext = () => use(SheetContext)
10 | export const sheetStackAtom = atom([] as HTMLDivElement[])
11 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.6.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.6
2 |
3 | We've made some ux optimizations:
4 |
5 | - [Mobile]: Now when returning to the list from the entry details, it will not return to the top of the page.
6 | - [Social Media]: Optimized the arrangement of multiple images with different aspect ratios.
7 | - [Social Media]: Long text auto-collapse.
8 |
9 | And many other bug fixes and improvements.
10 |
--------------------------------------------------------------------------------
/apps/mobile/src/atoms/hooks/useDeviceType.ts:
--------------------------------------------------------------------------------
1 | import { jotaiStore } from "@follow/utils"
2 | import { useAtomValue } from "jotai"
3 |
4 | import { appAtoms } from "../app"
5 |
6 | export const useDeviceType = () => {
7 | const deviceType = useAtomValue(appAtoms.deviceType)
8 | return deviceType
9 | }
10 |
11 | export const getDeviceType = () => {
12 | return jotaiStore.get(appAtoms.deviceType)
13 | }
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/ui/media/MediaContainerWidthProvider.tsx:
--------------------------------------------------------------------------------
1 | import { MediaContainerWidthContext } from "./MediaContainerWidthContext"
2 |
3 | export const MediaContainerWidthProvider = ({
4 | children,
5 | width,
6 | }: {
7 | children: React.ReactNode
8 | width: number
9 | }) => {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/file-upload/types.ts:
--------------------------------------------------------------------------------
1 | export interface FileUploadPluginConfig {
2 | /**
3 | * Enable drag and drop file upload
4 | */
5 | enableDragDrop?: boolean
6 | /**
7 | * Enable paste file upload
8 | */
9 | enablePaste?: boolean
10 | }
11 |
12 | export interface FileDropZoneState {
13 | isDragOver: boolean
14 | dragCounter: number
15 | }
16 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/discover/utils.ts:
--------------------------------------------------------------------------------
1 | import type { RSSHubParameter, RSSHubParameterObject } from "@follow/models/rsshub"
2 |
3 | export const normalizeRSSHubParameters = (
4 | parameters: RSSHubParameter,
5 | ): RSSHubParameterObject | null =>
6 | parameters
7 | ? typeof parameters === "string"
8 | ? { description: parameters, default: null }
9 | : parameters
10 | : null
11 |
--------------------------------------------------------------------------------
/packages/internal/atoms/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "declaration": true,
6 | "types": ["@follow/types/react", "@follow/types/global", "vite/client"],
7 | "paths": {
8 | "@follow/atoms/*": ["./src/*"],
9 | "@pkg": ["../../package.json"]
10 | }
11 | },
12 | "include": ["src/**/*"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/internal/database/src/drizzle/0018_dashing_the_fury.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `translations` (
2 | `entry_id` text PRIMARY KEY NOT NULL,
3 | `language` text NOT NULL,
4 | `title` text NOT NULL,
5 | `description` text NOT NULL,
6 | `content` text NOT NULL,
7 | `created_at` text NOT NULL
8 | );
9 | --> statement-breakpoint
10 | CREATE UNIQUE INDEX `translation-unique-index` ON `translations` (`entry_id`,`language`);
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 💬 Follow's Discord Server
4 | url: https://discord.gg/tUDVZjEr
5 | about: Want to discuss / chat with the community? Here you go!
6 | - name: Discuss an issue
7 | url: https://github.com/RSSNext/Follow/discussions
8 | about: For general questions, ideas, or non-bug related discussions, please use GitHub Discussions.
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/common/ProviderComposer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type { JSX } from "react"
4 | import { cloneElement } from "react"
5 |
6 | export const ProviderComposer: Component<{
7 | contexts: JSX.Element[]
8 | }> = ({ contexts, children }) =>
9 | contexts.reduceRight(
10 | (kids: any, parent: any) => cloneElement(parent, { children: kids }),
11 | children,
12 | )
13 |
--------------------------------------------------------------------------------
/apps/mobile/native/android/src/main/java/expo/modules/follownative/tabbar/TabBarPortalView.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.follownative.tabbar
2 |
3 | import android.content.Context
4 | import expo.modules.kotlin.AppContext
5 | import expo.modules.kotlin.views.ExpoView
6 | import androidx.core.view.isNotEmpty
7 |
8 | class TabBarPortalView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Extensions/UIImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/3/12.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImage {
11 | func withAlpha(_ alpha: CGFloat) -> UIImage {
12 | return UIGraphicsImageRenderer(size: size).image { _ in
13 | draw(at: .zero, blendMode: .normal, alpha: alpha)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/toast/constants.ts:
--------------------------------------------------------------------------------
1 | import { CheckCircleFilledIcon } from "@/src/icons/check_circle_filled"
2 | import { CloseCircleFillIcon } from "@/src/icons/close_circle_fill"
3 | import { InfoCircleFillIcon } from "@/src/icons/info_circle_fill"
4 |
5 | export const toastTypeToIcons = {
6 | success: CheckCircleFilledIcon,
7 | error: CloseCircleFillIcon,
8 | info: InfoCircleFillIcon,
9 | } as const
10 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/user/getters.ts:
--------------------------------------------------------------------------------
1 | import { useUserStore } from "./store"
2 |
3 | export const whoami = () => {
4 | return useUserStore.getState().whoami
5 | }
6 |
7 | export const role = () => {
8 | return useUserStore.getState().role
9 | }
10 |
11 | export const getUserList = (userIds: string[]) => {
12 | return userIds.map((id) => useUserStore.getState().users[id]).filter((i) => !!i)
13 | }
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/lib/log.ts:
--------------------------------------------------------------------------------
1 | import { log } from "@follow/logger"
2 |
3 | export const appLog = (...args: any[]) => {
4 | if (ELECTRON) log(...args)
5 | console.info(
6 | `%c ${APP_NAME} %c`,
7 | "color: #fff; margin: 0; padding: 5px 0; background: #ff5c00; border-radius: 3px;",
8 | ...args.reduce((acc, cur) => {
9 | acc.push("", cur)
10 | return acc
11 | }, []),
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/layouts/header/hooks.ts:
--------------------------------------------------------------------------------
1 | import { use } from "react"
2 |
3 | import { NavigationHeaderHeightContext } from "../views/NavigationHeaderContext"
4 |
5 | export const useNavigationHeaderHeight = () => {
6 | const headerHeight = use(NavigationHeaderHeightContext)
7 | if (!headerHeight) {
8 | throw new Error("NavigationHeaderHeightContext is not found")
9 | }
10 | return headerHeight
11 | }
12 |
--------------------------------------------------------------------------------
/apps/ssr/client/atoms/user.ts:
--------------------------------------------------------------------------------
1 | import { createAtomHooks } from "@follow/utils/jotai"
2 | import type { AuthUser } from "@follow-app/client-sdk"
3 | import { atom } from "jotai"
4 |
5 | export const [, , useWhoami, , whoami, setWhoami] = createAtomHooks(atom>(null))
6 |
7 | export const [, , useLoginModalShow, useSetLoginModalShow, getLoginModalShow, setLoginModalShow] =
8 | createAtomHooks(atom(false))
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/subscription-column/hook.ts:
--------------------------------------------------------------------------------
1 | import { useDndContext } from "@dnd-kit/core"
2 |
3 | import { useFeedAreaScrollProgressValue } from "./atom"
4 |
5 | export function useShouldFreeUpSpace() {
6 | const dndContext = useDndContext()
7 | const isDragging = !!dndContext.active
8 | const scrollProgress = useFeedAreaScrollProgressValue()
9 | return isDragging && scrollProgress === 0
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { extendConfig } from "@follow/configs/tailwindcss/web"
2 | import path from "pathe"
3 |
4 | const rootDir = path.resolve(__dirname, "../../../..")
5 |
6 | export default extendConfig({
7 | darkMode: "media",
8 | future: { hoverOnlyWhenSupported: true },
9 | content: ["./src/**/*.{ts,tsx}", path.resolve(rootDir, "packages/components/src/**/*.{ts,tsx}")],
10 | })
11 |
--------------------------------------------------------------------------------
/apps/ssr/client/initialize/analytics.ts:
--------------------------------------------------------------------------------
1 | import { env } from "@follow/shared/env.ssr"
2 | import { setOpenPanelTracker } from "@follow/tracker"
3 |
4 | import { op } from "./op"
5 |
6 | export const initAnalytics = () => {
7 | if (env.VITE_OPENPANEL_CLIENT_ID === undefined) return
8 |
9 | setOpenPanelTracker(op)
10 | op.setGlobalProperties({
11 | build: "external-web",
12 | hash: GIT_COMMIT_SHA,
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/icons/mgc/line_cute_re.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/mgc/loading_3_cute_re.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/desktop/layer/main/src/lib/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from "i18next"
2 |
3 | import { resources } from "../@types/resources"
4 |
5 | export const defaultNS = "native"
6 |
7 | export const i18n = i18next.createInstance() as typeof i18next
8 |
9 | i18n.init({
10 | fallbackLng: {
11 | default: ["en"],
12 | "zh-TW": ["zh-CN", "en"],
13 | },
14 | defaultNS,
15 | resources,
16 | })
17 |
18 | export const { t } = i18n
19 |
--------------------------------------------------------------------------------
/apps/desktop/layer/main/src/updater/windows-updater.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron"
2 | import { NsisUpdater } from "electron-updater"
3 | import { DownloadedUpdateHelper } from "electron-updater/out/DownloadedUpdateHelper"
4 |
5 | export class WindowsUpdater extends NsisUpdater {
6 | protected override downloadedUpdateHelper: DownloadedUpdateHelper = new DownloadedUpdateHelper(
7 | app.getPath("sessionData"),
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/settings/tabs/ai/byok/constants.ts:
--------------------------------------------------------------------------------
1 | import type { ByokProviderName } from "@follow/shared/settings/interface"
2 |
3 | export const PROVIDER_OPTIONS: { value: ByokProviderName; label: string }[] = [
4 | { value: "openai", label: "OpenAI" },
5 | { value: "google", label: "Google" },
6 | { value: "vercel-ai-gateway", label: "Vercel AI Gateway" },
7 | { value: "openrouter", label: "OpenRouter" },
8 | ]
9 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/subscription-column/sort-by/types.tsx:
--------------------------------------------------------------------------------
1 | import type { FeedViewType } from "@follow/constants"
2 |
3 | export type FeedListProps = {
4 | view: FeedViewType
5 | data: Record
6 | categoryOpenStateData: Record
7 | }
8 | export type SortBy = "count" | "alphabetical"
9 |
10 | export type ListListProps = {
11 | view: FeedViewType
12 | data: string[]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Folo/Folo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 | com.apple.developer.applesignin
8 |
9 | Default
10 |
11 |
12 |
--------------------------------------------------------------------------------
/apps/mobile/native/android/src/main/java/expo/modules/follownative/tabbar/TabScreenModule.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.follownative.tabbar
2 |
3 | import expo.modules.kotlin.modules.Module
4 | import expo.modules.kotlin.modules.ModuleDefinition
5 |
6 | class TabScreenModule : Module() {
7 | override fun definition() = ModuleDefinition {
8 | Name("TabScreen")
9 |
10 | View(TabScreenView::class) {
11 |
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageCarouselViewControllerProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageCarouselViewControllerProtocol.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/3/12.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol ImageCarouselViewControllerProtocol {
11 | func saveImageToPhotos()
12 | func copyImageToClipboard()
13 | func shareImage()
14 |
15 | func isLoadError() -> Bool
16 | }
17 |
--------------------------------------------------------------------------------
/apps/ssr/note.md:
--------------------------------------------------------------------------------
1 | Rewrite:
2 |
3 | Test url:
4 |
5 | http://localhost:2234/share/feeds/41223694984583197
6 | http://localhost:2234/share/feeds/41375451836487680?view=2
7 | http://localhost:2234/share/feeds/41147805276726317?view=2
8 | http://localhost:2234/share/lists/61046160274909184
9 |
10 | http://localhost:2234/og/list/61046160274909184
11 | http://localhost:2234/og/user/Innei
12 | http://localhost:2234/og/feed/41223694984583170
13 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.7.0.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.7.0
2 |
3 | ## Shiny new things
4 |
5 | - Add custom integration configurations to adapt to more apps
6 |
7 | ## Improvements
8 |
9 | - Redesign integration settings page for better user experience and support integration settings export and import.
10 |
11 | ## No longer broken
12 |
13 | ## Thanks
14 |
15 | Special thanks to volunteer contributors @ for their valuable contributions
16 |
--------------------------------------------------------------------------------
/apps/desktop/vite.config.electron-render.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "pathe"
2 | import { defineConfig } from "vite"
3 |
4 | import config from "./configs/vite.electron-render.config"
5 | import compressAndFingerprintPlugin from "./plugins/vite/compress"
6 |
7 | export default defineConfig({
8 | ...config,
9 | base: "./",
10 | plugins: [...config.plugins, compressAndFingerprintPlugin(resolve(import.meta.dirname, "dist"))],
11 | })
12 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/qrcode/constants.ts:
--------------------------------------------------------------------------------
1 | export const OUTER_EYE_SIZE_IN_BITS = 7
2 | export const INNER_EYE_SIZE_IN_BITS = 3
3 |
4 | export const EYES_POSITIONS = ["topLeft", "topRight", "bottomLeft"]
5 |
6 | // QR code error collection percentages
7 | export const QR_ECL_PERS = {
8 | L: 0.03,
9 | M: 0.06,
10 | Q: 0.1,
11 | H: 0.14,
12 | low: 0.03,
13 | medium: 0.06,
14 | quartile: 0.1,
15 | high: 0.14,
16 | }
17 |
--------------------------------------------------------------------------------
/icons/mgc/attachment_cute_re.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/image/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react"
2 |
3 | import { useImagesStore } from "./store"
4 |
5 | export const useImageColors = (url?: string | null) => {
6 | return useImagesStore(
7 | useCallback(
8 | (state) => {
9 | if (!url) {
10 | return
11 | }
12 | return state.images[url]?.colors
13 | },
14 | [url],
15 | ),
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/atoms/dom.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "jotai"
2 |
3 | import { createAtomHooks } from "~/lib/jotai"
4 |
5 | export const [, , useMainContainerElement, , getMainContainerElement, setMainContainerElement] =
6 | createAtomHooks(atom(null))
7 |
8 | export const [, , useRootContainerElement, , getRootContainerElement, setRootContainerElement] =
9 | createAtomHooks(atom(null))
10 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/hooks/useAIConfiguration.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query"
2 |
3 | import { followApi } from "~/lib/api-client"
4 |
5 | export const useAIConfiguration = () => {
6 | return useQuery({
7 | queryKey: ["aiConfiguration"],
8 | queryFn: async () => {
9 | return followApi.ai.config()
10 | },
11 | staleTime: 5 * 60 * 1000,
12 | retry: false,
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/subscription-column/context.ts:
--------------------------------------------------------------------------------
1 | import type { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core"
2 | import type { CSSProperties } from "react"
3 | import { createContext } from "react"
4 |
5 | export const DraggableContext = createContext<{
6 | attributes: DraggableAttributes
7 | listeners: DraggableSyntheticListeners
8 | style?: CSSProperties | undefined
9 | } | null>(null)
10 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/lexical-rich-editor/index.ts:
--------------------------------------------------------------------------------
1 | export { createDefaultLexicalEditor, createLexicalEditor } from "./editor"
2 | export { LexicalRichEditor } from "./LexicalRichEditor"
3 | export { LexicalRichEditorTextArea } from "./LexicalRichEditorTextArea"
4 | export { LexicalRichEditorNodes } from "./nodes"
5 | export { KeyboardPlugin } from "./plugins"
6 | export { defaultLexicalTheme } from "./theme"
7 | export type * from "./types"
8 |
--------------------------------------------------------------------------------
/packages/internal/models/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "jsx": "preserve",
6 | "declaration": true,
7 | "types": ["@follow/types/react", "@follow/types/global", "vite/client"],
8 | "paths": {
9 | "@follow/models/*": ["./src/*"],
10 | "@pkg": ["../../package.json"]
11 | }
12 | },
13 | "include": ["src/**/*"]
14 | }
15 |
--------------------------------------------------------------------------------
/apps/mobile/native/android/src/main/java/expo/modules/follownative/tabbar/TabBarPortalModule.kt:
--------------------------------------------------------------------------------
1 | package expo.modules.follownative.tabbar
2 |
3 | import expo.modules.kotlin.modules.Module
4 | import expo.modules.kotlin.modules.ModuleDefinition
5 |
6 | class TabBarPortalModule : Module() {
7 | override fun definition() = ModuleDefinition {
8 | Name("TabBarPortal")
9 |
10 | View(TabBarPortalView::class) {
11 |
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/grouped/constants.ts:
--------------------------------------------------------------------------------
1 | import { PixelRatio } from "react-native"
2 |
3 | const pixelRatio = PixelRatio.get()
4 | export const GROUPED_ICON_TEXT_GAP = 36 / pixelRatio
5 |
6 | export const GROUPED_LIST_MARGIN = 48 / pixelRatio
7 |
8 | export const GROUPED_LIST_ITEM_PADDING = 52 / pixelRatio
9 |
10 | export const GROUPED_SECTION_TOP_MARGIN = 87 / pixelRatio
11 | export const GROUPED_SECTION_BOTTOM_MARGIN = 36 / pixelRatio
12 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/src/atoms/index.ts:
--------------------------------------------------------------------------------
1 | import { atom } from "jotai"
2 |
3 | import type { EntryModel } from "../../types"
4 |
5 | export const entryAtom = atom(null)
6 |
7 | export const codeThemeLightAtom = atom(null)
8 | export const codeThemeDarkAtom = atom(null)
9 | export const readerRenderInlineStyleAtom = atom(false)
10 | export const noMediaAtom = atom(false)
11 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/summary/utils.ts:
--------------------------------------------------------------------------------
1 | import type { SupportedActionLanguage } from "@follow/shared/language"
2 |
3 | export function getGenerateSummaryStatusId(
4 | entryId: string,
5 | actionLanguage: SupportedActionLanguage,
6 | target: "content" | "readabilityContent",
7 | ): StatusID {
8 | return `${entryId}-${actionLanguage}-${target}` as StatusID
9 | }
10 |
11 | export type StatusID = `${string}-${string}-${string}`
12 |
--------------------------------------------------------------------------------
/packages/internal/tracker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@follow/tracker",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "./src/index.ts",
6 | "peerDependencies": {
7 | "posthog-js": "1.255.0",
8 | "posthog-react-native": "4.6.1"
9 | },
10 | "devDependencies": {
11 | "@follow-app/client-sdk": "catalog:",
12 | "@follow/configs": "workspace:*",
13 | "@react-native-firebase/analytics": "22.2.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.3.2.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.3.2
2 |
3 | ## New Features
4 |
5 | - Added support for Two-Factor Authentication (2FA) for login and large transactions.
6 |
7 | ## Improvements
8 |
9 | - Enhanced detection for translation needs and improved display of translated text.
10 |
11 | ## Bug Fixes
12 |
13 | - Resolved an issue where some read marks were missed during fast scrolling.
14 | - Unable to copy inbox email address correctly
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/assets/rsshub.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/components/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "jsx": "preserve",
6 | "declaration": true,
7 | "types": ["@follow/types/react", "@follow/types/global", "vite/client"],
8 | "paths": {
9 | "@follow/components/*": ["./src/*"],
10 | "@pkg": ["../../package.json"]
11 | }
12 | },
13 | "include": ["src/**/*"]
14 | }
15 |
--------------------------------------------------------------------------------
/packages/internal/constants/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "declaration": true,
6 | "types": ["@follow/types/react", "@follow/types/global", "vite/client"],
7 | "paths": {
8 | "@follow/constants/*": ["./src/*"],
9 | "@pkg": ["../../package.json"]
10 | }
11 | },
12 | "include": ["src/**/*", "../../../../locales/**/*.json"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/internal/hooks/src/useRefValue.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef } from "react"
2 |
3 | export const useRefValue = (
4 | value: S,
5 | ): Readonly<{
6 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
7 | current: S extends Function ? S : Readonly
8 | }> => {
9 | const ref = useRef(value)
10 |
11 | useLayoutEffect(() => {
12 | ref.current = value
13 | }, [value])
14 | return ref as any
15 | }
16 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/pressable/NativePressable.ios.tsx:
--------------------------------------------------------------------------------
1 | import { NativeItemPressable } from "./IosItemPressable"
2 | import type { NativePressableProps } from "./NativePressable.types"
3 |
4 | export const NativePressable = ({ children, onPress, ...props }: NativePressableProps) => {
5 | return (
6 |
7 | {children}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/src/lib/native/user-agent.ts:
--------------------------------------------------------------------------------
1 | import { nativeApplicationVersion, nativeBuildVersion } from "expo-application"
2 | import Constants from "expo-constants"
3 |
4 | const expoUserAgentPromise = Constants.getWebViewUserAgentAsync()
5 |
6 | export const getUserAgent = async () => {
7 | const expoUserAgent = await expoUserAgentPromise
8 | return `${expoUserAgent ? `${expoUserAgent} ` : ""}Folo/${nativeApplicationVersion}.${nativeBuildVersion}`
9 | }
10 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/unread/utils.ts:
--------------------------------------------------------------------------------
1 | // Inbox subscription's feedId is `inbox-${inboxId}`, we need to convert it between unread and entry store.
2 | export const INBOX_PREFIX_ID = "inbox-"
3 | export const getInboxHandleOrFeedIdFromFeedId = (id: string) =>
4 | id.startsWith(INBOX_PREFIX_ID) ? id.slice(INBOX_PREFIX_ID.length) : id
5 | export const getInboxFeedIdWithPrefix = (id: string) =>
6 | id.startsWith(INBOX_PREFIX_ID) ? id : INBOX_PREFIX_ID + id
7 |
--------------------------------------------------------------------------------
/packages/internal/utils/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./attribution"
2 | export * from "./bind-this"
3 | export * from "./cjk"
4 | export * from "./color"
5 | export * from "./data-structure/set"
6 | export * from "./dom"
7 | export * from "./duration"
8 | export * from "./jotai"
9 | export * from "./noop"
10 | export * from "./path-parser"
11 | export * from "./react"
12 | export * from "./resize"
13 | export * from "./url-for-video"
14 | export * from "./utils"
15 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/editor/index.ts:
--------------------------------------------------------------------------------
1 | import { LexicalRichEditorNodes } from "@follow/components/ui/lexical-rich-editor/nodes.js"
2 |
3 | import { FileAttachmentNode, MentionNode, SelectedTextNode, ShortcutNode } from "./plugins"
4 |
5 | export * from "./plugins"
6 |
7 | export const LexicalAIEditorNodes = [
8 | ...LexicalRichEditorNodes,
9 | MentionNode,
10 | ShortcutNode,
11 | FileAttachmentNode,
12 | SelectedTextNode,
13 | ]
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/editor/plugins/shortcut/constants.ts:
--------------------------------------------------------------------------------
1 | import { createCommand } from "lexical"
2 |
3 | import type { ShortcutData } from "./types"
4 |
5 | export const SHORTCUT_COMMAND = createCommand("SHORTCUT_COMMAND")
6 |
7 | export const DEFAULT_MAX_SHORTCUT_SUGGESTIONS = 10
8 |
9 | export const SHORTCUT_TRIGGER_PATTERN =
10 | /(?:^|\s)(\/[\w\-\s\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]*)$/
11 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/providers/user-provider.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 |
3 | import { setIntegrationIdentify } from "~/initialize/helper"
4 | import { useSession } from "~/queries/auth"
5 |
6 | export const UserProvider = () => {
7 | const { session } = useSession()
8 |
9 | useEffect(() => {
10 | if (!session?.user) return
11 |
12 | setIntegrationIdentify(session.user)
13 | }, [session?.user])
14 |
15 | return null
16 | }
17 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/platform-icon/icons.ts:
--------------------------------------------------------------------------------
1 | export * from "./collections/cubox"
2 | export * from "./collections/eagle"
3 | export * from "./collections/instapaper"
4 | export * from "./collections/obsidian"
5 | export * from "./collections/outline"
6 | export * from "./collections/readeck"
7 | export * from "./collections/readwise"
8 | export * from "./collections/rss3"
9 | export * from "./collections/rsshub"
10 | export * from "./collections/zotero"
11 |
--------------------------------------------------------------------------------
/packages/internal/hooks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@follow/configs/tsconfig.extend.json",
3 | "compilerOptions": {
4 | "noEmit": true,
5 | "baseUrl": ".",
6 | "jsx": "preserve",
7 | "declaration": true,
8 | "types": ["@follow/types/react", "@follow/types/global", "vite/client"],
9 | "paths": {
10 | "@follow/hooks/*": ["./src/*"],
11 | "@pkg": ["../../package.json"]
12 | }
13 | },
14 | "include": ["src/**/*"]
15 | }
16 |
--------------------------------------------------------------------------------
/packages/internal/logger/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@follow/logger",
3 | "private": true,
4 | "exports": {
5 | ".": {
6 | "types": "./electron.ts",
7 | "web": "./web.ts",
8 | "default": "./electron.ts"
9 | }
10 | },
11 | "scripts": {
12 | "typecheck": "tsc --noEmit"
13 | },
14 | "dependencies": {
15 | "electron-log": "5.4.3"
16 | },
17 | "devDependencies": {
18 | "@follow/configs": "workspace:*"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/store/event-system/types.ts:
--------------------------------------------------------------------------------
1 | import type { ChatStatus } from "ai"
2 |
3 | import type { BizUIMessage } from "../types"
4 |
5 | // Event types and payloads
6 | export interface ChatStateEvents {
7 | messages: { messages: UI_MESSAGE[] }
8 | status: { status: ChatStatus }
9 | error: { error: Error | undefined }
10 | }
11 |
12 | export type ChatStateEventType = keyof ChatStateEvents
13 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/types/ChatSession.ts:
--------------------------------------------------------------------------------
1 | import type { SerializedEditorState } from "lexical"
2 |
3 | export interface ChatSession {
4 | chatId: string
5 | title?: string
6 | createdAt: Date
7 | updatedAt: Date
8 | isLocal: boolean
9 | syncStatus: "local" | "synced"
10 | }
11 |
12 | export type RichTextPart = {
13 | type: "data-rich-text"
14 | data: {
15 | state: SerializedEditorState
16 | text: string
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/entry-column/layouts/AppendTaildingDivider.tsx:
--------------------------------------------------------------------------------
1 | import { DividerVertical } from "@follow/components/ui/divider/Divider.js"
2 | import * as React from "react"
3 |
4 | export const AppendTaildingDivider = ({ children }: { children: React.ReactNode }) => (
5 | <>
6 | {children}
7 | {React.Children.toArray(children).filter(Boolean).length > 0 && (
8 |
9 | )}
10 | >
11 | )
12 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/settings/constants.ts:
--------------------------------------------------------------------------------
1 | export const SETTING_MODAL_ID = "setting-modal"
2 |
3 | export const GUEST_ALLOWED_SETTING_TABS = ["general", "appearance", "about", "shortcuts"] as const
4 |
5 | const GUEST_ALLOWED_SETTING_TABS_SET = new Set(GUEST_ALLOWED_SETTING_TABS)
6 |
7 | export const isGuestAccessibleSettingTab = (tab?: string | null) => {
8 | if (!tab) return false
9 | return GUEST_ALLOWED_SETTING_TABS_SET.has(tab)
10 | }
11 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Modules/TabBar/TabBarPortalModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarPortalModule.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/3/17.
6 | //
7 |
8 | import ExpoModulesCore
9 | import UIKit
10 |
11 | public class TabBarPortalModule: Module {
12 | public func definition() -> ModuleDefinition {
13 | Name("TabBarPortal")
14 |
15 | View(TabBarPortalView.self) {}
16 | }
17 | }
18 |
19 | class TabBarPortalView: ExpoView {
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/native/webview/native-webview.tsx:
--------------------------------------------------------------------------------
1 | import { requireNativeView } from "expo"
2 | import type * as React from "react"
3 | import type { ViewProps } from "react-native"
4 |
5 | export const NativeWebView: React.ComponentType<
6 | ViewProps & {
7 | onContentHeightChange?: (e: { nativeEvent: { height: number } }) => void
8 | onSeekAudio?: (e: { time: number }) => void
9 | url?: string
10 | }
11 | > = requireNativeView("FOSharedWebView")
12 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/src/components/p.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { IsInParagraphContext } from "./__internal/ctx"
4 |
5 | export const MarkdownP: Component<
6 | React.DetailedHTMLProps, HTMLParagraphElement>
7 | > = ({ children, ...props }) => {
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/apps/mobile/changelog/0.2.5.md:
--------------------------------------------------------------------------------
1 | # What's New in v0.2.5
2 |
3 | ## Shiny new things
4 |
5 | - Add option for independent content font size and presets
6 |
7 | ## Improvements
8 |
9 | ## No longer broken
10 |
11 | - Address the issue of being unable to follow the feed in specific scenarios
12 | - Fixed the issue where entries opened from notifications could not be loaded
13 |
14 | ## Thanks
15 |
16 | Special thanks to volunteer contributors @ for their valuable contributions
17 |
--------------------------------------------------------------------------------
/apps/mobile/ios/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | project.xcworkspace
24 | .xcode.env.local
25 |
26 | # Bundle artifacts
27 | *.jsbundle
28 |
29 | # CocoaPods
30 | /Pods/
31 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/pressable/IosItemPressable.ios.tsx:
--------------------------------------------------------------------------------
1 | import { requireNativeView } from "expo"
2 | import { cssInterop } from "nativewind"
3 | import type { ViewProps } from "react-native"
4 |
5 | const NativeItemPressable = requireNativeView<
6 | ViewProps & {
7 | onItemPress?: () => any
8 | touchHighlight?: boolean
9 | }
10 | >("ItemPressable")
11 | cssInterop(NativeItemPressable, {
12 | className: "style",
13 | })
14 | export { NativeItemPressable }
15 |
--------------------------------------------------------------------------------
/apps/mobile/src/constants/spring.ts:
--------------------------------------------------------------------------------
1 | import type { WithSpringConfig } from "react-native-reanimated"
2 |
3 | export const gentleSpringPreset: WithSpringConfig = {
4 | damping: 15,
5 | stiffness: 100,
6 | mass: 1,
7 | }
8 |
9 | export const softSpringPreset: WithSpringConfig = {
10 | damping: 20,
11 | stiffness: 80,
12 | mass: 1,
13 | }
14 |
15 | export const quickSpringPreset: WithSpringConfig = {
16 | damping: 10,
17 | stiffness: 200,
18 | mass: 1,
19 | }
20 |
--------------------------------------------------------------------------------
/apps/mobile/src/lib/navigation/ScreenNameContext.tsx:
--------------------------------------------------------------------------------
1 | import type { PrimitiveAtom } from "jotai"
2 | import { useAtomValue } from "jotai"
3 | import { createContext, use } from "react"
4 |
5 | export const ScreenNameContext = createContext>(null!)
6 |
7 | export const useScreenName = () => {
8 | const name = use(ScreenNameContext)
9 | if (!name) {
10 | throw new Error("ScreenNameContext not mounted")
11 | }
12 | return useAtomValue(name)
13 | }
14 |
--------------------------------------------------------------------------------
/apps/mobile/web-app/html-renderer/global.d.ts:
--------------------------------------------------------------------------------
1 | import "vite/client"
2 | import "../../../../packages/types/react-global"
3 | import "../../../../packages/types/global"
4 |
5 | interface Bridge {
6 | measure: () => void
7 | setContentHeight: (height: number) => void
8 | previewImage: (data: { imageUrls: string[]; index: number }) => void
9 | seekAudio: (time: number) => void
10 | }
11 |
12 | declare global {
13 | export const bridge: Bridge
14 | }
15 |
16 | export {}
17 |
--------------------------------------------------------------------------------
/packages/internal/constants/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@follow/constants",
3 | "type": "module",
4 | "private": true,
5 | "sideEffects": false,
6 | "exports": {
7 | ".": "./src/index.ts"
8 | },
9 | "main": "./src/index.ts",
10 | "scripts": {
11 | "typecheck": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@follow-app/client-sdk": "catalog:",
15 | "@follow/configs": "workspace:*",
16 | "@follow/types": "workspace:*"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/lib/parsers.ts:
--------------------------------------------------------------------------------
1 | import { isTwitterUrl, isXUrl } from "@follow/utils/link-parser"
2 |
3 | export const parseSocialMedia = (parsedUrl?: string | null) => {
4 | if (!parsedUrl) return
5 |
6 | const isX = isXUrl(parsedUrl).validate || isTwitterUrl(parsedUrl).validate
7 |
8 | if (isX) {
9 | return {
10 | type: "x",
11 | meta: {
12 | handle: new URL(parsedUrl).pathname.split("/").pop(),
13 | },
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/subscription-column/styles.ts:
--------------------------------------------------------------------------------
1 | import { IN_ELECTRON } from "@follow/shared/constants"
2 | import { clsx } from "@follow/utils/utils"
3 |
4 | export const feedColumnStyles = {
5 | item: clsx(
6 | !IN_ELECTRON && tw`duration-200 hover:bg-theme-item-hover`,
7 | tw`flex w-full cursor-menu items-center rounded-md pr-2.5 text-base lg:text-sm font-medium !leading-loose`,
8 | tw`data-[active=true]:bg-theme-item-active`,
9 | ),
10 | }
11 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.8.0.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.8.0
2 |
3 | ## Shiny new things
4 |
5 | - Smart onboarding that gets you started in seconds.
6 | - One place for all your feeds.
7 | - A cleaner, consistent reading experience for social media posts, picture galleries, videos, and articles.
8 | - Support subtitles for podcasts and videos.
9 | - Redesigned Actions for easier setup.
10 |
11 | ## Improvements
12 |
13 | - We have made countless improvements and bug fixes in this version.
14 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/components/ui/markdown/types.ts:
--------------------------------------------------------------------------------
1 | export type MarkdownImage = {
2 | url: string
3 | width?: number | undefined
4 | height?: number | undefined
5 | preview_image_url?: string | undefined
6 | blurhash?: string | undefined
7 | }
8 |
9 | export interface MarkdownRenderActions {
10 | transformUrl: (url?: string) => string | undefined
11 | isAudio: (url?: string) => boolean
12 | ensureAndRenderTimeStamp: (children: string) => React.ReactNode
13 | }
14 |
--------------------------------------------------------------------------------
/apps/mobile/src/modules/settings/routes/navigateToPlanScreen.ts:
--------------------------------------------------------------------------------
1 | import { Navigation } from "@/src/lib/navigation/Navigation"
2 |
3 | export const navigateToPlanScreen = () => {
4 | return import("./Plan")
5 | .then(({ PlanScreen }) => {
6 | Navigation.rootNavigation.pushControllerView(PlanScreen)
7 | })
8 | .catch((error) => {
9 | if (__DEV__) {
10 | console.error("Failed to open plan screen", error)
11 | }
12 | throw error
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/apps/ssr/src/meta-handler.map.ts:
--------------------------------------------------------------------------------
1 | // This file is generated by `pnpm run meta`
2 | import i0 from "../client/pages/(login)/login/metadata"
3 | import i3 from "../client/pages/(main)/share/feeds/[id]/metadata"
4 | import i2 from "../client/pages/(main)/share/lists/[id]/metadata"
5 | import i1 from "../client/pages/(main)/share/users/[id]/metadata"
6 |
7 | export default {
8 | "/login": i0,
9 | "/share/users/:id": i1,
10 | "/share/lists/:id": i2,
11 | "/share/feeds/:id": i3,
12 | }
13 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Modules/PagerView/EnhancePageViewModule.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnhancePageViewModule.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/3/31.
6 | //
7 |
8 | import ExpoModulesCore
9 |
10 | public class EnhancePageViewModule: Module {
11 | public func definition() -> ModuleDefinition {
12 | Name("EnhancePageView")
13 |
14 | View(EnhancePageView.self) {
15 | }
16 | }
17 | }
18 |
19 |
20 | class EnhancePageView: ExpoView {
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Packages/ImageViewer_swift/SimpleImageDatasource.swift:
--------------------------------------------------------------------------------
1 | class SimpleImageDatasource:ImageDataSource {
2 |
3 | private(set) var imageItems:[ImageItem]
4 |
5 | init(imageItems: [ImageItem]) {
6 | self.imageItems = imageItems
7 | }
8 |
9 | func numberOfImages() -> Int {
10 | return imageItems.count
11 | }
12 |
13 | func imageItem(at index: Int) -> ImageItem {
14 | return imageItems[index]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/pressable/NativePressable.tsx:
--------------------------------------------------------------------------------
1 | import { Pressable } from "react-native"
2 |
3 | import type { NativePressableProps } from "./NativePressable.types"
4 |
5 | /**
6 | * In order to resolve the conflict between the gesture handling of the native view and the RCTSurfaceGestureHandler in React Native.
7 | *
8 | */
9 | export const NativePressable = ({ children, ...props }: NativePressableProps) => {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/platform-icon/collections/zotero.tsx:
--------------------------------------------------------------------------------
1 | import type { SVGProps } from "react"
2 |
3 | export function SimpleIconsZotero(props: SVGProps) {
4 | return (
5 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/scripts/skip-main-app-vercel-build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | LAST_DEPLOY_COMMIT=$(git rev-parse HEAD^)
4 |
5 | CHANGED_FILES=$(git diff --name-only $LAST_DEPLOY_COMMIT HEAD)
6 |
7 | ONLY_SERVER_CHANGES=true
8 | for file in $CHANGED_FILES; do
9 | if [[ $file != apps/ssr/* ]]; then
10 | ONLY_SERVER_CHANGES=false
11 | break
12 | fi
13 | done
14 |
15 | if [ "$ONLY_SERVER_CHANGES" = true ]; then
16 |
17 | echo "skip"
18 | exit 0
19 | else
20 | echo "continue"
21 | exit 1
22 | fi
23 |
--------------------------------------------------------------------------------
/apps/ssr/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/portal/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, PropsWithChildren } from "react"
2 | import { createPortal } from "react-dom"
3 |
4 | import { useRootPortal } from "./provider"
5 |
6 | export const RootPortal: FC<
7 | {
8 | to?: HTMLElement | null
9 | } & PropsWithChildren
10 | > = (props) => {
11 | const to = useRootPortal()
12 |
13 | if (props.to === null) return props.children
14 |
15 | return createPortal(props.children, props.to || to || document.body)
16 | }
17 |
--------------------------------------------------------------------------------
/packages/internal/database/src/drizzle/0005_tense_sleepwalker.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `entries` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `title` text,
4 | `url` text,
5 | `content` text,
6 | `description` text,
7 | `guid` text NOT NULL,
8 | `author` text,
9 | `author_url` text,
10 | `author_avatar` text,
11 | `inserted_at` integer NOT NULL,
12 | `published_at` integer NOT NULL,
13 | `media` text,
14 | `categories` text,
15 | `attachments` text,
16 | `extra` text,
17 | `language` text
18 | );
19 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.9.0.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.9.0
2 |
3 | ## Improvements
4 |
5 | - Removed the limit on the maximum number of views (b69a935)
6 | - Allow hiding the “All” view (0882e47)
7 | - Introduced a right-click menu for views, allowing quick access to hide or open settings (60dcd42)
8 | - Added a smooth sliding animation when opening entry details (1720ebb)
9 |
10 | ## No longer broken
11 |
12 | - Fixed an issue where the scrollbar didn’t reset when switching between entry details (21124f5)
13 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/setup-file.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import "fake-indexeddb/auto"
3 |
4 | import { enableMapSet } from "immer"
5 |
6 | globalThis.window = {
7 | location: new URL("https://example.com"),
8 | __dbIsReady: true,
9 | addEventListener: () => {},
10 | get navigator() {
11 | return globalThis.navigator
12 | },
13 | }
14 |
15 | if (!globalThis.navigator) {
16 | globalThis.navigator = {
17 | onLine: true,
18 | userAgent: "node",
19 | }
20 | }
21 | enableMapSet()
22 |
--------------------------------------------------------------------------------
/apps/mobile/ios/Folo/Images.xcassets/SplashScreenBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "color": {
5 | "components": {
6 | "alpha": "1.000",
7 | "blue": "1.00000000000000",
8 | "green": "1.00000000000000",
9 | "red": "1.00000000000000"
10 | },
11 | "color-space": "srgb"
12 | },
13 | "idiom": "universal"
14 | }
15 | ],
16 | "info": {
17 | "version": 1,
18 | "author": "expo"
19 | }
20 | }
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/overlay/Overlay.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react"
2 | import { FadeIn, FadeOut } from "react-native-reanimated"
3 |
4 | import { ReAnimatedPressable } from "../../common/AnimatedComponents"
5 |
6 | export const Overlay: FC<{ onPress: () => void }> = ({ onPress }) => {
7 | return (
8 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/icons/mgc/music_2_cute_fi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/internal/components/src/ui/navigation-menu/style.ts:
--------------------------------------------------------------------------------
1 | import { cva } from "class-variance-authority"
2 |
3 | export const navigationMenuTriggerStyle = cva(
4 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
5 | )
6 |
--------------------------------------------------------------------------------
/apps/desktop/changelog/0.2.1.md:
--------------------------------------------------------------------------------
1 | # What's new in v0.2.1
2 |
3 | ## New Features
4 |
5 | - Added zoom functionality for viewing pictures.
6 | - Set `Show Unread Only` as the default option.
7 | - Merged the redirect page with the login page.
8 | - Introduced entry conditions for actions.
9 | - Added `all` language filter in dicover page.
10 |
11 | ## Improvements
12 |
13 | - Enhanced the logic for multi-selection and dragging.
14 |
15 | ## Bug Fixes
16 |
17 | - Resolved issue with loading Instagram images.
18 |
--------------------------------------------------------------------------------
/apps/desktop/layer/main/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "node:url"
2 |
3 | import tsconfigPath from "vite-tsconfig-paths"
4 | import { defineProject } from "vitest/config"
5 |
6 | const __dirname = fileURLToPath(new URL(".", import.meta.url))
7 |
8 | export default defineProject({
9 | root: "./",
10 | test: {
11 | globals: true,
12 | environment: "node",
13 | },
14 |
15 | plugins: [
16 | tsconfigPath({
17 | projects: ["./tsconfig.json"],
18 | }),
19 | ],
20 | })
21 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/modules/ai-chat/components/displays/shared/EmptyState.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@follow/utils/utils"
2 |
3 | export interface EmptyStateProps {
4 | message: string
5 | icon?: string
6 | className?: string
7 | }
8 |
9 | export const EmptyState = ({ message, icon, className }: EmptyStateProps) => (
10 |
11 | {icon && {icon}}
12 | {message}
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/apps/mobile/src/components/ui/toast/ctx.tsx:
--------------------------------------------------------------------------------
1 | import type { PrimitiveAtom } from "jotai"
2 | import { createContext } from "react"
3 |
4 | import type { ToastProps, ToastRef } from "./types"
5 |
6 | export const ToastContainerContext = createContext>(null!)
7 |
8 | type Disposer = () => void
9 | interface ToastActionContext {
10 | register: (currentIndex: number, ref: ToastRef) => Disposer
11 | }
12 |
13 | export const ToastActionContext = createContext(null!)
14 |
--------------------------------------------------------------------------------
/packages/internal/store/src/modules/unread/types.ts:
--------------------------------------------------------------------------------
1 | export interface PublishAtTimeRangeFilter {
2 | startTime: number
3 | endTime: number
4 | }
5 |
6 | export interface InsertedBeforeTimeRangeFilter {
7 | insertedBefore: number
8 | }
9 |
10 | export interface UnreadUpdateOptions {
11 | reset?: boolean
12 | }
13 |
14 | export type FeedIdOrInboxHandle = string
15 | export type UnreadStoreModel = Record
16 | export interface UnreadState {
17 | data: UnreadStoreModel
18 | }
19 |
--------------------------------------------------------------------------------
/patches/jsonpointer.patch:
--------------------------------------------------------------------------------
1 | diff --git a/package.json b/package.json
2 | index a832ba9fc48f08300d8e5e595409c0488217af86..23af6ad8e16a8add0580151e8124f71451ebfc94 100644
3 | --- a/package.json
4 | +++ b/package.json
5 | @@ -23,8 +23,8 @@
6 | "engines": {
7 | "node": ">=0.10.0"
8 | },
9 | - "main": "./jsonpointer",
10 | - "typings": "jsonpointer.d.ts",
11 | + "main": "./jsonpointer.js",
12 | + "typings": "./jsonpointer.d.ts",
13 | "files": [
14 | "jsonpointer.js",
15 | "jsonpointer.d.ts"
16 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/desktop/layer/renderer/src/pages/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { withResponsiveComponent } from "@follow/components/utils/selector.js"
2 |
3 | import { MainDestopLayout } from "~/modules/app-layout/subscription-column/index"
4 |
5 | export const Component = withResponsiveComponent(
6 | () => Promise.resolve({ default: MainDestopLayout }),
7 | async () => {
8 | const { default: DownloadPage } = await import("~/modules/download")
9 | return { default: DownloadPage }
10 | },
11 | (w) => w < 768,
12 | )
13 |
--------------------------------------------------------------------------------
/apps/mobile/native/ios/Modules/TabBar/TabScreenView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabScreenView.swift
3 | // FollowNative
4 | //
5 | // Created by Innei on 2025/3/16.
6 | //
7 |
8 | import ExpoModulesCore
9 | import UIKit
10 |
11 | class TabScreenView: ExpoView {
12 | weak var ownerViewController: UIViewController?
13 |
14 | var icon: String?
15 | var activeIcon: String?
16 | var title: String?
17 |
18 | required init(appContext: AppContext? = nil) {
19 | super.init(appContext: appContext)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------