feat: mobile app architecture and initial screens (#2144)

* feat: initialize mobile app with Expo setup and configuration

* chore: update deps

* feat: update layout and integrate WebView for follow

* chore: update lock

* chore: lint

* chore: lock file

* feat: login with deeplink

* feat: auth

* feat: add Expo development script and refactor mobile app configuration

- Added a new development script for Expo in package.json.
- Refactored the app.config.ts to configure expo-router with a specified root.
- Removed unused files: _layout.tsx, auth.tsx, and index.tsx from the mobile app directory, streamlining the codebase.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: update dependencies and enhance mobile app configuration

- Added new dependencies: nativewind, swiftui-react-native, and tailwindcss to improve styling capabilities.
- Updated react-native-reanimated version to 3.16.5 for better performance.
- Refactored tailwind configuration to use baseTwConfig for consistency across applications.
- Enhanced mobile layout with color scheme toggle functionality and improved button styling.
- Included type definitions for nativewind in shim-env.d.ts.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: streamline color configuration and enhance authentication UI

- Updated tailwind configuration to simplify color definitions, removing unnecessary color mappings and using CSS variables for palette colors.
- Refactored authentication screen to utilize new color scheme and improved styling for better user experience.
- Introduced a function to retrieve current colors based on the system theme, enhancing dynamic styling capabilities.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: add tab

- Refactored app.config.ts to use resolved paths for icon assets, improving path management.
- Enhanced RootLayout component by adding additional Stack.Screen elements for better navigation structure.
- Removed unused authentication and index screens to streamline the codebase and improve maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: add new icons and update mobile app configuration

- Introduced multiple new icon components for enhanced UI representation in the mobile app.
- Updated package.json and pnpm-lock.yaml to include new dependencies and ensure proper package management.
- Refactored mobile screens (_layout.tsx, home.tsx, auth.tsx) for improved structure and navigation.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: init debug screen

- Added a new Link component in RootLayout for debugging purposes, improving navigation capabilities.
- Adjusted the position of the existing button in RootLayout to enhance UI layout.
- Refactored HeadlessLayout to ensure proper closing of Stack component.
- Updated home screen to include Link and removed unused components for cleaner code.

Signed-off-by: Innei <tukon479@gmail.com>
Signed-off-by: Innei <i@innei.in>

* feat: app route layer

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance mobile app layout and navigation

- Updated RootLayout to include a new Stack.Screen for "Add Subscription" with customized header styles.
- Refactored TabLayout to improve tab bar styling and adjusted icon sizes for better visual consistency.
- Replaced List component with ScrollView in FeedList, adding header actions for editing and navigation to the add screen.
- Introduced accentColor in the theme for consistent color usage across components.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: navigation theme

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update layout components and streamline navigation

- Removed unused headerTintColor from AppRootLayout for cleaner code.
- Simplified TabLayout by removing unnecessary screenOptions, enhancing readability.
- Updated RootLayout to import navigation themes from local theme file, improving theme management.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: update dependencies and refactor mobile app components

- Added new dependencies: @follow/constants, jotai, and zustand to enhance state management and component functionality.
- Updated package.json and pnpm-lock.yaml to reflect new dependencies and their versions.
- Removed the Logo component to streamline the codebase.
- Refactored FeedList and TabLayout components to improve layout and navigation, including the addition of themed blur views for better UI consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: integrate React Native Bottom Tabs

* refactor: use native bottom tab

* chore: jsx preserve for ts config

* Revert "refactor: use native bottom tab"

This reverts commit e9370470c9116bb7e0bf6675f1ddeba286189237.

* feat: enhance tab bar icons in TabLayout

- Updated TabLayout to use different icons based on tab focus state, improving user experience.
- Introduced SafariCuteFi and Setting7CuteFi icons for the "Discover" and "Settings" tabs, respectively.
- Adjusted icon sizes for better visual consistency across the tab bar.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: add withFollowAssets plugin for managing asset resources

* feat: add FollowWebView component

* chore: set base path to current directory in vite config

* refactor: rename withFollowAssets.js to with-follow-assets.js and add error handling for missing assets directory

* feat: debug for webview

Signed-off-by: Innei <tukon479@gmail.com>

* chore(lint): correct tailwind config for mobile

* fix: share cookie with webview

* fix: redirect to home for webview

* fix: bring cookie inject back

* feat: init db

Signed-off-by: Innei <tukon479@gmail.com>

* fix: inject js

* fix: add shared env

* feat: useAuthCookie

* fix: save token

* chore: update icon

* chore: add note

* feat: init auth client

* feat: simple auth flow

* refactor: use cookie-es

* chore: remove unstable_enablePackageExports

* chore: remove auth page

* chore: update dependencies and refactor storage management

- Added '@follow/shared' package to pnpm-lock and package.json.
- Updated 'hono' and 'ofetch' dependencies in package.json.
- Removed 'react-native-mmkv' and its related storage implementation.
- Refactored layout to use 'RootProviders' for better context management.
- Updated API fetch headers to include application name.
- Changed '@electron/node-gyp' resolution method to use git URL.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update dependencies and add subscriptions schema

- Added 'immer' dependency to package.json and pnpm-lock.yaml.
- Updated '@electron/node-gyp' resolution method to use tarball URL.
- Introduced 'subscriptionsTable' schema in the database for managing user subscriptions.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove unused config

* chore: remove unused code

* chore: signout for debug

* refactor: use kv as auth storage

* chore: remove expo-secure-store, see https://github.com/better-auth/better-auth/issues/928

* fix: logout from native

* feat: react-hook-form

* feat: simple ui

* chore: auto-fix linting and formatting issues

* feat: sn pro font

* chore: auto-fix linting and formatting issues

* feat: enhance database schema and API error handling; improve feed list functionality

* feat: update ThemedBlurView and enhance feed list with new subscription handling

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove login url env

* feat: implement navigation handling in FollowWebView

* feat: make social login work

* chore: update

* feat: update environment variables, add dayjs dependency, enhance database schema integration, and improve subscription handling with transaction support

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance tab layout and feed list with improved ThemedBlurView integration and dynamic tab height adjustment

- Updated tab bar background and style in TabLayout for better visual consistency.
- Integrated useBottomTabBarHeight in FeedList to dynamically adjust layout based on tab height.
- Enhanced SubscriptionList header background with ThemedBlurView for improved aesthetics.
- Added styles for tab container to maintain consistent appearance across screens.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: move video URL handling to utils

* feat: integrate utility functions for URL handling in FollowWebView

* chore: update lock

* fix: workaround potential null value in remoteSettings.updated

* feat: enhance FeedList layout with dynamic header and tab height adjustments

- Integrated useHeaderHeight to dynamically adjust the ScrollView's content inset based on the header height.
- Updated scrollIndicatorInsets to improve scrolling experience by accounting for safe area insets.
- Ensured consistent layout adjustments for better visual alignment with the tab bar.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update dependencies and configure babel-plugin-module-resolver

- Added babel-plugin-module-resolver version 5.0.2 to package.json for improved module resolution.
- Configured babel.config.js to use module-resolver with custom alias for es-toolkit.
- Updated pnpm-lock.yaml to reflect new dependencies and their resolutions.
- Refactored import statement in utils.ts to use the updated es-toolkit import path.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance database integration and add SQL support

- Added support for SQL file imports in the mobile app by configuring babel-plugin-inline-import and updating metro.config.js to recognize .sql file extensions.
- Introduced new database migrations for improved data management, including the addition of a new migration file.
- Updated the drizzle migration logic to include the new migration.
- Enhanced the database initialization process to support the new subscription service.
- Refactored the hydrate function to streamline database hydration and improve performance.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance database functionality

- Added expo-clipboard and expo-file-system dependencies to the mobile app for improved file handling and clipboard functionality.
- Implemented a new function to retrieve the SQLite database path using expo-file-system.
- Enhanced the DebugPanel to include options for clearing SQLite data and copying the database file location to the clipboard.
- Updated pnpm-lock.yaml and package.json to reflect the new dependencies and their versions.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: implement session token management and enhance DebugPanel

- Removed unused session token handling from apiFetch.
- Updated cookie management functions to include synchronous storage with kv for session tokens.
- Added functionality to get and set session tokens in the DebugPanel, allowing users to view and save session tokens.
- Improved user interface in DebugPanel for better session token management.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: handle keyboard

* feat: enhance layout and subscription service functionality

- Added BugCuteReIcon to the RootLayout for improved user interface.
- Updated the upsertMany method in SubscriptionService to handle conflicts during database insertions, ensuring data integrity by using SQL's onConflictDoUpdate.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update FeedList component with improved pressable interactions and remove unused code

- Deleted unused .gitkeep file in mobile modules directory.
- Enhanced SubscriptionItem to use Pressable for better user interaction feedback.
- Removed redundant text displays for subscription IDs and current view in SubscriptionList.
- Added state management for press interactions to visually indicate button presses.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: reduce bundle size

Signed-off-by: Innei <tukon479@gmail.com>

* fix: cookie

* fix: cookie name

* fix: update iOS URI for FollowWebView

* chore: eas link

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update @better-auth/expo

* chore: format

* chore: fix can not click debug button

* chore: add expo-drizzle-studio-plugin

* chore: turn off typechecked rules in eslint

* chore: pan debug button

Signed-off-by: Innei <tukon479@gmail.com>

* fix: dark mode in rn webview

* chore: update

* fix: detect safari in webview

* fix: try fix db init

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: adjust layout and improve tab view in FeedList component

- Reduced bottom view tab height from 45 to 35.
- Updated ScrollView content insets for better layout.
- Replaced static tab view with a horizontally scrollable tab view.
- Enhanced tab item styling for better visual feedback on selection.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: simplify FeedList component by removing unused code and optimizing imports

- Removed unused imports and components to streamline the FeedList.
- Consolidated the tab view logic and improved the overall structure.
- Enhanced readability by eliminating redundant code and simplifying the component's layout.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: enhance subscription sorting in useSubscriptionByView hook

- Updated useSubscriptionByView to accept a sorting parameter, defaulting to "alphabet".
- Implemented sorting logic for subscription IDs based on alphabetical order.
- Introduced a new helper function, sortByAlphabet, to compare subscriptions by title.
- Improved the overall functionality of the subscription list in the FeedList component.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: data schemas and improve feed and subscription handling

- Renamed FeedModel to FeedSchema and SubscriptionModel to SubscriptionSchema for consistency.
- Introduced new InboxSchema and ListSchema to enhance data structure.
- Updated FeedService and SubscriptionService to utilize the new schemas.
- Refactored the FeedList component to support grouped and ungrouped subscriptions.
- Removed unused Morph class and related types to clean up the codebase.
- Enhanced sorting logic for subscriptions in the useSubscriptionByView hook.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* fix: enhance FeedIcon styling for dark mode compatibility

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: enhance FeedList component with animated accordion for grouped subscriptions

- Introduced animated accordion functionality in the CategoryGrouped component to improve user experience when viewing grouped subscriptions.
- Replaced static toggle with AnimatedTouchableOpacity for better visual feedback.
- Updated the layout and styling of the CategoryGrouped component for improved aesthetics.
- Adjusted ScrollView in FeedList to include right scroll indicator insets for better layout.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* refactor: update database schema and migration files

- Removed obsolete SQL migration files for `bitter_bloodstrike` and `ancient_dexter_bennett`.
- Updated migration scripts to reflect new table structures and relationships.
- Enhanced `migrations.js` to import new migration files.
- Adjusted `_journal.json` and snapshot files to align with the new schema.
- Added error handling and logging in migration processes.
- Improved database initialization with logging capabilities.
- Refactored subscription handling in the store to accommodate new schema changes.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: enhance FeedList component with ItemPressable for improved interaction

- Replaced static Pressable components with ItemPressable in CategoryGrouped and SubscriptionItem for better touch handling.
- Updated the layout of CategoryGrouped to include a pressable area for category navigation.
- Added a new utility function, composeEventHandlers, to facilitate event handling in the utils package.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* refactor: restructure FeedIcon and enhance list handling in FeedList component

- Removed the FeedIcon component to streamline icon handling.
- Updated FeedList component to include a new ListList section for displaying lists.
- Introduced ListSubscriptionItem for rendering individual list items with fallback support for missing images.
- Enhanced state management in the list store to utilize a record structure for lists and list IDs.
- Improved subscription handling by adding list subscriptions to the subscription store.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: enhance FeedList and ViewTab components with new features and styling improvements

- Added a new StarItem component to the FeedList for collections navigation.
- Updated ListList and SubscriptionItem components for improved layout and spacing.
- Enhanced ViewTab with increased horizontal padding and a bottom border for better visual separation.
- Adjusted icon sizes in ViewTab for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: apple sign in, not work yet

* chore: bring type check lint back

* feat: enhance mobile app components and linting configuration

- Added a new ESLint rule for preventing leaked conditional rendering in React.
- Updated the initialization process in main.ts to ensure proper loading of the expo-router.
- Enhanced the FeedList component by integrating the HeaderBlur for improved header styling and added routing functionality in SubscriptionItem for better navigation.
- Modified the Add screen to include a Stack.Screen for improved navigation handling.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: enable no-inline-styles

* chore: update

* chore: auto-fix linting and formatting issues

* feat: enhance feed-list functionality with new components and dependencies

- Added react-native-pager-view for improved navigation between feed views.
- Introduced ViewPageCurrentViewContext for managing the current view state in the feed list.
- Updated SubscriptionList to utilize PagerView for dynamic content rendering.
- Enhanced ListList to use the new context for better state management.
- Refactored imports for consistency and clarity.

This update improves the user experience by allowing seamless transitions between different feed types.

* feat: improve ViewTab component with animated tab indicator and dynamic scrolling

- Added animated indicator for the currently selected tab, enhancing visual feedback.
- Implemented dynamic scrolling to the selected tab using ScrollView reference.
- Introduced state management for tab widths and positions to support the animation.
- Updated tab rendering to capture layout dimensions for improved responsiveness.

This update enhances the user experience by providing a smoother and more interactive tab navigation.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: lockfile

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: video URL transformation

* fix: web dev

* chore: add URL linking functionality to Add screen

* chore: expo update config

* feat: enhance feed-list and configuration with new features and optimizations

- Updated app configuration to support dark mode for splash screen background.
- Refactored feed-list components to utilize Jotai for state management, improving performance and readability.
- Introduced new hooks for managing current view state, enhancing the user experience.
- Implemented animated tab indicators and dynamic scrolling in ViewTab for better navigation feedback.
- Cleaned up imports and improved overall code structure for maintainability.

These changes collectively enhance the functionality and user experience of the feed-list feature in the mobile app.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: integrate gesture handling and state management for feed view

- Added Jotai state management for current feed view, allowing dynamic updates.
- Implemented double-tap gesture to switch to the Articles view, enhancing user interaction.
- Wrapped the main provider in GestureHandlerRootView to support gesture handling.
- Updated tab layout to utilize gesture detection for improved navigation experience.

These changes collectively enhance the functionality and interactivity of the feed-list feature in the mobile app.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: update prepare script

* chore: prepare for rn web build

* feat: add inbox management and improve list handling in feed view

- Introduced InboxList and InboxItem components to display inboxes within the Articles view.
- Enhanced state management by implementing useInboxSubscription for fetching inbox data.
- Updated ListService and SubscriptionService to use upsertManyInSession for improved session handling.
- Refactored inbox store to utilize createZustandStore for better state management.
- Added inboxIdByView to SubscriptionState for managing inbox subscriptions by view type.

These changes collectively enhance the feed-list feature by integrating inbox management, improving data handling, and optimizing state management in the mobile app.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* feat: enhance mobile app with new context menu and unread management

- Added `react-native-context-menu-view` version 1.16.0 to support context menus in the mobile app.
- Implemented new `unread` table schema to track unread counts for subscriptions.
- Updated `FeedList` and `ViewTab` components to display unread counts dynamically.
- Refactored inbox management to improve session handling with `upsertManyInSession`.
- Enhanced state management by integrating unread counts into subscription hooks.

These changes collectively improve user experience by providing context menus and better unread message tracking in the mobile application.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: integrate context menu and enhance unread management in feed list

- Added ContextMenu component to ViewTab for marking all items as read.
- Updated FeedList and ViewTab components to utilize context menus for improved user interaction.
- Enhanced unread management by implementing markViewAsRead functionality in UnreadSyncService.
- Refactored subscription getters and hooks to streamline data retrieval by view type.

These changes collectively improve user experience by providing context menus for actions and better management of unread items in the mobile application.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance feed list with swipeable actions and performance improvements

- Integrated swipeable functionality in SubscriptionItem for better user interaction, allowing users to unsubscribe or read items directly.
- Refactored styles for accordion icons to improve layout consistency.
- Optimized animated styles in ViewTab for smoother transitions and reduced performance issues.
- Consolidated spring configuration for animation to enhance maintainability.

These changes collectively improve the user experience by providing intuitive swipe actions and enhancing the performance of the feed list in the mobile application.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: proxy creation to use evalCode for improved reusability

* refactor: update Add screen to use router for navigation and handle deep linking

* feat: implement deep linking support and refactor navigation handling in FollowWebView

* fix: use hash router for rn web

* chore: fill submit info

* chore: autoIncrement version

* fix: remove unnecessary width class to prevent unexpected scrolling

* feat: improve login page ui

* fix: disable state

* refactor: enhance ViewTab layout and header background in FeedList

Signed-off-by: Innei <tukon479@gmail.com>

* feat: integrate react-native-reanimated and enhance feed list functionality

- Added react-native-reanimated plugin to Babel configuration for improved animation support.
- Updated metro configuration to wrap with reanimated settings.
- Refactored AccordionItem to use withTiming for smoother animations.
- Introduced JotaiPersistSyncStorage for persistent state management in feed list atoms.
- Enhanced SubscriptionList to utilize FlashList for better performance and rendering.
- Implemented sorting functionality in feed list with atom hooks for sort method and order.
- Improved ViewTab component to handle tab scrolling more effectively.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance feed list and debug panel functionality

- Added color scheme support in InboxItem for dynamic icon coloring based on theme.
- Updated FlashList padding to improve layout consistency.
- Introduced a new "Reload App" button in the DebugPanel for easier app reloading.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: sort

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance ItemPressable and refactor feed list interaction

Signed-off-by: Innei <tukon479@gmail.com>

* fix: padding safe for webview

* chore: update FollowWebView background style

* fix: enable hoverOnlyWhenSupported for rn web build

* feat: subscription management ctx menu

- Added '@follow/hooks' and 'usehooks-ts' as workspace dependencies in pnpm-lock.yaml and package.json.
- Refactored AccordionItem to use withSpring for improved animation performance.
- Introduced SubscriptionItemContextMenu for better context menu handling in feed list.
- Updated subscription model to handle 'isPrivate' as a boolean.
- Enhanced subscription store with new utility functions for managing subscription IDs and added patch functionality.
- Improved reset functionality in subscription store to clear state by view.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update API URL handling and enhance subscription management

- Replaced direct usage of process.env.EXPO_PUBLIC_API_URL with a new getApiUrl() function across multiple files for improved maintainability.
- Updated SubscriptionItemContextMenu to SubscriptionFeedItemContextMenu for better clarity in context menu handling.
- Implemented a delete method in SubscriptionService to handle subscription removals and associated cleanup for feeds, lists, and inboxes.
- Enhanced the unsubscribe functionality in SubscriptionSyncService to ensure proper state management during subscription deletions.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: user management features and list and category ctx menu

- Added a new migration script (m0004) for "majestic_thunderbolt_ross" to the SQLite migrations.
- Updated the journal JSON to include the new migration entry.
- Introduced a new users table schema to manage user data, including fields for id, email, handle, name, image, and isMe.
- Refactored context menu actions in feeds to improve clarity and functionality.
- Removed deprecated feed list components and consolidated related logic for better maintainability.
- Updated RootProviders to ensure proper context management across the application.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance Add screen and layout for better user experience

- Updated the AppRootLayout to include modal presentation for the "Add Subscription" screen.
- Refactored the Add screen to improve navigation and user interaction, including a new header with close and search icons.
- Introduced a state for managing the feed URL input, enhancing the user interface with appropriate styles and functionality.
- Added new color handling in the theme to support disabled states and improve accessibility.

Signed-off-by: Innei <tukon479@gmail.com>

* style: update input field styling in Add screen for improved dark mode support

- Modified the className of the URL input field in the Add screen to enhance its appearance in dark mode, ensuring better visibility and user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: update mobile app configuration and enhance subscription list interaction

- Added application category type to the iOS infoPlist for better app classification.
- Increased hitSlop on the AnimatedTouchableOpacity in the subscription list for improved touch responsiveness.
- Un-commented the DebugButton in the layout to ensure it is always visible for debugging purposes.

These changes aim to enhance the user experience and improve app visibility in app stores.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Signed-off-by: Innei <i@innei.in>
Co-authored-by: lawvs <18554747+lawvs@users.noreply.github.com>
Co-authored-by: Innei <tukon479@gmail.com>
Co-authored-by: Innei <i@innei.in>
Co-authored-by: hyoban <hyoban@users.noreply.github.com>
Co-authored-by: Innei <Innei@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2024-12-26 00:06:03 +08:00 committed by GitHub
parent 8dbd7003d3
commit 4b6a0fca2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
317 changed files with 18445 additions and 269 deletions

View File

@ -6,3 +6,7 @@ VITE_BUILD_TYPE=production
VITE_INBOXES_EMAIL=@follow.re
VITE_OPENPANEL_CLIENT_ID=
VITE_OPENPANEL_API_URL=
EXPO_PUBLIC_API_URL=https://api.follow.is
EXPO_PUBLIC_OPENPANEL_CLIENT_ID=
EXPO_PUBLIC_OPENPANEL_API_URL=

18
.vscode/settings.json vendored
View File

@ -13,6 +13,8 @@
["[a-zA-Z]+[cC]lass[nN]ame[\"'`]?:\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"],
["[a-zA-Z]+[cC]lass[nN]ame\\s*=\\s*[\"'`]([^\"'`]*)[\"'`]", "([^\"'`]*)"]
],
"typescript.tsserver.maxTsServerMemory": 8096,
"typescript.tsserver.nodePath": "node",
// If you do not want to autofix some rules on save
// You can put this in your user settings or workspace settings
"eslint.codeActionsOnSave.rules": [
@ -54,22 +56,6 @@
{
"rule": "unused-imports/no-unused-imports",
"severity": "off"
},
{
"rule": "@typescript-eslint/require-await",
"severity": "info"
},
{
"rule": "@typescript-eslint/await-thenable",
"severity": "info"
},
{
"rule": "@typescript-eslint/no-floating-promises",
"severity": "info"
},
{
"rule": "@typescript-eslint/no-misused-promises",
"severity": "info"
}
],
"cSpell.words": ["Hydable", "rsshub", "Русский"],

1
apps/mobile/.env Symbolic link
View File

@ -0,0 +1 @@
../../.env

50
apps/mobile/README.md Normal file
View File

@ -0,0 +1,50 @@
# Welcome to your Expo app 👋
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

97
apps/mobile/app.config.ts Normal file
View File

@ -0,0 +1,97 @@
import { resolve } from "node:path"
import type { ConfigContext, ExpoConfig } from "expo/config"
// const roundedIconPath = resolve(__dirname, "../../resources/icon.png")
const iconPath = resolve(__dirname, "./assets/icon.png")
const adaptiveIconPath = resolve(__dirname, "./assets/adaptive-icon.png")
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
extra: {
eas: {
projectId: "a6335b14-fb84-45aa-ba80-6f6ab8926920",
},
},
owner: "follow",
updates: {
url: "https://u.expo.dev/a6335b14-fb84-45aa-ba80-6f6ab8926920",
},
runtimeVersion: {
policy: "appVersion",
},
name: "Follow",
slug: "follow",
version: "1.0.0",
orientation: "portrait",
icon: iconPath,
scheme: "follow",
userInterfaceStyle: "automatic",
newArchEnabled: true,
ios: {
supportsTablet: true,
bundleIdentifier: "is.follow",
usesAppleSignIn: true,
infoPlist: {
LSApplicationCategoryType: "public.app-category.news",
},
},
android: {
package: "is.follow",
adaptiveIcon: {
foregroundImage: adaptiveIconPath,
backgroundColor: "#FF5C00",
},
},
web: {
bundler: "metro",
output: "static",
favicon: iconPath,
},
plugins: [
[
"expo-router",
{
root: "./src/screens",
},
],
[
"expo-splash-screen",
{
backgroundColor: "#ffffff",
dark: {
backgroundColor: "#000000",
},
},
],
[
"expo-font",
{
fonts: [
"./assets/font/sn-pro/SNPro-Black.otf",
"./assets/font/sn-pro/SNPro-BlackItalic.otf",
"./assets/font/sn-pro/SNPro-Bold.otf",
"./assets/font/sn-pro/SNPro-BoldItalic.otf",
"./assets/font/sn-pro/SNPro-Heavy.otf",
"./assets/font/sn-pro/SNPro-HeavyItalic.otf",
"./assets/font/sn-pro/SNPro-Light.otf",
"./assets/font/sn-pro/SNPro-LightItalic.otf",
"./assets/font/sn-pro/SNPro-Medium.otf",
"./assets/font/sn-pro/SNPro-MediumItalic.otf",
"./assets/font/sn-pro/SNPro-Regular.otf",
"./assets/font/sn-pro/SNPro-RegularItalic.otf",
"./assets/font/sn-pro/SNPro-Semibold.otf",
"./assets/font/sn-pro/SNPro-SemiboldItalic.otf",
"./assets/font/sn-pro/SNPro-Thin.otf",
"./assets/font/sn-pro/SNPro-ThinItalic.otf",
],
},
],
"expo-apple-authentication",
[require("./scripts/with-follow-assets.js")],
],
experiments: {
typedRoutes: true,
},
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
apps/mobile/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,21 @@
const path = require("node:path")
module.exports = function (api) {
api.cache(true)
return {
presets: [["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel"],
plugins: [
["inline-import", { extensions: [".sql"] }],
[
"module-resolver",
{
alias: {
"es-toolkit": path.resolve(__dirname, "../../node_modules/es-toolkit/dist/index.js"),
},
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
],
"react-native-reanimated/plugin",
],
}
}

View File

@ -0,0 +1,8 @@
import { defineConfig } from "drizzle-kit"
export default defineConfig({
dialect: "sqlite",
driver: "expo",
schema: "./src/database/schemas/index.ts",
out: "./drizzle",
})

View File

@ -0,0 +1,43 @@
CREATE TABLE `feeds` (
`id` text PRIMARY KEY NOT NULL,
`title` text,
`url` text NOT NULL,
`description` text,
`image` text,
`error_at` text,
`site_url` text,
`owner_user_id` text,
`error_message` text
);
--> statement-breakpoint
CREATE TABLE `inboxes` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`title` text
);
--> statement-breakpoint
CREATE TABLE `lists` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`title` text NOT NULL,
`feed_ids` text,
`description` text,
`view` integer NOT NULL,
`image` text,
`fee` integer,
`owner_user_id` text
);
--> statement-breakpoint
CREATE TABLE `subscriptions` (
`feed_id` text,
`list_id` text,
`inbox_id` text,
`user_id` text NOT NULL,
`view` integer NOT NULL,
`is_private` integer NOT NULL,
`title` text,
`category` text,
`created_at` text,
`type` text NOT NULL,
`id` text PRIMARY KEY NOT NULL
);

View File

@ -0,0 +1 @@
ALTER TABLE `inboxes` DROP COLUMN `user_id`;

View File

@ -0,0 +1,5 @@
CREATE TABLE `unread` (
`id` text PRIMARY KEY NOT NULL,
`subscription_id` text NOT NULL,
`count` integer NOT NULL
);

View File

@ -0,0 +1,10 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_unread` (
`subscription_id` text PRIMARY KEY NOT NULL,
`count` integer NOT NULL
);
--> statement-breakpoint
INSERT INTO `__new_unread`("subscription_id", "count") SELECT "subscription_id", "count" FROM `unread`;--> statement-breakpoint
DROP TABLE `unread`;--> statement-breakpoint
ALTER TABLE `__new_unread` RENAME TO `unread`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,8 @@
CREATE TABLE `users` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`handle` text,
`name` text,
`image` text,
`is_me` integer NOT NULL
);

View File

@ -0,0 +1,282 @@
{
"version": "6",
"dialect": "sqlite",
"id": "f277136a-3ff5-4d70-8509-c1f40f0aa4ca",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,275 @@
{
"version": "6",
"dialect": "sqlite",
"id": "03bb7af3-104a-4220-aa4d-46abfac83303",
"prevId": "f277136a-3ff5-4d70-8509-c1f40f0aa4ca",
"tables": {
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,306 @@
{
"version": "6",
"dialect": "sqlite",
"id": "38eb9a81-9c8d-4f2f-8c98-8f065decf7ed",
"prevId": "03bb7af3-104a-4220-aa4d-46abfac83303",
"tables": {
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"unread": {
"name": "unread",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"subscription_id": {
"name": "subscription_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,299 @@
{
"version": "6",
"dialect": "sqlite",
"id": "b74f8057-78c5-4675-94dd-e41d94674ef7",
"prevId": "38eb9a81-9c8d-4f2f-8c98-8f065decf7ed",
"tables": {
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"unread": {
"name": "unread",
"columns": {
"subscription_id": {
"name": "subscription_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,351 @@
{
"version": "6",
"dialect": "sqlite",
"id": "fc790c48-4ade-4648-a1b4-0a3e9faf65c5",
"prevId": "b74f8057-78c5-4675-94dd-e41d94674ef7",
"tables": {
"feeds": {
"name": "feeds",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_at": {
"name": "error_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"site_url": {
"name": "site_url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"inboxes": {
"name": "inboxes",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"lists": {
"name": "lists",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feed_ids": {
"name": "feed_ids",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fee": {
"name": "fee",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner_user_id": {
"name": "owner_user_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"subscriptions": {
"name": "subscriptions",
"columns": {
"feed_id": {
"name": "feed_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"list_id": {
"name": "list_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inbox_id": {
"name": "inbox_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"view": {
"name": "view",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"is_private": {
"name": "is_private",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"category": {
"name": "category",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"unread": {
"name": "unread",
"columns": {
"subscription_id": {
"name": "subscription_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"count": {
"name": "count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"handle": {
"name": "handle",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"is_me": {
"name": "is_me",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,41 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1734622443265,
"tag": "0000_harsh_shiva",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1734622716320,
"tag": "0001_bored_hobgoblin",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1734962295258,
"tag": "0002_smart_power_man",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1734963906590,
"tag": "0003_known_roland_deschain",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1735137187415,
"tag": "0004_majestic_thunderbolt_ross",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,19 @@
// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo
import m0000 from "./0000_harsh_shiva.sql"
import m0001 from "./0001_bored_hobgoblin.sql"
import m0002 from "./0002_smart_power_man.sql"
import m0003 from "./0003_known_roland_deschain.sql"
import m0004 from "./0004_majestic_thunderbolt_ross.sql"
import journal from "./meta/_journal.json"
export default {
journal,
migrations: {
m0000,
m0001,
m0002,
m0003,
m0004,
},
}

24
apps/mobile/eas.json Normal file
View File

@ -0,0 +1,24 @@
{
"cli": {
"appVersionSource": "remote"
},
"build": {
"preview": {
"channel": "preview",
"distribution": "internal"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"appleId": "rss3@diygod.me",
"ascAppId": "6739802604",
"appleTeamId": "NCLY2BLCL2"
}
}
}
}

View File

@ -0,0 +1,10 @@
const { getDefaultConfig } = require("expo/metro-config")
const { withNativeWind } = require("nativewind/metro")
const { wrapWithReanimatedMetroConfig } = require("react-native-reanimated/metro-config")
const config = getDefaultConfig(__dirname, { isCSSEnabled: true })
config.resolver.sourceExts.push("sql")
module.exports = wrapWithReanimatedMetroConfig(
withNativeWind(config, { input: "./src/global.css" }),
)

3
apps/mobile/nativewind-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

89
apps/mobile/package.json Normal file
View File

@ -0,0 +1,89 @@
{
"name": "@follow/mobile",
"version": "1.0.0",
"private": true,
"main": "src/main.ts",
"scripts": {
"android": "expo run:android",
"db:generate": "drizzle-kit generate",
"dev": "npm run start",
"ios": "expo run:ios",
"ios:device": "expo run:ios --device",
"lint": "expo lint",
"prepare": "cd ../.. && shx test -d out/rn-web || pnpm run build:rn-web",
"start": "expo start --dev-client",
"web": "expo start --web"
},
"dependencies": {
"@better-auth/expo": "1.0.23-beta.2",
"@expo/vector-icons": "^14.0.2",
"@follow/constants": "workspace:*",
"@follow/hooks": "workspace:*",
"@follow/shared": "workspace:*",
"@follow/utils": "workspace:*",
"@hookform/resolvers": "3.9.1",
"@react-native-cookies/cookies": "^6.2.1",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@shopify/flash-list": "1.7.1",
"@tanstack/react-query": "5.62.3",
"better-auth": "1.1.3",
"cookie-es": "^1.2.2",
"dayjs": "1.11.13",
"expo": "52.0.18",
"expo-apple-authentication": "~7.1.2",
"expo-blur": "~14.0.1",
"expo-build-properties": "^0.13.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.3",
"expo-file-system": "~18.0.6",
"expo-font": "~13.0.1",
"expo-haptics": "~14.0.0",
"expo-linear-gradient": "~14.0.1",
"expo-linking": "~7.0.3",
"expo-router": "4.0.11",
"expo-sharing": "~13.0.0",
"expo-splash-screen": "~0.29.18",
"expo-sqlite": "15.0.3",
"expo-status-bar": "~2.0.0",
"expo-symbols": "~0.2.0",
"expo-system-ui": "~4.0.6",
"expo-updates": "~0.26.10",
"expo-web-browser": "~14.0.1",
"hono": "4.6.13",
"immer": "10.1.1",
"jotai": "2.10.3",
"nativewind": "4.1.23",
"ofetch": "1.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "7.54.0",
"react-native": "0.76.5",
"react-native-context-menu-view": "1.16.0",
"react-native-gesture-handler": "~2.20.2",
"react-native-keyboard-controller": "^1.15.0",
"react-native-pager-view": "6.6.1",
"react-native-reanimated": "~3.16.5",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"swiftui-react-native": "6.3.3",
"tailwindcss": "3.4.16",
"usehooks-ts": "3.1.0",
"zod": "3.23.8",
"zustand": "5.0.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "^18.3.12",
"@types/react-test-renderer": "^18.3.0",
"babel-plugin-inline-import": "3.0.0",
"babel-plugin-module-resolver": "5.0.2",
"drizzle-kit": "0.30.1",
"eas-cli": "14.2.0",
"expo-drizzle-studio-plugin": "0.1.1",
"react-test-renderer": "18.3.1"
}
}

View File

@ -0,0 +1,98 @@
/*
* If you add an asset you need to run `npx expo prebuild`
* If you rename or delete an asset you need to run `npx expo prebuild --clean` to delete them in your android and ios folder as well.
*
* This plugin is inspired by the following plugins:
* - [expo-custom-assets](https://github.com/Malaa-tech/expo-custom-assets)
* - [spacedrive](https://github.com/spacedriveapp/spacedrive/blob/main/apps/mobile/scripts/withRiveAssets.js)
*/
const { withDangerousMod, withXcodeProject, IOSConfig } = require("@expo/config-plugins")
const fs = require("node:fs")
const path = require("node:path")
const followRoot = path.resolve(__dirname, "..", "..", "..")
// Specify the source directory of your assets
const ASSET_SOURCE_DIR = path.join("out", "rn-web")
const IOS_GROUP_NAME = "Assets"
const withFollowAssets = (config) => {
if (!fs.existsSync(path.resolve(followRoot, ASSET_SOURCE_DIR))) {
throw new Error("Assets source directory not found! Do you forget to run `pnpm build:rn-web`?")
}
config = addAndroidResources(config)
config = addIOSResources(config)
return config
}
// Code inspired by https://github.com/rive-app/rive-react-native/issues/185#issuecomment-1593396573
function addAndroidResources(config) {
return withDangerousMod(config, [
"android",
async (config) => {
// Get the path to the Android project directory
const { projectRoot } = config.modRequest
// Get the path to the Android resources directory
const resDir = path.join(projectRoot, "android", "app", "src", "main", "res")
// Create the 'raw' directory if it doesn't exist
const rawDir = path.join(resDir, "raw")
fs.mkdirSync(rawDir, { recursive: true })
// Get the path to the assets directory
const assetSourcePath = path.join(followRoot, ASSET_SOURCE_DIR)
// Retrieve all files in the assets directory
// const assetFiles = fs.readdirSync(assetSourcePath)
// Move asset file to the resources 'raw' directory
fs.cpSync(assetSourcePath, rawDir, { recursive: true })
// Move each asset file to the resources 'raw' directory
// for (const assetFile of assetFiles) {
// const srcAssetPath = path.join(assetSourcePath, assetFile)
// const destAssetPath = path.join(rawDir, assetFile)
// fs.copyFileSync(srcAssetPath, destAssetPath)
// }
return config
},
])
}
// Code inspired by https://github.com/expo/expo/blob/61f8cf8d4b3cf5f8bf61f346476ebdb4aff40545/packages/expo-font/plugin/src/withFontsIos.ts
function addIOSResources(config) {
return withXcodeProject(config, async (config) => {
const project = config.modResults
const { platformProjectRoot } = config.modRequest
// Create Assets group in project
IOSConfig.XcodeUtils.ensureGroupRecursively(project, IOS_GROUP_NAME)
// Get filepaths
const assetSourcePath = path.resolve(followRoot, ASSET_SOURCE_DIR)
// Add assets to group
addIOSResourceFile(project, platformProjectRoot, [assetSourcePath])
return config
})
function addIOSResourceFile(project, platformRoot, assetFilesPaths) {
for (const assetFile of assetFilesPaths) {
const riveFilePath = path.relative(platformRoot, assetFile)
IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: riveFilePath,
groupName: IOS_GROUP_NAME,
project,
isBuildFile: true,
verbose: true,
})
}
}
}
module.exports = withFollowAssets

5
apps/mobile/shim-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="nativewind/types" />
declare namespace NodeJS {
export type ProcessEnv = Record<string, string | undefined>
}

View File

@ -0,0 +1,11 @@
import { createAtomHooks } from "@follow/utils"
import { atomWithStorage } from "jotai/utils"
import { JotaiPersistSyncStorage } from "../lib/jotai"
type Environment = "prod" | "dev" | "staging"
export const [, , useEnvironment, , getEnvironment, setEnvironment] = createAtomHooks(
atomWithStorage<Environment>("debug-env", "prod", JotaiPersistSyncStorage, {
getOnInit: true,
}),
)

View File

@ -0,0 +1,165 @@
import { callWebviewExpose } from "@follow/shared"
import { parseSafeUrl, transformVideoUrl } from "@follow/utils"
import * as Linking from "expo-linking"
import type { RefObject } from "react"
import { useCallback, useEffect, useState } from "react"
import { Platform } from "react-native"
import type { WebViewNavigation, WebViewProps } from "react-native-webview"
import { WebView } from "react-native-webview"
import { signOut } from "@/src/lib/auth"
import { useOpenLink } from "@/src/lib/hooks/useOpenLink"
const presetUri = Platform.select({
ios: "rn-web/index.html",
android: "file:///android_asset/raw/index.html",
default: "https://app.follow.is",
})
const allowHosts = new Set(["app.follow.is"])
interface FollowWebViewProps extends WebViewProps {
customUrl?: string
}
const injectedJavaScript = [
`window.__RN__ = true`,
// TODO use more reliable way to detect the page is ready
`window.setTimeout(() => window.ReactNativeWebView.postMessage("ready"), 2000)`,
].join(";")
const styles = {
// https://github.com/react-native-webview/react-native-webview/issues/318#issuecomment-503979211
webview: { backgroundColor: "transparent" },
webviewContainer: { width: "100%" },
} as const
export const FollowWebView = ({
webViewRef,
customUrl,
...props
}: { webViewRef: RefObject<WebView> } & FollowWebViewProps) => {
const { onNavigationStateChange } = useWebViewNavigation({ webViewRef })
const [ready, setReady] = useState(false)
useDeepLink({ webViewRef, ready })
return (
<WebView
ref={webViewRef}
source={{ uri: customUrl ?? presetUri }}
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
sharedCookiesEnabled
originWhitelist={["*"]}
allowUniversalAccessFromFileURLs
startInLoadingState
allowsBackForwardNavigationGestures
/* Open chrome://inspect/#devices , or Development menu on Safari to debug the WebView. https://github.com/react-native-webview/react-native-webview/blob/master/docs/Debugging.md#debugging-webview-contents */
webviewDebuggingEnabled={__DEV__}
style={styles.webview}
containerStyle={styles.webviewContainer}
onNavigationStateChange={onNavigationStateChange}
// onLoad={() => setReady(false)}
// onLoadProgress={({ nativeEvent }) => setProgress(nativeEvent.progress)}
onError={(e) => {
console.error("WebView error:", e)
}}
onContentProcessDidTerminate={() => webViewRef.current?.reload()}
onMessage={(e) => {
const message = e.nativeEvent.data
if (message === "sign-out") {
signOut()
return
}
if (message === "ready") {
setReady(true)
return
}
}}
{...props}
/>
)
}
const useWebViewNavigation = ({ webViewRef }: { webViewRef: RefObject<WebView> }) => {
const openLink = useOpenLink()
const onNavigationStateChange = useCallback(
(newNavState: WebViewNavigation) => {
const { url: urlStr } = newNavState
const url = parseSafeUrl(urlStr)
if (!url) return
if (url.protocol === "file:") return
if (allowHosts.has(url.host)) return
webViewRef.current?.stopLoading()
const formattedUrl = transformVideoUrl({ url: urlStr })
if (formattedUrl) {
openLink(formattedUrl)
return
}
openLink(urlStr)
},
[openLink, webViewRef],
)
return { onNavigationStateChange }
}
// We only need to handle deep link at the first time the app is opened
let lastInitialUrl: string | null = null
const useDeepLink = ({
webViewRef,
ready = true,
}: {
webViewRef: RefObject<WebView>
ready?: boolean
}) => {
const handleDeepLink = useCallback(
async (url: string) => {
const { queryParams } = Linking.parse(url)
if (!queryParams) {
console.error("Invalid URL! queryParams is not available", url)
return
}
const id = queryParams["id"] ?? undefined
const isList = queryParams["type"] === "list"
// const urlParam = queryParams["url"] ?? undefined
if (!id || typeof id !== "string") {
console.error("Invalid URL! id is not a string", url)
return
}
const injectJavaScript = webViewRef.current?.injectJavaScript
if (!injectJavaScript) {
console.error("injectJavaScript is not available")
return
}
callWebviewExpose(injectJavaScript).follow({ id, isList })
},
[webViewRef],
)
// https://reactnative.dev/docs/linking#handling-deep-links
//
// The handler added with Linking.addEventListener() is only triggered when app is already open.
//
// When the app is not already open and the deep link triggered app launch,
// the URL can be obtained with Linking.getInitialURL().
useEffect(() => {
if (!ready) return
const getUrlAsync = async () => {
const initialUrl = await Linking.getInitialURL()
if (!initialUrl) return
if (initialUrl === lastInitialUrl) return
lastInitialUrl = initialUrl
handleDeepLink(initialUrl)
}
getUrlAsync()
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener
const deepLinkListener = Linking.addEventListener("url", (event: { url: string }) => {
handleDeepLink(event.url)
})
return () => deepLinkListener.remove()
}, [handleDeepLink, ready])
}

View File

@ -0,0 +1,16 @@
import { StyleSheet } from "react-native"
import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
const node = (
<ThemedBlurView
style={{
...StyleSheet.absoluteFillObject,
overflow: "hidden",
backgroundColor: "transparent",
}}
/>
)
export const HeaderBlur = () => {
return node
}

View File

@ -0,0 +1,14 @@
import type { BlurViewProps } from "expo-blur"
import { BlurView } from "expo-blur"
import { useColorScheme } from "nativewind"
import type { FC } from "react"
export const ThemedBlurView: FC<BlurViewProps> = ({ tint, ...rest }) => {
const { colorScheme } = useColorScheme()
return (
<BlurView
tint={colorScheme === "light" ? "systemMaterialLight" : "systemMaterialDark"}
{...rest}
/>
)
}

View File

@ -0,0 +1,6 @@
import type { TextProps } from "react-native"
import { Text } from "react-native"
export function ThemedText(props: TextProps) {
return <Text {...props} className={`font-sn text-text ${props.className}`} />
}

View File

@ -0,0 +1,65 @@
import type { StyleProp, ViewStyle } from "react-native"
import { StyleSheet, View } from "react-native"
import type { SharedValue } from "react-native-reanimated"
import Animated, {
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from "react-native-reanimated"
export function AccordionItem({
isExpanded,
children,
viewKey,
style,
wrapperStyle,
wrapperClassName,
}: {
isExpanded: SharedValue<boolean>
children: React.ReactNode
viewKey: string
style?: StyleProp<ViewStyle>
wrapperStyle?: StyleProp<ViewStyle>
wrapperClassName?: string
duration?: number
}) {
const height = useSharedValue(0)
const derivedHeight = useDerivedValue(() =>
withSpring(height.value * Number(isExpanded.value), {
damping: 30,
stiffness: 100,
}),
)
const bodyStyle = useAnimatedStyle(() => ({
height: derivedHeight.value,
}))
return (
<Animated.View key={`accordionItem_${viewKey}`} style={[styles.animatedView, bodyStyle, style]}>
<View
onLayout={(e) => {
height.value = e.nativeEvent.layout.height
}}
style={[styles.wrapper, wrapperStyle]}
className={wrapperClassName}
>
{children}
</View>
</Animated.View>
)
}
const styles = StyleSheet.create({
wrapper: {
width: "100%",
position: "absolute",
display: "flex",
flex: 1,
},
animatedView: {
width: "100%",
overflow: "hidden",
},
})

View File

@ -0,0 +1,7 @@
import type { FC } from "react"
import type { ContextMenuProps } from "./index.ios"
export const ContextMenu: FC<ContextMenuProps> = () => {
throw new Error("ContextMenu not implemented on Android")
}

View File

@ -0,0 +1 @@
export { default as ContextMenu, type ContextMenuProps } from "react-native-context-menu-view"

View File

@ -0,0 +1 @@
export * from "./index.ios"

View File

@ -0,0 +1,52 @@
import { getBackgroundGradient, isCJKChar } from "@follow/utils"
import { LinearGradient } from "expo-linear-gradient"
import { useMemo } from "react"
import type { StyleProp, ViewStyle } from "react-native"
import { StyleSheet, Text } from "react-native"
export const FallbackIcon = ({
title,
url,
size,
className,
style,
}: {
title: string
url?: string
size: number
className?: string
style?: StyleProp<ViewStyle>
}) => {
const colors = useMemo(() => getBackgroundGradient(title || url || ""), [title, url])
const sizeStyle = useMemo(() => ({ width: size, height: size }), [size])
const [, , , bgAccent, bgAccentLight, bgAccentUltraLight] = colors
const renderedText = useMemo(() => {
const isCJK = isCJKChar(title[0])
return <Text style={styles.text}>{isCJK ? title[0] : title.slice(0, 2)}</Text>
}, [title])
return (
<LinearGradient
className={className}
colors={[bgAccent, bgAccentLight, bgAccentUltraLight]}
locations={[0, 0.99, 1]}
style={[sizeStyle, styles.container, style]}
>
{renderedText}
</LinearGradient>
)
}
const styles = StyleSheet.create({
container: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
text: {
fontSize: 12,
color: "#fff",
},
})

View File

@ -0,0 +1,61 @@
import type { FeedViewType } from "@follow/constants"
import { getUrlIcon } from "@follow/utils/src/utils"
import type { ReactNode } from "react"
import { useMemo } from "react"
import type { ImageProps } from "react-native"
import { Image } from "react-native"
import type { FeedSchema } from "@/src/database/schemas/types"
type FeedIconFeed =
| (Pick<FeedSchema, "ownerUserId" | "id" | "title" | "url" | "image"> & {
type: FeedViewType
siteUrl?: string
})
| FeedSchema
const getFeedIconSrc = (siteUrl: string, fallback: boolean) => {
const ret = getUrlIcon(siteUrl, fallback)
return [ret.src, ret.fallbackUrl]
}
interface FeedIconProps {
feed?: FeedIconFeed
fallbackUrl?: string
className?: string
size?: number
siteUrl?: string
/**
* Image loading error fallback to site icon
*/
fallback?: boolean
fallbackElement?: ReactNode
}
export function FeedIcon({
feed,
fallbackUrl,
className,
size = 20,
fallback = true,
fallbackElement,
siteUrl,
...props
}: FeedIconProps & ImageProps) {
const src = useMemo(() => {
switch (true) {
case !feed && !!siteUrl: {
const [src] = getFeedIconSrc(siteUrl, fallback)
return src
}
case !!feed && !!feed.image: {
return feed.image
}
case !!feed && !feed.image && !!feed.siteUrl: {
const [src] = getFeedIconSrc(feed.siteUrl, fallback)
return src
}
}
}, [fallback, feed, siteUrl])
return <Image height={size} width={size} source={{ uri: src }} {...props} />
}

View File

@ -0,0 +1,49 @@
import type { FC, PropsWithChildren } from "react"
import { useEffect } from "react"
import { View } from "react-native"
import Animated, {
Easing,
interpolate,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"
import { Loading3CuteLiIcon } from "@/src/icons/loading_3_cute_li"
export const LoadingIndicator: FC<
{
size?: number
} & PropsWithChildren
> = ({ size = 60, children }) => {
const rotateValue = useSharedValue(0)
const rotation = useDerivedValue(() => {
return interpolate(rotateValue.value, [0, 360], [0, 360])
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}))
useEffect(() => {
rotateValue.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1,
false,
)
return () => {
rotateValue.value = 0
}
}, [rotateValue])
return (
<View className="flex-1 items-center justify-center">
<Animated.View style={[animatedStyle, { width: size, height: size }]} className={"mb-2"}>
<Loading3CuteLiIcon width={size} height={size} />
</Animated.View>
{children}
</View>
)
}

View File

@ -0,0 +1,51 @@
import type { StyleProp, ViewStyle } from "react-native"
import type { SvgProps } from "react-native-svg"
import Svg, { ClipPath, Defs, G, Mask, Path, Rect } from "react-native-svg"
import { accentColor } from "@/src/theme/colors"
export const Logo: React.FC<{ color?: string } & SvgProps> = ({ color = accentColor, ...rest }) => {
return (
<Svg viewBox="0 0 145 145" fill="none" {...rest}>
<Mask id="mask0_54_296" maskUnits="userSpaceOnUse" x="0" y="0" width="145" height="145">
<Path
fillRule="evenodd"
clipRule="evenodd"
d="M144.491 45.4541C144.491 43.75 144.492 42.0456 144.481 40.3413C144.473 38.9056 144.456 37.4702 144.417 36.0352C144.333 32.9075 144.148 29.753 143.592 26.6601C143.028 23.5227 142.107 20.6027 140.655 17.752C139.228 14.9501 137.364 12.3862 135.139 10.1633C132.915 7.94053 130.351 6.07747 127.548 4.65117C124.694 3.19908 121.771 2.27818 118.63 1.71424C115.537 1.15884 112.381 0.974699 109.254 0.890204C107.818 0.851354 106.382 0.834978 104.945 0.826093C103.24 0.81564 101.535 0.816511 99.8296 0.816511L80.0305 0.745605H65.2222L45.7734 0.816511C44.0649 0.816511 42.3564 0.81564 40.6479 0.826093C39.2085 0.834978 37.7698 0.851354 36.331 0.890204C33.196 0.974699 30.0335 1.15902 26.933 1.71511C23.7879 2.27887 20.8604 3.19943 18.0029 4.65047C15.194 6.07694 12.6239 7.94018 10.3953 10.1633C8.1671 12.386 6.29934 14.9494 4.86956 17.7508C3.41382 20.6028 2.49083 23.5246 1.92533 26.6638C1.36854 29.7554 1.18405 32.9089 1.09921 36.0352C1.06053 37.4704 1.04381 38.9057 1.03509 40.3413C1.02464 42.0458 0.937012 44.163 0.937012 45.8674L0.937534 65.0468L0.937012 80.0136L1.02551 99.6005C1.02551 101.307 1.02482 103.014 1.03509 104.72C1.04381 106.158 1.06053 107.595 1.09938 109.032C1.18405 112.164 1.36889 115.323 1.9262 118.42C2.49153 121.562 3.41434 124.486 4.86886 127.34C6.29882 130.146 8.16692 132.713 10.3953 134.939C12.6237 137.165 15.1932 139.031 18.0015 140.459C20.8607 141.913 23.7896 142.835 26.9366 143.4C30.0357 143.956 33.1972 144.141 36.331 144.225C37.7698 144.264 39.2087 144.281 40.648 144.289C42.3566 144.3 44.0649 144.299 45.7734 144.299L65.398 144.3H80.243L99.8296 144.299C101.535 144.299 103.24 144.3 104.945 144.289C106.382 144.281 107.818 144.264 109.254 144.225C112.383 144.141 115.539 143.956 118.634 143.399C121.773 142.835 124.694 141.913 127.547 140.46C130.35 139.031 132.915 137.166 135.139 134.939C137.363 132.714 139.227 130.147 140.654 127.341C142.107 124.486 143.028 121.56 143.593 118.416C144.148 115.321 144.333 112.163 144.417 109.032C144.456 107.595 144.473 106.158 144.481 104.72C144.492 103.014 144.491 101.307 144.491 99.6005C144.491 99.6005 144.49 80.3594 144.49 80.0136V65.0311C144.49 64.7757 144.491 45.4541 144.491 45.4541Z"
fill="white"
/>
</Mask>
<G mask="url(#mask0_54_296)">
<Rect x="0.414551" y="-6.74585" width="149.477" height="158.537" fill={color} />
<Path
d="M103.609 36.6948H53.5823C47.5327 36.6948 42.6284 41.5946 42.6284 47.6388C42.6284 53.683 47.5327 58.5828 53.5823 58.5828H103.609C109.659 58.5828 114.563 53.683 114.563 47.6388C114.563 41.5946 109.659 36.6948 103.609 36.6948Z"
fill="white"
/>
<Path
d="M76.9348 65.1489H43.5916C37.5419 65.1489 32.6377 70.0487 32.6377 76.0929C32.6377 82.1371 37.5419 87.0369 43.5916 87.0369H76.9348C82.9845 87.0369 87.8888 82.1371 87.8888 76.0929C87.8888 70.0487 82.9845 65.1489 76.9348 65.1489Z"
fill="white"
/>
<Path
d="M80.1608 104.571C80.1608 98.5272 75.2565 93.6274 69.2068 93.6274C63.1572 93.6274 58.2529 98.5272 58.2529 104.571C58.2529 110.616 63.1572 115.515 69.2068 115.515C75.2565 115.515 80.1608 110.616 80.1608 104.571Z"
fill="white"
/>
</G>
</Svg>
)
}
export const FollowIcon: React.FC<{ color: string; style?: StyleProp<ViewStyle> }> = ({
color,
style,
}) => (
<Svg viewBox="0 0 344 330" fill="none" style={style}>
<G fill={color} clipPath="url(#a)">
<Path d="M297.639 0H88.19C62.862 0 42.33 20.514 42.33 45.82c0 25.305 20.532 45.819 45.86 45.819H297.64c25.328 0 45.861-20.514 45.861-45.82C343.5 20.514 322.967 0 297.639 0ZM185.96 119.13H46.361C21.033 119.13.5 139.644.5 164.95c0 25.305 20.533 45.819 45.861 45.819H185.96c25.328 0 45.861-20.514 45.861-45.819 0-25.306-20.533-45.82-45.861-45.82ZM199.466 284.181c0-25.306-20.533-45.82-45.861-45.82-25.328 0-45.861 20.514-45.861 45.82 0 25.305 20.533 45.819 45.861 45.819 25.328 0 45.861-20.514 45.861-45.819Z" />
</G>
<Defs>
<ClipPath id="a">
<Path fill={color} d="M.5 0h343v330H.5z" />
</ClipPath>
</Defs>
</Svg>
)

View File

@ -0,0 +1,22 @@
import { cn, composeEventHandlers } from "@follow/utils"
import { memo, useState } from "react"
import { Pressable } from "react-native"
export const ItemPressable: typeof Pressable = memo(({ children, ...props }) => {
const [isPressing, setIsPressing] = useState(false)
return (
<Pressable
{...props}
onPressIn={composeEventHandlers(props.onPressIn, () => setIsPressing(true))}
onPressOut={composeEventHandlers(props.onPressOut, () => setIsPressing(false))}
onHoverIn={composeEventHandlers(props.onHoverIn, () => setIsPressing(true))}
onHoverOut={composeEventHandlers(props.onHoverOut, () => setIsPressing(false))}
className={cn(
isPressing ? "bg-tertiary-system-background" : "bg-system-background",
props.className,
)}
>
{children}
</Pressable>
)
})

View File

@ -0,0 +1,14 @@
export const appEndpointMap = {
prod: {
api: "https://api.follow.is",
web: "https://app.follow.is",
},
dev: {
api: "https://api.dev.follow.is",
web: "https://dev.follow.is",
},
staging: {
api: "https://api.follow.is",
web: "https://staging.follow.is",
},
}

View File

@ -0,0 +1 @@
export const bottomViewTabHeight = 35

View File

@ -0,0 +1,65 @@
import { FeedViewType } from "@follow/constants"
import type * as React from "react"
import colors from "tailwindcss/colors"
import { AnnouncementCuteFiIcon } from "../icons/announcement_cute_fi"
import { MicCuteFiIcon } from "../icons/mic_cute_fi"
import { PaperCuteFiIcon } from "../icons/paper_cute_fi"
import { PicCuteFiIcon } from "../icons/pic_cute_fi"
import { TwitterCuteFiIcon } from "../icons/twitter_cute_fi"
import { VideoCuteFiIcon } from "../icons/video_cute_fi"
import { accentColor } from "../theme/colors"
export interface ViewDefinition {
name: string
icon: React.FC<{ color?: string; height?: number; width?: number }>
activeColor: string
translation: string
view: FeedViewType
wideMode?: boolean
gridMode?: boolean
}
export const views: ViewDefinition[] = [
{
name: "Articles",
icon: PaperCuteFiIcon,
activeColor: accentColor,
translation: "title,description",
view: FeedViewType.Articles,
},
{
name: "Social Media",
icon: TwitterCuteFiIcon,
activeColor: colors.sky[500],
translation: "content",
view: FeedViewType.SocialMedia,
},
{
name: "Pictures",
icon: PicCuteFiIcon,
activeColor: colors.green[500],
translation: "title",
view: FeedViewType.Pictures,
},
{
name: "Videos",
icon: VideoCuteFiIcon,
activeColor: colors.red[500],
translation: "title",
view: FeedViewType.Videos,
},
{
name: "Audios",
icon: MicCuteFiIcon,
activeColor: colors.purple[500],
translation: "title",
view: FeedViewType.Audios,
},
{
name: "Notifications",
icon: AnnouncementCuteFiIcon,
activeColor: colors.yellow[500],
translation: "title",
view: FeedViewType.Notifications,
},
]

View File

@ -0,0 +1,25 @@
import type { ExpoSQLiteDatabase } from "drizzle-orm/expo-sqlite"
import { drizzle } from "drizzle-orm/expo-sqlite"
import * as FileSystem from "expo-file-system"
import * as SQLite from "expo-sqlite"
import * as schema from "./schemas"
export const sqlite = SQLite.openDatabaseSync("follow.db")
let db: ExpoSQLiteDatabase<typeof schema> & {
$client: SQLite.SQLiteDatabase
}
export function initializeDb() {
db = drizzle(sqlite, {
schema,
logger: true,
})
}
export { db }
export const getDbPath = () => {
return `${FileSystem.documentDirectory}SQLite/follow.db`
}
if (__DEV__) console.info("SQLite:", getDbPath())

View File

@ -0,0 +1,59 @@
import type { FeedViewType } from "@follow/constants"
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
export const feedsTable = sqliteTable("feeds", {
id: text("id").primaryKey(),
title: text("title"),
url: text("url").notNull(),
description: text("description"),
image: text("image"),
errorAt: text("error_at"),
siteUrl: text("site_url"),
ownerUserId: text("owner_user_id"),
errorMessage: text("error_message"),
})
export const subscriptionsTable = sqliteTable("subscriptions", {
feedId: text("feed_id"),
listId: text("list_id"),
inboxId: text("inbox_id"),
userId: text("user_id").notNull(),
view: integer("view").notNull().$type<FeedViewType>(),
isPrivate: integer("is_private").notNull(),
title: text("title"),
category: text("category"),
createdAt: text("created_at"),
type: text("type").notNull().$type<"feed" | "list" | "inbox">(),
id: text("id").primaryKey(),
})
export const inboxesTable = sqliteTable("inboxes", {
id: text("id").primaryKey(),
title: text("title"),
})
export const listsTable = sqliteTable("lists", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
title: text("title").notNull(),
feedIds: text("feed_ids", { mode: "json" }).$type<string>(),
description: text("description"),
view: integer("view").notNull().$type<FeedViewType>(),
image: text("image"),
fee: integer("fee"),
ownerUserId: text("owner_user_id"),
})
export const unreadTable = sqliteTable("unread", {
subscriptionId: text("subscription_id").notNull().primaryKey(),
count: integer("count").notNull(),
})
export const usersTable = sqliteTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull(),
handle: text("handle"),
name: text("name"),
image: text("image"),
isMe: integer("is_me").notNull(),
})

View File

@ -0,0 +1,20 @@
import type {
feedsTable,
inboxesTable,
listsTable,
subscriptionsTable,
unreadTable,
usersTable,
} from "."
export type SubscriptionSchema = typeof subscriptionsTable.$inferSelect
export type FeedSchema = typeof feedsTable.$inferSelect
export type InboxSchema = typeof inboxesTable.$inferSelect
export type ListSchema = typeof listsTable.$inferSelect
export type UnreadSchema = typeof unreadTable.$inferSelect
export type UserSchema = typeof usersTable.$inferSelect

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AZSortAscendingLettersCuteReIconProps {
width?: number
height?: number
color?: string
}
export const AZSortAscendingLettersCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AZSortAscendingLettersCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5.714 9h4.572M5 11a35.518 35.518 0 0 1 2.365-6.715.496.496 0 0 1 .45-.285h.37c.192 0 .368.11.45.285A35.52 35.52 0 0 1 11 11m-6 3h5.5a.5.5 0 0 1 .3.9l-5.6 4.2a.5.5 0 0 0 .3.9H11m6-15v14m2.828-2c-.691 1.251-1.577 2.137-2.828 2.828-1.251-.691-2.137-1.577-2.828-2.828"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AZSortDescendingLettersCuteReIconProps {
width?: number
height?: number
color?: string
}
export const AZSortDescendingLettersCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AZSortDescendingLettersCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5.714 9h4.572M5 11a35.518 35.518 0 0 1 2.365-6.715.496.496 0 0 1 .45-.285h.37c.192 0 .368.11.45.285A35.52 35.52 0 0 1 11 11m-6 3h5.5a.5.5 0 0 1 .3.9l-5.6 4.2a.5.5 0 0 0 .3.9H11m6 0V6m2.828 2C19.138 6.75 18.252 5.863 17 5.172 15.75 5.863 14.863 6.749 14.172 8"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface VIP2CuteFiIconProps {
width?: number
height?: number
color?: string
}
export const VIP2CuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: VIP2CuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M10 4a2 2 0 1 1 2.987 1.74c.275.548.585 1.08.916 1.596.526.818 1.172 1.813 2.06 2.278 1.14.596 2.717.474 4.043.256a1.5 1.5 0 1 1 1.768 1.605l-1.51 3.902c-.449 1.156-.813 2.098-1.193 2.833-.396.767-.847 1.391-1.513 1.864-.719.512-1.562.744-2.43.838-.886.095-1.78.092-2.671.089L12 21l-.457.001c-.89.003-1.785.006-2.67-.09-.869-.093-1.712-.325-2.43-.837-.667-.473-1.118-1.098-1.514-1.864-.38-.735-.744-1.677-1.192-2.833l-1.511-3.902A1.5 1.5 0 1 1 3.994 9.87c1.326.218 2.902.34 4.043-.256.889-.465 1.534-1.46 2.06-2.278.332-.516.641-1.048.916-1.596A2 2 0 0 1 10 4"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,31 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface VIP2CuteReIconProps {
width?: number
height?: number
color?: string
}
export const VIP2CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: VIP2CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 4a1 1 0 1 1-2 0 1 1 0 0 1 2 0M12 20c2.573 0 3.86 0 4.862-.66a4.19 4.19 0 0 0 .116-.08c.978-.696 1.443-1.896 2.372-4.295l1.65-4.26s-3.7.736-5.5-.205C13.7 9.559 12 6 12 6s-1.7 3.765-3.5 4.706c-1.8.941-5.5 0-5.5 0l1.65 4.259c.929 2.4 1.394 3.599 2.372 4.295l.116.08C8.14 20 9.427 20 12 20"
/>
<Path
fill={color}
d="M4 10a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0M23 10a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"
/>
</Svg>
)
}

View File

@ -0,0 +1,21 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AddCuteReIconProps {
width?: number
height?: number
color?: string
}
export const AddCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AddCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path stroke={color} strokeLinecap="round" strokeWidth={2} d="M5 12h14m-7 7V5" />
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AlignJustifyCuteReIconProps {
width?: number
height?: number
color?: string
}
export const AlignJustifyCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AlignJustifyCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="M4 4h16M4 9h16M4 14h16M4 19h16"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AlignLeftCuteReIconProps {
width?: number
height?: number
color?: string
}
export const AlignLeftCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AlignLeftCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="M4 4h16M4 9h10M4 14h16M4 19h10"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AnnouncementCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const AnnouncementCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AnnouncementCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M16.895 1.822c.79-.158 1.62.187 2.063.86.285.431.33.962.4 1.46.183 1.326.314 2.641.393 3.952a3.001 3.001 0 0 1 0 5.811 50.66 50.66 0 0 1-.379 3.845c-.056.416-.095.852-.3 1.227a2.017 2.017 0 0 1-2.492.9c-.396-.158-.709-.472-1.018-.756-.466-.427-.924-.869-1.407-1.277A11 11 0 0 0 10 15.645v2.645a2.71 2.71 0 0 1-5.316.744l-1.57-5.497a4.7 4.7 0 0 1 3.326-7.73c.855-.047 1.719-.063 2.57-.165a11 11 0 0 0 4.727-1.723c.636-.416 1.232-.896 1.84-1.352.4-.3.814-.643 1.318-.745M5.634 15.078l.973 3.407A.71.71 0 0 0 8 18.29v-3.006c-.786-.065-1.595-.027-2.366-.206m14.2-3.527a.996.996 0 0 0 0-1.103c.005.368.005.736 0 1.103"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface AppleCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const AppleCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: AppleCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M13.538 1.328a1.147 1.147 0 0 1 1.336 1.336 3.302 3.302 0 0 1-.89 1.72 3.302 3.302 0 0 1-1.72.89 1.147 1.147 0 0 1-1.336-1.336 3.302 3.302 0 0 1 .89-1.72 3.302 3.302 0 0 1 1.72-.89m-3.134 4.888c.66.277 1.106.457 1.615.457.51 0 .955-.18 1.616-.457.68-.286 1.616-.678 2.76-.486a6.213 6.213 0 0 1 2.254.825c.447.276 1.14.644 1.339 1.535.131.585-.11 1.124-.299 1.487-.204.392-.516.873-.88 1.432a2.788 2.788 0 0 0-.466 1.526c0 .687.253 1.312.671 1.792.336.384.678.764.99 1.168.253.329.495.692.548 1.114.08.642-.234 1.217-.515 1.77-.956 1.885-2.34 3.51-3.923 4.136-.532.21-1.087.183-1.526.116-.853-.13-1.704-.58-2.569-.58s-1.716.45-2.569.58c-.439.067-.994.095-1.526-.116-1.337-.529-2.523-1.768-3.423-3.238-.914-1.495-1.608-3.35-1.89-5.285-.241-1.646-.11-3.49.648-5.047.78-1.602 2.206-2.849 4.385-3.215 1.143-.192 2.079.2 2.76.486"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface ArrowRightCircleCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const ArrowRightCircleCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: ArrowRightCircleCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12s4.477 10 10 10 10-4.477 10-10M11.819 8.014a1 1 0 0 0 .443 1.343c.887.447 1.58.977 2.146 1.643h-6.65a1 1 0 1 0 0 2h6.65c-.566.666-1.258 1.195-2.146 1.642a1 1 0 1 0 .9 1.787c1.456-.734 2.564-1.692 3.422-3.01.214-.327.539-.772.539-1.42 0-.646-.325-1.092-.539-1.42-.859-1.316-1.966-2.275-3.422-3.008a1 1 0 0 0-1.344.443"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface ArrowRightUpCuteReIconProps {
width?: number
height?: number
color?: string
}
export const ArrowRightUpCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: ArrowRightUpCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16.946 7.054 6.339 17.661M9.409 6.59a15.963 15.963 0 0 1 6.796-.249c.571.1.857.149 1.08.373.225.224.274.51.374 1.08a15.963 15.963 0 0 1-.25 6.796"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Back2CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Back2CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Back2CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
d="M10.704 12.858a1 1 0 0 0-.86-1.806zM4.319 7.182a1 1 0 1 0-1.991-.189zM12.499 7v1zm7.5 8.5a1 1 0 1 0 2 0zM4.644 13.02l-.073.998zm5.202-1.968c-1.688.802-3.296 1.106-5.129.971l-.146 1.995c2.184.16 4.137-.211 6.134-1.16zm-4.692 1.277c-.752-1.674-1.01-3.284-.834-5.147l-1.991-.189c-.209 2.198.103 4.157 1 6.156zM12.5 8a7.5 7.5 0 0 1 7.5 7.5h2A9.5 9.5 0 0 0 12.5 6zm-7.17 5.294A7.504 7.504 0 0 1 12.5 8V6a9.502 9.502 0 0 0-9.082 6.706zm-.614-1.27a.52.52 0 0 1 .437.305l-1.824.82a1.48 1.48 0 0 0 1.24.869z"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface BugCuteReIconProps {
width?: number
height?: number
color?: string
}
export const BugCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: BugCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 13H3m18 0h-3M7 9C6 9 4.5 8 4 7m3 11c-1.5 0-2 2-2 3m12-3c1.5 0 2 2 2 3M17 9c1 0 2.5-1 3-2m-8 13.88v-8m-6 .62c0-1.688.446-3.246 1.2-4.5h9.6c.754 1.254 1.2 2.812 1.2 4.5 0 4.142-2.686 7.5-6 7.5s-6-3.358-6-7.5M8.535 6h6.93A3.998 3.998 0 0 0 12 4c-1.48 0-2.773.804-3.465 2"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CelebrateCuteReIconProps {
width?: number
height?: number
color?: string
}
export const CelebrateCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CelebrateCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m14.182 9.818 1.06-1.06m-3.535-.708s1.414-2.828.707-4.95m3.182 9.546s2.475-.353 4.243.707m-2.475-6.717.707-.707m.354 3.889h.707M9.236 19.844l1.394-.477c2.723-.932 4.085-1.398 4.336-2.48.25-1.08-.767-2.098-2.802-4.133l-.918-.918C9.211 9.801 8.193 8.783 7.112 9.034c-1.081.25-1.547 1.612-2.479 4.336l-.477 1.394c-1.15 3.36-1.724 5.04-.842 5.922.881.882 2.561.307 5.922-.842"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CertificateCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const CertificateCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CertificateCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M9.843 2.27a3.05 3.05 0 0 1 4.314 0l1.046 1.047a.95.95 0 0 0 .672.278h1.479a3.05 3.05 0 0 1 3.05 3.05v1.48c0 .251.1.493.279.67l1.046 1.047a3.05 3.05 0 0 1 0 4.314l-1.046 1.046a.95.95 0 0 0-.279.672v1.479a3.05 3.05 0 0 1-3.05 3.05h-1.48a.95.95 0 0 0-.67.279l-1.047 1.046a3.05 3.05 0 0 1-4.314 0l-1.046-1.046a.95.95 0 0 0-.671-.278h-1.48a3.05 3.05 0 0 1-3.05-3.051v-1.48a.95.95 0 0 0-.279-.67l-1.046-1.047a3.05 3.05 0 0 1 0-4.314l1.046-1.046a.95.95 0 0 0 .279-.671v-1.48a3.05 3.05 0 0 1 3.05-3.05h1.48a.95.95 0 0 0 .67-.278zm5.4 6.52a1.05 1.05 0 1 1 1.087 1.798c-1.98 1.196-3.448 2.617-4.586 4.58a1.05 1.05 0 0 1-1.746.108 11.643 11.643 0 0 0-2.273-2.273 1.05 1.05 0 1 1 1.27-1.674c.628.476 1.2.99 1.728 1.551 1.209-1.683 2.696-2.988 4.52-4.09"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CertificateCuteReIconProps {
width?: number
height?: number
color?: string
}
export const CertificateCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CertificateCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.786 9.69c-2.113 1.276-3.715 2.82-4.951 4.951a12.695 12.695 0 0 0-2.475-2.475m-1.714-7.52h1.48a2 2 0 0 0 1.413-.586l1.046-1.046a2 2 0 0 1 2.829 0L14.46 4.06a2 2 0 0 0 1.414.586h1.48a2 2 0 0 1 2 2v1.48a2 2 0 0 0 .585 1.413l1.046 1.046a2 2 0 0 1 0 2.829L19.94 14.46a2 2 0 0 0-.585 1.414v1.48a2 2 0 0 1-2 2h-1.48a2 2 0 0 0-1.414.585l-1.046 1.046a2 2 0 0 1-2.829 0L9.54 19.94a2 2 0 0 0-1.414-.585h-1.48a2 2 0 0 1-2-2v-1.48a2 2 0 0 0-.585-1.414l-1.046-1.046a2 2 0 0 1 0-2.829L4.06 9.54a2 2 0 0 0 .586-1.414v-1.48a2 2 0 0 1 2-2"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CheckCircleCuteReIconProps {
width?: number
height?: number
color?: string
}
export const CheckCircleCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CheckCircleCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
d="M8.332 11.099a1 1 0 1 0-1.15 1.636zm2.254 3.646-.818.575a1 1 0 0 0 1.695-.095zm6.136-4.78a1 1 0 1 0-.96-1.754zM20 12a8 8 0 0 1-8 8v2c5.523 0 10-4.477 10-10zm-8 8a8 8 0 0 1-8-8H2c0 5.523 4.477 10 10 10zm-8-8a8 8 0 0 1 8-8V2C6.477 2 2 6.477 2 12zm8-8a8 8 0 0 1 8 8h2c0-5.523-4.477-10-10-10zm-4.818 8.735a10.387 10.387 0 0 1 2.586 2.585l1.636-1.15a12.385 12.385 0 0 0-3.072-3.071zm4.281 2.49c1.282-2.346 2.913-3.977 5.26-5.26l-.96-1.754c-2.682 1.466-4.59 3.372-6.055 6.054z"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CheckCircleFilledIconProps {
width?: number
height?: number
color?: string
}
export const CheckCircleFilledIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CheckCircleFilledIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m4.95 7.795a1 1 0 0 0-1.415-1.414l-4.95 4.95-2.12-2.121a1 1 0 0 0-1.415 1.414l2.829 2.828a1 1 0 0 0 1.414 0z"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CheckFilledIconProps {
width?: number
height?: number
color?: string
}
export const CheckFilledIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CheckFilledIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M21.546 5.112a1.5 1.5 0 0 1 0 2.121L10.232 18.547a1.5 1.5 0 0 1-2.121 0L2.454 12.89a1.5 1.5 0 1 1 2.121-2.121l4.596 4.596L19.424 5.112a1.5 1.5 0 0 1 2.122 0"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CloseCuteReIconProps {
width?: number
height?: number
color?: string
}
export const CloseCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CloseCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
d="M6.343 4.93a1 1 0 0 0-1.414 1.414zm11.314 14.142a1 1 0 1 0 1.414-1.415zM4.929 17.658a1 1 0 0 0 1.414 1.414zM19.07 6.344a1 1 0 0 0-1.414-1.414zm-14.142 0 12.728 12.728 1.414-1.415L6.343 4.93zm1.414 12.728L19.07 6.344 17.657 4.93 4.929 17.658z"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Compass3CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Compass3CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Compass3CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="m8.465 8.465 7.07 7.07m-4.007 2.166 1.31-.544c1.505-.626 2.258-.938 2.82-1.5.56-.56.873-1.314 1.499-2.82l.544-1.309c1.58-3.804 2.37-5.706 1.424-6.652-.946-.946-2.849-.156-6.653 1.424l-1.309.543c-1.506.626-2.259.939-2.82 1.5-.561.561-.874 1.314-1.5 2.82l-.543 1.31c-1.58 3.803-2.37 5.706-1.424 6.652.946.945 2.848.155 6.652-1.424Z"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CompassCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const CompassCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CompassCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10M7.757 16.242c-1.06-1.06.707-4.95 2.122-6.364 1.414-1.414 5.303-3.181 6.364-2.12 1.06 1.06-.707 4.949-2.122 6.363-1.414 1.414-5.303 3.182-6.364 2.121M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Copy2CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Copy2CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Copy2CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeWidth={2}
d="M7.824 5c.09-.266.204-.504.35-.722a4 4 0 0 1 1.104-1.104C10.286 2.5 11.69 2.5 14.5 2.5c2.809 0 4.213 0 5.222.674.437.292.812.667 1.104 1.104.674 1.009.674 2.413.674 5.222 0 2.809 0 4.213-.674 5.222a4.003 4.003 0 0 1-1.104 1.104c-.219.146-.456.26-.722.35M9.5 21.5c2.809 0 4.213 0 5.222-.674a4.003 4.003 0 0 0 1.104-1.104c.674-1.009.674-2.413.674-5.222 0-2.809 0-4.213-.674-5.222a4.002 4.002 0 0 0-1.104-1.104C13.713 7.5 12.31 7.5 9.5 7.5c-2.809 0-4.213 0-5.222.674a4 4 0 0 0-1.104 1.104C2.5 10.287 2.5 11.69 2.5 14.5c0 2.809 0 4.213.674 5.222.292.437.667.812 1.104 1.104 1.009.674 2.413.674 5.222.674Z"
/>
</Svg>
)
}

View File

@ -0,0 +1,24 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface CopyCuteReIconProps {
width?: number
height?: number
color?: string
}
export const CopyCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: CopyCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
d="M7.5 10.5a1 1 0 1 0 0 2zm4 2a1 1 0 1 0 0-2zm-4 3a1 1 0 1 0 0 2zm2 2a1 1 0 1 0 0-2zM20.028 4.614l-.882.472zm-1.642-1.642-.472.882zM5.614 21.028l.472-.882zm-1.642-1.642-.882.471zm12.056 0 .882.471zm0-10.772.882-.471zm-1.642-1.642-.472.882zM3.972 8.614l-.882-.471zm1.642-1.642.472.882zM15.5 13v2h2v-2zm-11 2v-2h-2v2zm3-2.5h4v-2h-4zm0 5h2v-2h-2zm14-4V9h-2v4.5zM14 1.5h-2.5v2H14zm-2.5 0a5 5 0 0 0-5 5h2a3 3 0 0 1 3-3zm10 7.5c0-1.15 0-2.075-.063-2.822-.064-.758-.2-1.424-.527-2.035l-1.764.943c.145.272.245.632.299 1.262.054.642.055 1.467.055 2.652zM14 3.5c1.185 0 2.01 0 2.652.055.63.054.99.154 1.262.3l.943-1.765c-.611-.326-1.277-.463-2.035-.527C16.075 1.499 15.15 1.5 14 1.5zm6.91.643a5 5 0 0 0-2.053-2.053l-.943 1.764a3 3 0 0 1 1.232 1.232zM19.5 13.5a3 3 0 0 1-3 3v2a5 5 0 0 0 5-5zm-9.5 7c-1.185 0-2.01 0-2.652-.055-.63-.054-.99-.154-1.262-.3l-.943 1.765c.611.326 1.277.463 2.035.527.747.064 1.672.063 2.822.063zM2.5 15c0 1.15 0 2.075.063 2.822.064.758.2 1.424.527 2.035l1.764-.943c-.145-.272-.245-.632-.299-1.262C4.501 17.01 4.5 16.184 4.5 15zm3.586 5.146a3 3 0 0 1-1.232-1.232l-1.764.943a5 5 0 0 0 2.053 2.053zM15.5 15c0 1.185 0 2.01-.055 2.652-.054.63-.154.99-.3 1.262l1.765.943c.326-.611.463-1.277.527-2.035.064-.747.063-1.672.063-2.822zM10 22.5c1.15 0 2.075 0 2.822-.063.758-.064 1.424-.2 2.035-.527l-.943-1.764c-.272.145-.632.245-1.262.299-.642.054-1.467.055-2.652.055zm5.146-3.586a3 3 0 0 1-1.232 1.232l.943 1.764a5 5 0 0 0 2.053-2.053zM17.5 13c0-1.15 0-2.075-.063-2.822-.064-.758-.2-1.424-.527-2.035l-1.764.943c.145.272.245.632.299 1.262.054.642.055 1.467.055 2.652zM10 7.5c1.185 0 2.01 0 2.652.055.63.054.99.154 1.262.3l.943-1.765c-.611-.326-1.277-.463-2.035-.527C12.075 5.499 11.15 5.5 10 5.5zm6.91.643a5 5 0 0 0-2.053-2.053l-.943 1.764a3 3 0 0 1 1.232 1.232zM4.5 13c0-1.184 0-2.01.055-2.652.054-.63.154-.99.3-1.262L3.09 8.143c-.326.611-.463 1.277-.527 2.035C2.499 10.925 2.5 11.85 2.5 13zM10 5.5c-1.15 0-2.075 0-2.822.063-.758.064-1.424.2-2.035.527l.943 1.764c.272-.145.632-.245 1.262-.299C7.99 7.501 8.815 7.5 10 7.5zM4.854 9.086a3 3 0 0 1 1.232-1.232L5.143 6.09A5 5 0 0 0 3.09 8.143z"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Cursor3CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Cursor3CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Cursor3CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6.167 6.172 4.752 4.757m7.072 1.415 1.414-1.415M8.995 5V3m-4 6h-2m1.758 4.243 1.414-1.414m10.854 2.835-.91.383c-.371.156-.557.234-.695.373-.139.139-.217.324-.373.695l-.382.909c-.707 1.679-1.06 2.518-1.726 2.517-.665-.001-1.002-.81-1.675-2.429a34.465 34.465 0 0 1-1.766-5.472c-.273-1.178-.409-1.766-.017-2.158.392-.393.981-.256 2.159.016 1.875.434 3.7 1.028 5.473 1.766 1.618.673 2.427 1.01 2.428 1.675.002.666-.837 1.019-2.516 1.725"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Delete2CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Delete2CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Delete2CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="m4.5 5.5.595 9.513c.16 2.576.241 3.864.89 4.788a4 4 0 0 0 1.18 1.108c.963.591 2.254.591 4.835.591v0c2.581 0 3.872 0 4.834-.59a4 4 0 0 0 1.18-1.11c.65-.923.73-2.211.892-4.787L19.5 5.5m-12 0 .088-.265c.44-1.32.66-1.98 1.184-2.357.524-.378 1.22-.378 2.611-.378h1.234c1.391 0 2.087 0 2.61.378.525.377.745 1.037 1.185 2.357l.088.265M10 11v5m4-5v5M3.5 5.5h17"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface DepartmentCuteReIconProps {
width?: number
height?: number
color?: string
}
export const DepartmentCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: DepartmentCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinejoin="round"
strokeWidth={2}
d="M12 8a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm0 0v4m-6 4a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm0 0a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4m0 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface DiscordCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const DiscordCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: DiscordCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M9.99 5.147A13.65 13.65 0 0 1 12 5c.692 0 1.366.05 2.014.148A1.012 1.012 0 0 1 15.004 4c.93 0 1.924.406 2.777.763 1.26.528 1.968 1.636 2.517 2.853.89 1.975 1.509 4.608 1.723 6.61.102.95.127 1.906-.056 2.549-.245.858-1.232 1.403-1.995 1.824l-.18.1c-.687.384-1.446.756-1.97 1.013l-.37.182a1 1 0 1 1-.894-1.788l.416-.206.375-.184-.58-.609c-1.39.57-3.028.893-4.767.893-1.739 0-3.376-.322-4.766-.893l-.58.608.339.166.455.225a1 1 0 1 1-.895 1.788l-.544-.27c-.604-.298-1.208-.596-1.796-.925a54.33 54.33 0 0 0-.18-.1c-.762-.42-1.749-.966-1.994-1.824-.184-.643-.158-1.598-.056-2.55.214-2.001.832-4.634 1.723-6.609.549-1.217 1.257-2.325 2.517-2.853C7.06 4.413 8.072 4 9 4c.603 0 1.077.555.99 1.147m.51 7.103a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0M15.25 14a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface DocmentCuteFiIconProps {
width?: number
height?: number
color?: string
}
export const DocmentCuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: DocmentCuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
fill={color}
fillRule="evenodd"
d="M12 1.5c1.315 0 2.646-.034 3.954.12 1.137.135 2.217.497 3.046 1.31.855.837 1.24 1.942 1.38 3.116.12 1.02.12 2.301.12 3.89v4.128c0 1.59 0 2.871-.12 3.89-.14 1.174-.525 2.28-1.38 3.117-.829.812-1.909 1.174-3.046 1.309-1.308.155-2.64.12-3.954.12-1.315 0-2.646.035-3.954-.12-1.137-.134-2.217-.497-3.046-1.309-.855-.838-1.24-1.943-1.38-3.117-.12-1.019-.12-2.3-.12-3.89V9.937c0-1.59 0-2.872.12-3.89.14-1.175.525-2.28 1.38-3.117.829-.813 1.909-1.175 3.046-1.31 1.308-.154 2.64-.12 3.954-.12M8 9a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1m0 5a1 1 0 0 1 1-1h3a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1"
clipRule="evenodd"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface DocmentCuteReIconProps {
width?: number
height?: number
color?: string
}
export const DocmentCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: DocmentCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="M9 9h6m-6 5h3m0 7.5v0c3.287 0 4.931 0 6.038-.908.202-.166.388-.352.554-.554.908-1.107.908-2.75.908-6.038v-4c0-3.287 0-4.931-.908-6.038a4 4 0 0 0-.554-.554C16.93 2.5 15.288 2.5 12 2.5v0c-3.287 0-4.931 0-6.038.908a4 4 0 0 0-.554.554C4.5 5.07 4.5 6.712 4.5 10v4c0 3.288 0 4.931.908 6.038a4 4 0 0 0 .554.554c1.107.908 2.75.908 6.038.908Z"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Download2CuteFiIconProps {
width?: number
height?: number
color?: string
}
export const Download2CuteFiIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Download2CuteFiIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M12 3v12m-4.242-3.586c.83 1.915 2.023 3.18 3.848 4.062.249.12.54.12.789 0 1.824-.882 3.017-2.147 3.848-4.062M4.002 16c.012 2.175.109 3.353.877 4.121C5.758 21 7.172 21 10 21h4c2.829 0 4.243 0 5.122-.879.768-.768.864-1.946.877-4.121"
/>
</Svg>
)
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface Download2CuteReIconProps {
width?: number
height?: number
color?: string
}
export const Download2CuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: Download2CuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 3v12m-4.242-3.586c.83 1.915 2.023 3.18 3.848 4.062.249.12.54.12.789 0 1.824-.882 3.017-2.147 3.848-4.062M4.002 16c.012 2.175.109 3.353.877 4.121C5.758 21 7.172 21 10 21h4c2.829 0 4.243 0 5.122-.879.768-.768.864-1.946.877-4.121"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface EditCuteReIconProps {
width?: number
height?: number
color?: string
}
export const EditCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: EditCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="M12.5 3.5h-1c-3.771 0-5.657 0-6.828 1.172C3.5 5.843 3.5 7.729 3.5 11.5v1c0 3.771 0 5.657 1.172 6.828C5.843 20.5 7.729 20.5 11.5 20.5h1c3.771 0 5.657 0 6.828-1.172C20.5 18.157 20.5 16.271 20.5 12.5v-1M20 4 10 14"
/>
</Svg>
)
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import Svg, { Path } from "react-native-svg"
interface ExitCuteReIconProps {
width?: number
height?: number
color?: string
}
export const ExitCuteReIcon = ({
width = 24,
height = 24,
color = "#10161F",
}: ExitCuteReIconProps) => {
return (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill="#fff" fillOpacity={0.01} d="M24 0v24H0V0z" />
<Path
stroke={color}
strokeLinecap="round"
strokeWidth={2}
d="M12.5 12H20m-2.5-2.828a8.616 8.616 0 0 1 2.478 2.307.89.89 0 0 1 0 1.042A8.631 8.631 0 0 1 17.5 14.83M12.5 3.5c-3.44.002-5.21.053-6.328 1.171C5 5.843 5 7.73 5 11.5v1c0 3.771 0 5.657 1.172 6.828C7.29 20.447 9.06 20.498 12.5 20.5"
/>
</Svg>
)
}

Some files were not shown because too many files have changed in this diff Show More