diff --git a/.env.example b/.env.example index fb4546e9a4..25d43ec9eb 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/.vscode/settings.json b/.vscode/settings.json index 5231117611..ab2014bc79 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", "Русский"], diff --git a/apps/mobile/.env b/apps/mobile/.env new file mode 120000 index 0000000000..c7360fb82d --- /dev/null +++ b/apps/mobile/.env @@ -0,0 +1 @@ +../../.env \ No newline at end of file diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 0000000000..cd4feb8a3c --- /dev/null +++ b/apps/mobile/README.md @@ -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. diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts new file mode 100644 index 0000000000..95cfea0af5 --- /dev/null +++ b/apps/mobile/app.config.ts @@ -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, + }, +}) diff --git a/apps/mobile/assets/adaptive-icon.png b/apps/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000000..f0cf831f1e Binary files /dev/null and b/apps/mobile/assets/adaptive-icon.png differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Black.otf b/apps/mobile/assets/font/sn-pro/SNPro-Black.otf new file mode 100644 index 0000000000..d355197c58 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Black.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-BlackItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-BlackItalic.otf new file mode 100644 index 0000000000..396d9c0e68 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-BlackItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Bold.otf b/apps/mobile/assets/font/sn-pro/SNPro-Bold.otf new file mode 100644 index 0000000000..0f7bdcb60d Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Bold.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-BoldItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-BoldItalic.otf new file mode 100644 index 0000000000..721f48f474 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-BoldItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Book.otf b/apps/mobile/assets/font/sn-pro/SNPro-Book.otf new file mode 100644 index 0000000000..a9f56b3b71 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Book.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-BookItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-BookItalic.otf new file mode 100644 index 0000000000..d3bc6cb362 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-BookItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Heavy.otf b/apps/mobile/assets/font/sn-pro/SNPro-Heavy.otf new file mode 100644 index 0000000000..7df132536b Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Heavy.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-HeavyItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-HeavyItalic.otf new file mode 100644 index 0000000000..33c151f4d8 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-HeavyItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Light.otf b/apps/mobile/assets/font/sn-pro/SNPro-Light.otf new file mode 100644 index 0000000000..dd8700d11a Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Light.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-LightItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-LightItalic.otf new file mode 100644 index 0000000000..d6658004c1 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-LightItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Medium.otf b/apps/mobile/assets/font/sn-pro/SNPro-Medium.otf new file mode 100644 index 0000000000..df5c6d9e62 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Medium.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-MediumItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-MediumItalic.otf new file mode 100644 index 0000000000..da35d3cb1f Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-MediumItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Regular.otf b/apps/mobile/assets/font/sn-pro/SNPro-Regular.otf new file mode 100644 index 0000000000..39d81f36ad Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Regular.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-RegularItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-RegularItalic.otf new file mode 100644 index 0000000000..c768aec692 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-RegularItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Semibold.otf b/apps/mobile/assets/font/sn-pro/SNPro-Semibold.otf new file mode 100644 index 0000000000..3b83cd3eba Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Semibold.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-SemiboldItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-SemiboldItalic.otf new file mode 100644 index 0000000000..47a4b7cd84 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-SemiboldItalic.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-Thin.otf b/apps/mobile/assets/font/sn-pro/SNPro-Thin.otf new file mode 100644 index 0000000000..ec723c9415 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-Thin.otf differ diff --git a/apps/mobile/assets/font/sn-pro/SNPro-ThinItalic.otf b/apps/mobile/assets/font/sn-pro/SNPro-ThinItalic.otf new file mode 100644 index 0000000000..b23046af47 Binary files /dev/null and b/apps/mobile/assets/font/sn-pro/SNPro-ThinItalic.otf differ diff --git a/apps/mobile/assets/icon.png b/apps/mobile/assets/icon.png new file mode 100644 index 0000000000..a99d48fa1f Binary files /dev/null and b/apps/mobile/assets/icon.png differ diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 0000000000..012481e1ed --- /dev/null +++ b/apps/mobile/babel.config.js @@ -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", + ], + } +} diff --git a/apps/mobile/drizzle.config.ts b/apps/mobile/drizzle.config.ts new file mode 100644 index 0000000000..c634f89b35 --- /dev/null +++ b/apps/mobile/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "drizzle-kit" + +export default defineConfig({ + dialect: "sqlite", + driver: "expo", + schema: "./src/database/schemas/index.ts", + out: "./drizzle", +}) diff --git a/apps/mobile/drizzle/0000_harsh_shiva.sql b/apps/mobile/drizzle/0000_harsh_shiva.sql new file mode 100644 index 0000000000..a988d078ae --- /dev/null +++ b/apps/mobile/drizzle/0000_harsh_shiva.sql @@ -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 +); diff --git a/apps/mobile/drizzle/0001_bored_hobgoblin.sql b/apps/mobile/drizzle/0001_bored_hobgoblin.sql new file mode 100644 index 0000000000..d2f5b5e531 --- /dev/null +++ b/apps/mobile/drizzle/0001_bored_hobgoblin.sql @@ -0,0 +1 @@ +ALTER TABLE `inboxes` DROP COLUMN `user_id`; \ No newline at end of file diff --git a/apps/mobile/drizzle/0002_smart_power_man.sql b/apps/mobile/drizzle/0002_smart_power_man.sql new file mode 100644 index 0000000000..256e69246a --- /dev/null +++ b/apps/mobile/drizzle/0002_smart_power_man.sql @@ -0,0 +1,5 @@ +CREATE TABLE `unread` ( + `id` text PRIMARY KEY NOT NULL, + `subscription_id` text NOT NULL, + `count` integer NOT NULL +); diff --git a/apps/mobile/drizzle/0003_known_roland_deschain.sql b/apps/mobile/drizzle/0003_known_roland_deschain.sql new file mode 100644 index 0000000000..ed751f077a --- /dev/null +++ b/apps/mobile/drizzle/0003_known_roland_deschain.sql @@ -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; \ No newline at end of file diff --git a/apps/mobile/drizzle/0004_majestic_thunderbolt_ross.sql b/apps/mobile/drizzle/0004_majestic_thunderbolt_ross.sql new file mode 100644 index 0000000000..fb381bdc72 --- /dev/null +++ b/apps/mobile/drizzle/0004_majestic_thunderbolt_ross.sql @@ -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 +); diff --git a/apps/mobile/drizzle/meta/0000_snapshot.json b/apps/mobile/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000000..8c4e5c5f35 --- /dev/null +++ b/apps/mobile/drizzle/meta/0000_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/0001_snapshot.json b/apps/mobile/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000000..89cde8e823 --- /dev/null +++ b/apps/mobile/drizzle/meta/0001_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/0002_snapshot.json b/apps/mobile/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000000..eb2b394022 --- /dev/null +++ b/apps/mobile/drizzle/meta/0002_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/0003_snapshot.json b/apps/mobile/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000000..2039facecd --- /dev/null +++ b/apps/mobile/drizzle/meta/0003_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/0004_snapshot.json b/apps/mobile/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000000..69fe91979c --- /dev/null +++ b/apps/mobile/drizzle/meta/0004_snapshot.json @@ -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": {} + } +} diff --git a/apps/mobile/drizzle/meta/_journal.json b/apps/mobile/drizzle/meta/_journal.json new file mode 100644 index 0000000000..1a807c5b3d --- /dev/null +++ b/apps/mobile/drizzle/meta/_journal.json @@ -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 + } + ] +} diff --git a/apps/mobile/drizzle/migrations.js b/apps/mobile/drizzle/migrations.js new file mode 100644 index 0000000000..70c732a848 --- /dev/null +++ b/apps/mobile/drizzle/migrations.js @@ -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, + }, +} diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json new file mode 100644 index 0000000000..fcb962cc06 --- /dev/null +++ b/apps/mobile/eas.json @@ -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" + } + } + } +} diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js new file mode 100644 index 0000000000..857e635c20 --- /dev/null +++ b/apps/mobile/metro.config.js @@ -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" }), +) diff --git a/apps/mobile/nativewind-env.d.ts b/apps/mobile/nativewind-env.d.ts new file mode 100644 index 0000000000..9583462875 --- /dev/null +++ b/apps/mobile/nativewind-env.d.ts @@ -0,0 +1,3 @@ +/// + +// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. diff --git a/apps/mobile/package.json b/apps/mobile/package.json new file mode 100644 index 0000000000..f1105c8372 --- /dev/null +++ b/apps/mobile/package.json @@ -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" + } +} diff --git a/apps/mobile/scripts/with-follow-assets.js b/apps/mobile/scripts/with-follow-assets.js new file mode 100644 index 0000000000..f6fea0a060 --- /dev/null +++ b/apps/mobile/scripts/with-follow-assets.js @@ -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 diff --git a/apps/mobile/shim-env.d.ts b/apps/mobile/shim-env.d.ts new file mode 100644 index 0000000000..847b46fe82 --- /dev/null +++ b/apps/mobile/shim-env.d.ts @@ -0,0 +1,5 @@ +/// + +declare namespace NodeJS { + export type ProcessEnv = Record +} diff --git a/apps/mobile/src/atoms/env.ts b/apps/mobile/src/atoms/env.ts new file mode 100644 index 0000000000..dea86e6ba2 --- /dev/null +++ b/apps/mobile/src/atoms/env.ts @@ -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("debug-env", "prod", JotaiPersistSyncStorage, { + getOnInit: true, + }), +) diff --git a/apps/mobile/src/components/common/FollowWebView.tsx b/apps/mobile/src/components/common/FollowWebView.tsx new file mode 100644 index 0000000000..3c2bf39a55 --- /dev/null +++ b/apps/mobile/src/components/common/FollowWebView.tsx @@ -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 } & FollowWebViewProps) => { + const { onNavigationStateChange } = useWebViewNavigation({ webViewRef }) + const [ready, setReady] = useState(false) + useDeepLink({ webViewRef, ready }) + + return ( + 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 }) => { + 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 + 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]) +} diff --git a/apps/mobile/src/components/common/HeaderBlur.tsx b/apps/mobile/src/components/common/HeaderBlur.tsx new file mode 100644 index 0000000000..8f89b60d6a --- /dev/null +++ b/apps/mobile/src/components/common/HeaderBlur.tsx @@ -0,0 +1,16 @@ +import { StyleSheet } from "react-native" + +import { ThemedBlurView } from "@/src/components/common/ThemedBlurView" + +const node = ( + +) +export const HeaderBlur = () => { + return node +} diff --git a/apps/mobile/src/components/common/ThemedBlurView.tsx b/apps/mobile/src/components/common/ThemedBlurView.tsx new file mode 100644 index 0000000000..9a7843ab07 --- /dev/null +++ b/apps/mobile/src/components/common/ThemedBlurView.tsx @@ -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 = ({ tint, ...rest }) => { + const { colorScheme } = useColorScheme() + return ( + + ) +} diff --git a/apps/mobile/src/components/common/ThemedText.tsx b/apps/mobile/src/components/common/ThemedText.tsx new file mode 100644 index 0000000000..d10df57934 --- /dev/null +++ b/apps/mobile/src/components/common/ThemedText.tsx @@ -0,0 +1,6 @@ +import type { TextProps } from "react-native" +import { Text } from "react-native" + +export function ThemedText(props: TextProps) { + return +} diff --git a/apps/mobile/src/components/ui/accordion/index.tsx b/apps/mobile/src/components/ui/accordion/index.tsx new file mode 100644 index 0000000000..9866871ec8 --- /dev/null +++ b/apps/mobile/src/components/ui/accordion/index.tsx @@ -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 + children: React.ReactNode + viewKey: string + style?: StyleProp + wrapperStyle?: StyleProp + 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 ( + + { + height.value = e.nativeEvent.layout.height + }} + style={[styles.wrapper, wrapperStyle]} + className={wrapperClassName} + > + {children} + + + ) +} + +const styles = StyleSheet.create({ + wrapper: { + width: "100%", + position: "absolute", + display: "flex", + flex: 1, + }, + animatedView: { + width: "100%", + overflow: "hidden", + }, +}) diff --git a/apps/mobile/src/components/ui/context-menu/index.android.tsx b/apps/mobile/src/components/ui/context-menu/index.android.tsx new file mode 100644 index 0000000000..f36d0b7e6f --- /dev/null +++ b/apps/mobile/src/components/ui/context-menu/index.android.tsx @@ -0,0 +1,7 @@ +import type { FC } from "react" + +import type { ContextMenuProps } from "./index.ios" + +export const ContextMenu: FC = () => { + throw new Error("ContextMenu not implemented on Android") +} diff --git a/apps/mobile/src/components/ui/context-menu/index.ios.tsx b/apps/mobile/src/components/ui/context-menu/index.ios.tsx new file mode 100644 index 0000000000..5a5237d1ab --- /dev/null +++ b/apps/mobile/src/components/ui/context-menu/index.ios.tsx @@ -0,0 +1 @@ +export { default as ContextMenu, type ContextMenuProps } from "react-native-context-menu-view" diff --git a/apps/mobile/src/components/ui/context-menu/index.ts b/apps/mobile/src/components/ui/context-menu/index.ts new file mode 100644 index 0000000000..df697997b9 --- /dev/null +++ b/apps/mobile/src/components/ui/context-menu/index.ts @@ -0,0 +1 @@ +export * from "./index.ios" diff --git a/apps/mobile/src/components/ui/icon/fallback-icon.tsx b/apps/mobile/src/components/ui/icon/fallback-icon.tsx new file mode 100644 index 0000000000..1989071514 --- /dev/null +++ b/apps/mobile/src/components/ui/icon/fallback-icon.tsx @@ -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 +}) => { + 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 {isCJK ? title[0] : title.slice(0, 2)} + }, [title]) + + return ( + + {renderedText} + + ) +} + +const styles = StyleSheet.create({ + container: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + text: { + fontSize: 12, + color: "#fff", + }, +}) diff --git a/apps/mobile/src/components/ui/icon/feed-icon.tsx b/apps/mobile/src/components/ui/icon/feed-icon.tsx new file mode 100644 index 0000000000..48a4db177a --- /dev/null +++ b/apps/mobile/src/components/ui/icon/feed-icon.tsx @@ -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 & { + 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 +} diff --git a/apps/mobile/src/components/ui/loading/index.tsx b/apps/mobile/src/components/ui/loading/index.tsx new file mode 100644 index 0000000000..bd9f645e44 --- /dev/null +++ b/apps/mobile/src/components/ui/loading/index.tsx @@ -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 ( + + + + + {children} + + ) +} diff --git a/apps/mobile/src/components/ui/logo/index.tsx b/apps/mobile/src/components/ui/logo/index.tsx new file mode 100644 index 0000000000..a54b89f6ce --- /dev/null +++ b/apps/mobile/src/components/ui/logo/index.tsx @@ -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 ( + + + + + + + + + + + + ) +} + +export const FollowIcon: React.FC<{ color: string; style?: StyleProp }> = ({ + color, + style, +}) => ( + + + + + + + + + + +) diff --git a/apps/mobile/src/components/ui/pressable/item-pressable.tsx b/apps/mobile/src/components/ui/pressable/item-pressable.tsx new file mode 100644 index 0000000000..e614596ee3 --- /dev/null +++ b/apps/mobile/src/components/ui/pressable/item-pressable.tsx @@ -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 ( + 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} + + ) +}) diff --git a/apps/mobile/src/constants/env.ts b/apps/mobile/src/constants/env.ts new file mode 100644 index 0000000000..c96372730c --- /dev/null +++ b/apps/mobile/src/constants/env.ts @@ -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", + }, +} diff --git a/apps/mobile/src/constants/ui.ts b/apps/mobile/src/constants/ui.ts new file mode 100644 index 0000000000..caa3ddddd9 --- /dev/null +++ b/apps/mobile/src/constants/ui.ts @@ -0,0 +1 @@ +export const bottomViewTabHeight = 35 diff --git a/apps/mobile/src/constants/views.tsx b/apps/mobile/src/constants/views.tsx new file mode 100644 index 0000000000..18a61d48b5 --- /dev/null +++ b/apps/mobile/src/constants/views.tsx @@ -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, + }, +] diff --git a/apps/mobile/src/database/index.ts b/apps/mobile/src/database/index.ts new file mode 100644 index 0000000000..cef2d2206d --- /dev/null +++ b/apps/mobile/src/database/index.ts @@ -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 & { + $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()) diff --git a/apps/mobile/src/database/schemas/index.ts b/apps/mobile/src/database/schemas/index.ts new file mode 100644 index 0000000000..600ec76c07 --- /dev/null +++ b/apps/mobile/src/database/schemas/index.ts @@ -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(), + 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(), + description: text("description"), + view: integer("view").notNull().$type(), + 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(), +}) diff --git a/apps/mobile/src/database/schemas/types.ts b/apps/mobile/src/database/schemas/types.ts new file mode 100644 index 0000000000..12fefcc6ad --- /dev/null +++ b/apps/mobile/src/database/schemas/types.ts @@ -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 diff --git a/apps/mobile/src/global.css b/apps/mobile/src/global.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/apps/mobile/src/global.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/mobile/src/icons/AZ_sort_ascending_letters_cute_re.tsx b/apps/mobile/src/icons/AZ_sort_ascending_letters_cute_re.tsx new file mode 100644 index 0000000000..d62d496b24 --- /dev/null +++ b/apps/mobile/src/icons/AZ_sort_ascending_letters_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/AZ_sort_descending_letters_cute_re.tsx b/apps/mobile/src/icons/AZ_sort_descending_letters_cute_re.tsx new file mode 100644 index 0000000000..ce6824c76e --- /dev/null +++ b/apps/mobile/src/icons/AZ_sort_descending_letters_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/VIP_2_cute_fi.tsx b/apps/mobile/src/icons/VIP_2_cute_fi.tsx new file mode 100644 index 0000000000..5c6da0d0f4 --- /dev/null +++ b/apps/mobile/src/icons/VIP_2_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/VIP_2_cute_re.tsx b/apps/mobile/src/icons/VIP_2_cute_re.tsx new file mode 100644 index 0000000000..5e4ae66409 --- /dev/null +++ b/apps/mobile/src/icons/VIP_2_cute_re.tsx @@ -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 ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/add_cute_re.tsx b/apps/mobile/src/icons/add_cute_re.tsx new file mode 100644 index 0000000000..c2c63942c8 --- /dev/null +++ b/apps/mobile/src/icons/add_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/align_justify_cute_re.tsx b/apps/mobile/src/icons/align_justify_cute_re.tsx new file mode 100644 index 0000000000..5ab9c094e3 --- /dev/null +++ b/apps/mobile/src/icons/align_justify_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/align_left_cute_re.tsx b/apps/mobile/src/icons/align_left_cute_re.tsx new file mode 100644 index 0000000000..a5cc816272 --- /dev/null +++ b/apps/mobile/src/icons/align_left_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/announcement_cute_fi.tsx b/apps/mobile/src/icons/announcement_cute_fi.tsx new file mode 100644 index 0000000000..58ec8c66a1 --- /dev/null +++ b/apps/mobile/src/icons/announcement_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/apple_cute_fi.tsx b/apps/mobile/src/icons/apple_cute_fi.tsx new file mode 100644 index 0000000000..76dd1bf3c8 --- /dev/null +++ b/apps/mobile/src/icons/apple_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/arrow_right_circle_cute_fi.tsx b/apps/mobile/src/icons/arrow_right_circle_cute_fi.tsx new file mode 100644 index 0000000000..232cebf42a --- /dev/null +++ b/apps/mobile/src/icons/arrow_right_circle_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/arrow_right_up_cute_re.tsx b/apps/mobile/src/icons/arrow_right_up_cute_re.tsx new file mode 100644 index 0000000000..445b9036ba --- /dev/null +++ b/apps/mobile/src/icons/arrow_right_up_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/back_2_cute_re.tsx b/apps/mobile/src/icons/back_2_cute_re.tsx new file mode 100644 index 0000000000..ff3f5d1a89 --- /dev/null +++ b/apps/mobile/src/icons/back_2_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/bug_cute_re.tsx b/apps/mobile/src/icons/bug_cute_re.tsx new file mode 100644 index 0000000000..dba4edd3c1 --- /dev/null +++ b/apps/mobile/src/icons/bug_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/celebrate_cute_re.tsx b/apps/mobile/src/icons/celebrate_cute_re.tsx new file mode 100644 index 0000000000..17bc899aae --- /dev/null +++ b/apps/mobile/src/icons/celebrate_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/certificate_cute_fi.tsx b/apps/mobile/src/icons/certificate_cute_fi.tsx new file mode 100644 index 0000000000..15b843b580 --- /dev/null +++ b/apps/mobile/src/icons/certificate_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/certificate_cute_re.tsx b/apps/mobile/src/icons/certificate_cute_re.tsx new file mode 100644 index 0000000000..85e2f81b06 --- /dev/null +++ b/apps/mobile/src/icons/certificate_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/check_circle_cute_re.tsx b/apps/mobile/src/icons/check_circle_cute_re.tsx new file mode 100644 index 0000000000..9375997809 --- /dev/null +++ b/apps/mobile/src/icons/check_circle_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/check_circle_filled.tsx b/apps/mobile/src/icons/check_circle_filled.tsx new file mode 100644 index 0000000000..24b373e91b --- /dev/null +++ b/apps/mobile/src/icons/check_circle_filled.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/check_filled.tsx b/apps/mobile/src/icons/check_filled.tsx new file mode 100644 index 0000000000..f6454863f0 --- /dev/null +++ b/apps/mobile/src/icons/check_filled.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/close_cute_re.tsx b/apps/mobile/src/icons/close_cute_re.tsx new file mode 100644 index 0000000000..c5b1930617 --- /dev/null +++ b/apps/mobile/src/icons/close_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/compass_3_cute_re.tsx b/apps/mobile/src/icons/compass_3_cute_re.tsx new file mode 100644 index 0000000000..a622a82ba7 --- /dev/null +++ b/apps/mobile/src/icons/compass_3_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/compass_cute_fi.tsx b/apps/mobile/src/icons/compass_cute_fi.tsx new file mode 100644 index 0000000000..ccafbb3e8f --- /dev/null +++ b/apps/mobile/src/icons/compass_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/copy_2_cute_re.tsx b/apps/mobile/src/icons/copy_2_cute_re.tsx new file mode 100644 index 0000000000..cbea148f9b --- /dev/null +++ b/apps/mobile/src/icons/copy_2_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/copy_cute_re.tsx b/apps/mobile/src/icons/copy_cute_re.tsx new file mode 100644 index 0000000000..52c03ad1c4 --- /dev/null +++ b/apps/mobile/src/icons/copy_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/cursor_3_cute_re.tsx b/apps/mobile/src/icons/cursor_3_cute_re.tsx new file mode 100644 index 0000000000..7c35eb8ddc --- /dev/null +++ b/apps/mobile/src/icons/cursor_3_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/delete_2_cute_re.tsx b/apps/mobile/src/icons/delete_2_cute_re.tsx new file mode 100644 index 0000000000..371d78addc --- /dev/null +++ b/apps/mobile/src/icons/delete_2_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/department_cute_re.tsx b/apps/mobile/src/icons/department_cute_re.tsx new file mode 100644 index 0000000000..c94561bb5f --- /dev/null +++ b/apps/mobile/src/icons/department_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/discord_cute_fi.tsx b/apps/mobile/src/icons/discord_cute_fi.tsx new file mode 100644 index 0000000000..68f886c09f --- /dev/null +++ b/apps/mobile/src/icons/discord_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/docment_cute_fi.tsx b/apps/mobile/src/icons/docment_cute_fi.tsx new file mode 100644 index 0000000000..d2d3cfc6ce --- /dev/null +++ b/apps/mobile/src/icons/docment_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/docment_cute_re.tsx b/apps/mobile/src/icons/docment_cute_re.tsx new file mode 100644 index 0000000000..76241465d2 --- /dev/null +++ b/apps/mobile/src/icons/docment_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/download_2_cute_fi.tsx b/apps/mobile/src/icons/download_2_cute_fi.tsx new file mode 100644 index 0000000000..cbda2dc43b --- /dev/null +++ b/apps/mobile/src/icons/download_2_cute_fi.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/download_2_cute_re.tsx b/apps/mobile/src/icons/download_2_cute_re.tsx new file mode 100644 index 0000000000..1c114ac4b9 --- /dev/null +++ b/apps/mobile/src/icons/download_2_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/edit_cute_re.tsx b/apps/mobile/src/icons/edit_cute_re.tsx new file mode 100644 index 0000000000..3358f45785 --- /dev/null +++ b/apps/mobile/src/icons/edit_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/exit_cute_re.tsx b/apps/mobile/src/icons/exit_cute_re.tsx new file mode 100644 index 0000000000..5b0e5a0db9 --- /dev/null +++ b/apps/mobile/src/icons/exit_cute_re.tsx @@ -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 ( + + + + + ) +} diff --git a/apps/mobile/src/icons/external_link_cute_re.tsx b/apps/mobile/src/icons/external_link_cute_re.tsx new file mode 100644 index 0000000000..53fe1232b3 --- /dev/null +++ b/apps/mobile/src/icons/external_link_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ExternalLinkCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ExternalLinkCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ExternalLinkCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/eye_2_cute_re.tsx b/apps/mobile/src/icons/eye_2_cute_re.tsx new file mode 100644 index 0000000000..54cf2becf4 --- /dev/null +++ b/apps/mobile/src/icons/eye_2_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Eye2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Eye2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Eye2CuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/eye_close_cute_re.tsx b/apps/mobile/src/icons/eye_close_cute_re.tsx new file mode 100644 index 0000000000..039e9e5910 --- /dev/null +++ b/apps/mobile/src/icons/eye_close_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface EyeCloseCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const EyeCloseCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: EyeCloseCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/fast_forward_cute_re.tsx b/apps/mobile/src/icons/fast_forward_cute_re.tsx new file mode 100644 index 0000000000..f593aad984 --- /dev/null +++ b/apps/mobile/src/icons/fast_forward_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FastForwardCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FastForwardCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FastForwardCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/file_import_cute_re.tsx b/apps/mobile/src/icons/file_import_cute_re.tsx new file mode 100644 index 0000000000..ebb39bb880 --- /dev/null +++ b/apps/mobile/src/icons/file_import_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FileImportCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FileImportCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FileImportCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/file_upload_cute_re.tsx b/apps/mobile/src/icons/file_upload_cute_re.tsx new file mode 100644 index 0000000000..76fb9b9c09 --- /dev/null +++ b/apps/mobile/src/icons/file_upload_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FileUploadCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FileUploadCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FileUploadCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/fire_cute_fi.tsx b/apps/mobile/src/icons/fire_cute_fi.tsx new file mode 100644 index 0000000000..aaea192381 --- /dev/null +++ b/apps/mobile/src/icons/fire_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FireCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const FireCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FireCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/flag_1_cute_fi.tsx b/apps/mobile/src/icons/flag_1_cute_fi.tsx new file mode 100644 index 0000000000..01d1be3079 --- /dev/null +++ b/apps/mobile/src/icons/flag_1_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Flag1CuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const Flag1CuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Flag1CuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/folder_open_cute_re.tsx b/apps/mobile/src/icons/folder_open_cute_re.tsx new file mode 100644 index 0000000000..c3ed3e5a4c --- /dev/null +++ b/apps/mobile/src/icons/folder_open_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FolderOpenCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FolderOpenCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FolderOpenCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/forward_2_cute_re.tsx b/apps/mobile/src/icons/forward_2_cute_re.tsx new file mode 100644 index 0000000000..9fe632e1ec --- /dev/null +++ b/apps/mobile/src/icons/forward_2_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Forward2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Forward2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Forward2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/fullscreen_2_cute_re.tsx b/apps/mobile/src/icons/fullscreen_2_cute_re.tsx new file mode 100644 index 0000000000..82202e403d --- /dev/null +++ b/apps/mobile/src/icons/fullscreen_2_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Fullscreen2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Fullscreen2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Fullscreen2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/fullscreen_cute_re.tsx b/apps/mobile/src/icons/fullscreen_cute_re.tsx new file mode 100644 index 0000000000..2a14233837 --- /dev/null +++ b/apps/mobile/src/icons/fullscreen_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FullscreenCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FullscreenCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FullscreenCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/fullscreen_exit_cute_re.tsx b/apps/mobile/src/icons/fullscreen_exit_cute_re.tsx new file mode 100644 index 0000000000..bafe475803 --- /dev/null +++ b/apps/mobile/src/icons/fullscreen_exit_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface FullscreenExitCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const FullscreenExitCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: FullscreenExitCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/gift_cute_re.tsx b/apps/mobile/src/icons/gift_cute_re.tsx new file mode 100644 index 0000000000..aae89e1dc3 --- /dev/null +++ b/apps/mobile/src/icons/gift_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface GiftCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const GiftCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: GiftCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/github_2_cute_fi.tsx b/apps/mobile/src/icons/github_2_cute_fi.tsx new file mode 100644 index 0000000000..0082e8063b --- /dev/null +++ b/apps/mobile/src/icons/github_2_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Github2CuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const Github2CuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Github2CuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/github_cute_fi.tsx b/apps/mobile/src/icons/github_cute_fi.tsx new file mode 100644 index 0000000000..194a93f9fc --- /dev/null +++ b/apps/mobile/src/icons/github_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface GithubCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const GithubCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: GithubCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/google_cute_fi.tsx b/apps/mobile/src/icons/google_cute_fi.tsx new file mode 100644 index 0000000000..0a1320a277 --- /dev/null +++ b/apps/mobile/src/icons/google_cute_fi.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface GoogleCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const GoogleCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: GoogleCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/grid_2_cute_re.tsx b/apps/mobile/src/icons/grid_2_cute_re.tsx new file mode 100644 index 0000000000..f9d1263c82 --- /dev/null +++ b/apps/mobile/src/icons/grid_2_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Grid2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Grid2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Grid2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/grid_cute_re.tsx b/apps/mobile/src/icons/grid_cute_re.tsx new file mode 100644 index 0000000000..71464419a4 --- /dev/null +++ b/apps/mobile/src/icons/grid_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface GridCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const GridCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: GridCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/heart_cute_fi.tsx b/apps/mobile/src/icons/heart_cute_fi.tsx new file mode 100644 index 0000000000..fb670e544e --- /dev/null +++ b/apps/mobile/src/icons/heart_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface HeartCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const HeartCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: HeartCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/hotkey_cute_re.tsx b/apps/mobile/src/icons/hotkey_cute_re.tsx new file mode 100644 index 0000000000..3b21f5d6d5 --- /dev/null +++ b/apps/mobile/src/icons/hotkey_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface HotkeyCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const HotkeyCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: HotkeyCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/inbox_cute_fi.tsx b/apps/mobile/src/icons/inbox_cute_fi.tsx new file mode 100644 index 0000000000..1a957e8479 --- /dev/null +++ b/apps/mobile/src/icons/inbox_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface InboxCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const InboxCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: InboxCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/inbox_cute_re.tsx b/apps/mobile/src/icons/inbox_cute_re.tsx new file mode 100644 index 0000000000..a916cf856b --- /dev/null +++ b/apps/mobile/src/icons/inbox_cute_re.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface InboxCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const InboxCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: InboxCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/information_cute_re copy.tsx b/apps/mobile/src/icons/information_cute_re copy.tsx new file mode 100644 index 0000000000..ab3ae38cb6 --- /dev/null +++ b/apps/mobile/src/icons/information_cute_re copy.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface InformationCuteReCopyIconProps { + width?: number + height?: number + color?: string +} + +export const InformationCuteReCopyIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: InformationCuteReCopyIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/information_cute_re.tsx b/apps/mobile/src/icons/information_cute_re.tsx new file mode 100644 index 0000000000..17399a9435 --- /dev/null +++ b/apps/mobile/src/icons/information_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface InformationCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const InformationCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: InformationCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/layout_4_cute_re.tsx b/apps/mobile/src/icons/layout_4_cute_re.tsx new file mode 100644 index 0000000000..89128e520d --- /dev/null +++ b/apps/mobile/src/icons/layout_4_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Layout4CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Layout4CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Layout4CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/layout_leftbar_close_cute_re.tsx b/apps/mobile/src/icons/layout_leftbar_close_cute_re.tsx new file mode 100644 index 0000000000..84d00482e7 --- /dev/null +++ b/apps/mobile/src/icons/layout_leftbar_close_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LayoutLeftbarCloseCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const LayoutLeftbarCloseCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LayoutLeftbarCloseCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/layout_leftbar_open_cute_re.tsx b/apps/mobile/src/icons/layout_leftbar_open_cute_re.tsx new file mode 100644 index 0000000000..922a99fc44 --- /dev/null +++ b/apps/mobile/src/icons/layout_leftbar_open_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LayoutLeftbarOpenCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const LayoutLeftbarOpenCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LayoutLeftbarOpenCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/left_cute_fi.tsx b/apps/mobile/src/icons/left_cute_fi.tsx new file mode 100644 index 0000000000..d1b225d72b --- /dev/null +++ b/apps/mobile/src/icons/left_cute_fi.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LeftCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const LeftCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LeftCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/line_cute_re.tsx b/apps/mobile/src/icons/line_cute_re.tsx new file mode 100644 index 0000000000..1c348baf7a --- /dev/null +++ b/apps/mobile/src/icons/line_cute_re.tsx @@ -0,0 +1,21 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LineCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const LineCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LineCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/link_cute_re.tsx b/apps/mobile/src/icons/link_cute_re.tsx new file mode 100644 index 0000000000..4961462d47 --- /dev/null +++ b/apps/mobile/src/icons/link_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LinkCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const LinkCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LinkCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/list_check_3_cute_re.tsx b/apps/mobile/src/icons/list_check_3_cute_re.tsx new file mode 100644 index 0000000000..6793913102 --- /dev/null +++ b/apps/mobile/src/icons/list_check_3_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListCheck3CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ListCheck3CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListCheck3CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/list_check_cute_re.tsx b/apps/mobile/src/icons/list_check_cute_re.tsx new file mode 100644 index 0000000000..0894fad588 --- /dev/null +++ b/apps/mobile/src/icons/list_check_cute_re.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListCheckCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ListCheckCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListCheckCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/list_collapse_cute_fi.tsx b/apps/mobile/src/icons/list_collapse_cute_fi.tsx new file mode 100644 index 0000000000..cf247b5aad --- /dev/null +++ b/apps/mobile/src/icons/list_collapse_cute_fi.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListCollapseCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const ListCollapseCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListCollapseCuteFiIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/list_collapse_cute_re.tsx b/apps/mobile/src/icons/list_collapse_cute_re.tsx new file mode 100644 index 0000000000..31e72e7903 --- /dev/null +++ b/apps/mobile/src/icons/list_collapse_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListCollapseCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ListCollapseCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListCollapseCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/list_expansion_cute_fi.tsx b/apps/mobile/src/icons/list_expansion_cute_fi.tsx new file mode 100644 index 0000000000..f6437d28b4 --- /dev/null +++ b/apps/mobile/src/icons/list_expansion_cute_fi.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListExpansionCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const ListExpansionCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListExpansionCuteFiIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/list_expansion_cute_re.tsx b/apps/mobile/src/icons/list_expansion_cute_re.tsx new file mode 100644 index 0000000000..5740af66d6 --- /dev/null +++ b/apps/mobile/src/icons/list_expansion_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ListExpansionCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ListExpansionCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ListExpansionCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/loading_3_cute_li.tsx b/apps/mobile/src/icons/loading_3_cute_li.tsx new file mode 100644 index 0000000000..77790b0442 --- /dev/null +++ b/apps/mobile/src/icons/loading_3_cute_li.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Loading3CuteLiIconProps { + width?: number + height?: number + color?: string +} + +export const Loading3CuteLiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Loading3CuteLiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/loading_3_cute_re.tsx b/apps/mobile/src/icons/loading_3_cute_re.tsx new file mode 100644 index 0000000000..9ddd9d61cf --- /dev/null +++ b/apps/mobile/src/icons/loading_3_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Loading3CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Loading3CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Loading3CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/love_cute_re.tsx b/apps/mobile/src/icons/love_cute_re.tsx new file mode 100644 index 0000000000..00289e1c88 --- /dev/null +++ b/apps/mobile/src/icons/love_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface LoveCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const LoveCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: LoveCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/magic_2_cute_re.tsx b/apps/mobile/src/icons/magic_2_cute_re.tsx new file mode 100644 index 0000000000..28b69b806a --- /dev/null +++ b/apps/mobile/src/icons/magic_2_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Magic2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Magic2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Magic2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/mic_cute_fi.tsx b/apps/mobile/src/icons/mic_cute_fi.tsx new file mode 100644 index 0000000000..3fd93f77e8 --- /dev/null +++ b/apps/mobile/src/icons/mic_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface MicCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const MicCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: MicCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/mingcute_right_line.tsx b/apps/mobile/src/icons/mingcute_right_line.tsx new file mode 100644 index 0000000000..951c5e78b2 --- /dev/null +++ b/apps/mobile/src/icons/mingcute_right_line.tsx @@ -0,0 +1,23 @@ +import Svg, { G, Path } from "react-native-svg" + +export function MingcuteRightLine({ + color, + height, + width, +}: { + width?: number + height?: number + color?: string +}) { + return ( + + + + + + + ) +} diff --git a/apps/mobile/src/icons/more_1_cute_re.tsx b/apps/mobile/src/icons/more_1_cute_re.tsx new file mode 100644 index 0000000000..63725ccbfd --- /dev/null +++ b/apps/mobile/src/icons/more_1_cute_re.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface More1CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const More1CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: More1CuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/numbers_09_sort_ascending_cute_re.tsx b/apps/mobile/src/icons/numbers_09_sort_ascending_cute_re.tsx new file mode 100644 index 0000000000..9cd3166cf2 --- /dev/null +++ b/apps/mobile/src/icons/numbers_09_sort_ascending_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Numbers09SortAscendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Numbers09SortAscendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Numbers09SortAscendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/numbers_09_sort_descending_cute_re.tsx b/apps/mobile/src/icons/numbers_09_sort_descending_cute_re.tsx new file mode 100644 index 0000000000..85c8cf3912 --- /dev/null +++ b/apps/mobile/src/icons/numbers_09_sort_descending_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Numbers09SortDescendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Numbers09SortDescendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Numbers09SortDescendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/numbers_90_sort_ascending_cute_re.tsx b/apps/mobile/src/icons/numbers_90_sort_ascending_cute_re.tsx new file mode 100644 index 0000000000..77bc34767b --- /dev/null +++ b/apps/mobile/src/icons/numbers_90_sort_ascending_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Numbers90SortAscendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Numbers90SortAscendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Numbers90SortAscendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/numbers_90_sort_descending_cute_re.tsx b/apps/mobile/src/icons/numbers_90_sort_descending_cute_re.tsx new file mode 100644 index 0000000000..3f60e3bb6d --- /dev/null +++ b/apps/mobile/src/icons/numbers_90_sort_descending_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Numbers90SortDescendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Numbers90SortDescendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Numbers90SortDescendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/palette_cute_re.tsx b/apps/mobile/src/icons/palette_cute_re.tsx new file mode 100644 index 0000000000..9f07abc8eb --- /dev/null +++ b/apps/mobile/src/icons/palette_cute_re.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PaletteCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const PaletteCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PaletteCuteReIconProps) => { + return ( + + + + + + + ) +} diff --git a/apps/mobile/src/icons/paper_cute_fi.tsx b/apps/mobile/src/icons/paper_cute_fi.tsx new file mode 100644 index 0000000000..628aec763b --- /dev/null +++ b/apps/mobile/src/icons/paper_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PaperCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const PaperCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PaperCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/pause_cute_fi.tsx b/apps/mobile/src/icons/pause_cute_fi.tsx new file mode 100644 index 0000000000..b187775038 --- /dev/null +++ b/apps/mobile/src/icons/pause_cute_fi.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PauseCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const PauseCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PauseCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/pause_cute_re.tsx b/apps/mobile/src/icons/pause_cute_re.tsx new file mode 100644 index 0000000000..27619cfa7d --- /dev/null +++ b/apps/mobile/src/icons/pause_cute_re.tsx @@ -0,0 +1,21 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PauseCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const PauseCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PauseCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/photo_album_cute_fi.tsx b/apps/mobile/src/icons/photo_album_cute_fi.tsx new file mode 100644 index 0000000000..24b01b77f0 --- /dev/null +++ b/apps/mobile/src/icons/photo_album_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PhotoAlbumCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const PhotoAlbumCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PhotoAlbumCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/photo_album_cute_re.tsx b/apps/mobile/src/icons/photo_album_cute_re.tsx new file mode 100644 index 0000000000..8228d895c0 --- /dev/null +++ b/apps/mobile/src/icons/photo_album_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PhotoAlbumCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const PhotoAlbumCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PhotoAlbumCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/pic_cute_fi.tsx b/apps/mobile/src/icons/pic_cute_fi.tsx new file mode 100644 index 0000000000..fa30b9fa0f --- /dev/null +++ b/apps/mobile/src/icons/pic_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PicCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const PicCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PicCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/play_cute_fi.tsx b/apps/mobile/src/icons/play_cute_fi.tsx new file mode 100644 index 0000000000..9e8645e335 --- /dev/null +++ b/apps/mobile/src/icons/play_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PlayCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const PlayCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PlayCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/play_cute_re.tsx b/apps/mobile/src/icons/play_cute_re.tsx new file mode 100644 index 0000000000..a4bbc604fb --- /dev/null +++ b/apps/mobile/src/icons/play_cute_re.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PlayCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const PlayCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PlayCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/power.tsx b/apps/mobile/src/icons/power.tsx new file mode 100644 index 0000000000..5268567df2 --- /dev/null +++ b/apps/mobile/src/icons/power.tsx @@ -0,0 +1,21 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PowerIconProps { + width?: number + height?: number + color?: string +} + +export const PowerIcon = ({ width = 24, height = 24, color = "#10161F" }: PowerIconProps) => { + return ( + + + + ) +} diff --git a/apps/mobile/src/icons/power_mono.tsx b/apps/mobile/src/icons/power_mono.tsx new file mode 100644 index 0000000000..1946376cce --- /dev/null +++ b/apps/mobile/src/icons/power_mono.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PowerMonoIconProps { + width?: number + height?: number + color?: string +} + +export const PowerMonoIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PowerMonoIconProps) => { + return ( + + + + ) +} diff --git a/apps/mobile/src/icons/power_outline.tsx b/apps/mobile/src/icons/power_outline.tsx new file mode 100644 index 0000000000..7644208476 --- /dev/null +++ b/apps/mobile/src/icons/power_outline.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface PowerOutlineIconProps { + width?: number + height?: number + color?: string +} + +export const PowerOutlineIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: PowerOutlineIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/question_cute_re.tsx b/apps/mobile/src/icons/question_cute_re.tsx new file mode 100644 index 0000000000..b03eb7af7b --- /dev/null +++ b/apps/mobile/src/icons/question_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface QuestionCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const QuestionCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: QuestionCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/rada_cute_fi.tsx b/apps/mobile/src/icons/rada_cute_fi.tsx new file mode 100644 index 0000000000..c124d6eaf2 --- /dev/null +++ b/apps/mobile/src/icons/rada_cute_fi.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RadaCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const RadaCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RadaCuteFiIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/rada_cute_re.tsx b/apps/mobile/src/icons/rada_cute_re.tsx new file mode 100644 index 0000000000..2b93c9b5f5 --- /dev/null +++ b/apps/mobile/src/icons/rada_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RadaCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const RadaCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RadaCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/refresh_2_cute_re.tsx b/apps/mobile/src/icons/refresh_2_cute_re.tsx new file mode 100644 index 0000000000..a5a359cf15 --- /dev/null +++ b/apps/mobile/src/icons/refresh_2_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Refresh2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Refresh2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Refresh2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/right_cute_fi.tsx b/apps/mobile/src/icons/right_cute_fi.tsx new file mode 100644 index 0000000000..bd7eb2f6a9 --- /dev/null +++ b/apps/mobile/src/icons/right_cute_fi.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RightCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const RightCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RightCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/right_cute_li.tsx b/apps/mobile/src/icons/right_cute_li.tsx new file mode 100644 index 0000000000..10f290fda3 --- /dev/null +++ b/apps/mobile/src/icons/right_cute_li.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RightCuteLiIconProps { + width?: number + height?: number + color?: string +} + +export const RightCuteLiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RightCuteLiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/right_cute_re.tsx b/apps/mobile/src/icons/right_cute_re.tsx new file mode 100644 index 0000000000..1837a488b6 --- /dev/null +++ b/apps/mobile/src/icons/right_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RightCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const RightCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RightCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/rocket_cute_fi.tsx b/apps/mobile/src/icons/rocket_cute_fi.tsx new file mode 100644 index 0000000000..52cc9af948 --- /dev/null +++ b/apps/mobile/src/icons/rocket_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RocketCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const RocketCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RocketCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/rocket_cute_re.tsx b/apps/mobile/src/icons/rocket_cute_re.tsx new file mode 100644 index 0000000000..6df85b07b9 --- /dev/null +++ b/apps/mobile/src/icons/rocket_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RocketCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const RocketCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RocketCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/rss_2_cute_fi.tsx b/apps/mobile/src/icons/rss_2_cute_fi.tsx new file mode 100644 index 0000000000..cba3964b5a --- /dev/null +++ b/apps/mobile/src/icons/rss_2_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Rss2CuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const Rss2CuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Rss2CuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/rss_cute_fi.tsx b/apps/mobile/src/icons/rss_cute_fi.tsx new file mode 100644 index 0000000000..1f4c84ae38 --- /dev/null +++ b/apps/mobile/src/icons/rss_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface RssCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const RssCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: RssCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/sad_cute_re.tsx b/apps/mobile/src/icons/sad_cute_re.tsx new file mode 100644 index 0000000000..16d383e41e --- /dev/null +++ b/apps/mobile/src/icons/sad_cute_re.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface SadCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const SadCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: SadCuteReIconProps) => { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/icons/safari_cute-re.tsx b/apps/mobile/src/icons/safari_cute-re.tsx new file mode 100644 index 0000000000..c1818203d9 --- /dev/null +++ b/apps/mobile/src/icons/safari_cute-re.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import type { SvgProps } from "react-native-svg" +import Svg, { Path } from "react-native-svg" + +export const SafariCuteIcon = ( + props: SvgProps & { + color?: string + }, +) => ( + + + + + +) diff --git a/apps/mobile/src/icons/safari_cute_fi.tsx b/apps/mobile/src/icons/safari_cute_fi.tsx new file mode 100644 index 0000000000..077cd42e6d --- /dev/null +++ b/apps/mobile/src/icons/safari_cute_fi.tsx @@ -0,0 +1,15 @@ +import * as React from "react" +import type { SvgProps } from "react-native-svg" +import Svg, { Path } from "react-native-svg" + +export const SafariCuteFi = (props: SvgProps) => ( + + + + +) diff --git a/apps/mobile/src/icons/search_2_cute_re.tsx b/apps/mobile/src/icons/search_2_cute_re.tsx new file mode 100644 index 0000000000..fa803559d9 --- /dev/null +++ b/apps/mobile/src/icons/search_2_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Search2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Search2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Search2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/setting_7_cute_fi.tsx b/apps/mobile/src/icons/setting_7_cute_fi.tsx new file mode 100644 index 0000000000..dde4d9adaa --- /dev/null +++ b/apps/mobile/src/icons/setting_7_cute_fi.tsx @@ -0,0 +1,15 @@ +import * as React from "react" +import type { SvgProps } from "react-native-svg" +import Svg, { Path } from "react-native-svg" + +export const Setting7CuteFi = (props: SvgProps) => ( + + + + +) diff --git a/apps/mobile/src/icons/settings_7_cute_re.tsx b/apps/mobile/src/icons/settings_7_cute_re.tsx new file mode 100644 index 0000000000..6ebc360446 --- /dev/null +++ b/apps/mobile/src/icons/settings_7_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Settings7CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Settings7CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Settings7CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/share_3_cute_re.tsx b/apps/mobile/src/icons/share_3_cute_re.tsx new file mode 100644 index 0000000000..a37b1aaa60 --- /dev/null +++ b/apps/mobile/src/icons/share_3_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Share3CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Share3CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Share3CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/share_forward_cute_re.tsx b/apps/mobile/src/icons/share_forward_cute_re.tsx new file mode 100644 index 0000000000..8d4f1589ca --- /dev/null +++ b/apps/mobile/src/icons/share_forward_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface ShareForwardCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const ShareForwardCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: ShareForwardCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/social_x_cute_li.tsx b/apps/mobile/src/icons/social_x_cute_li.tsx new file mode 100644 index 0000000000..79e561a1f8 --- /dev/null +++ b/apps/mobile/src/icons/social_x_cute_li.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface SocialXCuteLiIconProps { + width?: number + height?: number + color?: string +} + +export const SocialXCuteLiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: SocialXCuteLiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/social_x_cute_re.tsx b/apps/mobile/src/icons/social_x_cute_re.tsx new file mode 100644 index 0000000000..1ba7e58a7d --- /dev/null +++ b/apps/mobile/src/icons/social_x_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface SocialXCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const SocialXCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: SocialXCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/sort_ascending_cute_re.tsx b/apps/mobile/src/icons/sort_ascending_cute_re.tsx new file mode 100644 index 0000000000..e7d0ec920f --- /dev/null +++ b/apps/mobile/src/icons/sort_ascending_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface SortAscendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const SortAscendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: SortAscendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/sort_descending_cute_re.tsx b/apps/mobile/src/icons/sort_descending_cute_re.tsx new file mode 100644 index 0000000000..37f69f63a6 --- /dev/null +++ b/apps/mobile/src/icons/sort_descending_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface SortDescendingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const SortDescendingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: SortDescendingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/star_cute_fi.tsx b/apps/mobile/src/icons/star_cute_fi.tsx new file mode 100644 index 0000000000..429ce01fdb --- /dev/null +++ b/apps/mobile/src/icons/star_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface StarCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const StarCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: StarCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/star_cute_re.tsx b/apps/mobile/src/icons/star_cute_re.tsx new file mode 100644 index 0000000000..d9ab4287ba --- /dev/null +++ b/apps/mobile/src/icons/star_cute_re.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface StarCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const StarCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: StarCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/time_cute_re copy.tsx b/apps/mobile/src/icons/time_cute_re copy.tsx new file mode 100644 index 0000000000..597dbf373a --- /dev/null +++ b/apps/mobile/src/icons/time_cute_re copy.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TimeCuteReCopyIconProps { + width?: number + height?: number + color?: string +} + +export const TimeCuteReCopyIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TimeCuteReCopyIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/time_cute_re.tsx b/apps/mobile/src/icons/time_cute_re.tsx new file mode 100644 index 0000000000..8f98fee808 --- /dev/null +++ b/apps/mobile/src/icons/time_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TimeCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const TimeCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TimeCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/train_cute_fi.tsx b/apps/mobile/src/icons/train_cute_fi.tsx new file mode 100644 index 0000000000..baa3085a5c --- /dev/null +++ b/apps/mobile/src/icons/train_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TrainCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const TrainCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TrainCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/translate_2_cute_re.tsx b/apps/mobile/src/icons/translate_2_cute_re.tsx new file mode 100644 index 0000000000..c6743534c7 --- /dev/null +++ b/apps/mobile/src/icons/translate_2_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Translate2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Translate2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Translate2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/trending_up_cute_re.tsx b/apps/mobile/src/icons/trending_up_cute_re.tsx new file mode 100644 index 0000000000..e1c934cbd3 --- /dev/null +++ b/apps/mobile/src/icons/trending_up_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TrendingUpCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const TrendingUpCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TrendingUpCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/trophy_cute_re.tsx b/apps/mobile/src/icons/trophy_cute_re.tsx new file mode 100644 index 0000000000..8d2d6ebd8a --- /dev/null +++ b/apps/mobile/src/icons/trophy_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TrophyCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const TrophyCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TrophyCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/twitter_cute_fi.tsx b/apps/mobile/src/icons/twitter_cute_fi.tsx new file mode 100644 index 0000000000..b3d8d43984 --- /dev/null +++ b/apps/mobile/src/icons/twitter_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface TwitterCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const TwitterCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: TwitterCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/user_3_cute_re.tsx b/apps/mobile/src/icons/user_3_cute_re.tsx new file mode 100644 index 0000000000..94b82e7b68 --- /dev/null +++ b/apps/mobile/src/icons/user_3_cute_re.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface User3CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const User3CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: User3CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/user_heart_cute_fi.tsx b/apps/mobile/src/icons/user_heart_cute_fi.tsx new file mode 100644 index 0000000000..63535109a0 --- /dev/null +++ b/apps/mobile/src/icons/user_heart_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface UserHeartCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const UserHeartCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: UserHeartCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/user_heart_cute_re.tsx b/apps/mobile/src/icons/user_heart_cute_re.tsx new file mode 100644 index 0000000000..16896de40a --- /dev/null +++ b/apps/mobile/src/icons/user_heart_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface UserHeartCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const UserHeartCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: UserHeartCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/user_setting_cute_re.tsx b/apps/mobile/src/icons/user_setting_cute_re.tsx new file mode 100644 index 0000000000..7cafdcf797 --- /dev/null +++ b/apps/mobile/src/icons/user_setting_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface UserSettingCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const UserSettingCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: UserSettingCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/video_cute_fi.tsx b/apps/mobile/src/icons/video_cute_fi.tsx new file mode 100644 index 0000000000..2621c02a95 --- /dev/null +++ b/apps/mobile/src/icons/video_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface VideoCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const VideoCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: VideoCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/voice_cute_re.tsx b/apps/mobile/src/icons/voice_cute_re.tsx new file mode 100644 index 0000000000..d8c4bb151d --- /dev/null +++ b/apps/mobile/src/icons/voice_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface VoiceCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const VoiceCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: VoiceCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/volume_cute_re.tsx b/apps/mobile/src/icons/volume_cute_re.tsx new file mode 100644 index 0000000000..c19d66f1b4 --- /dev/null +++ b/apps/mobile/src/icons/volume_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface VolumeCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const VolumeCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: VolumeCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/volume_mute_cute_re.tsx b/apps/mobile/src/icons/volume_mute_cute_re.tsx new file mode 100644 index 0000000000..fec07290e2 --- /dev/null +++ b/apps/mobile/src/icons/volume_mute_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface VolumeMuteCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const VolumeMuteCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: VolumeMuteCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/volume_off_cute_re.tsx b/apps/mobile/src/icons/volume_off_cute_re.tsx new file mode 100644 index 0000000000..6819f8ea62 --- /dev/null +++ b/apps/mobile/src/icons/volume_off_cute_re.tsx @@ -0,0 +1,27 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface VolumeOffCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const VolumeOffCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: VolumeOffCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/wifi_off_cute_re.tsx b/apps/mobile/src/icons/wifi_off_cute_re.tsx new file mode 100644 index 0000000000..7df7442db1 --- /dev/null +++ b/apps/mobile/src/icons/wifi_off_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface WifiOffCuteReIconProps { + width?: number + height?: number + color?: string +} + +export const WifiOffCuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: WifiOffCuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/world_2_cute_fi.tsx b/apps/mobile/src/icons/world_2_cute_fi.tsx new file mode 100644 index 0000000000..40db9110ec --- /dev/null +++ b/apps/mobile/src/icons/world_2_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface World2CuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const World2CuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: World2CuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/world_2_cute_re.tsx b/apps/mobile/src/icons/world_2_cute_re.tsx new file mode 100644 index 0000000000..255f73e60e --- /dev/null +++ b/apps/mobile/src/icons/world_2_cute_re.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface World2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const World2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: World2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/icons/youtube_cute_fi.tsx b/apps/mobile/src/icons/youtube_cute_fi.tsx new file mode 100644 index 0000000000..fac64989c4 --- /dev/null +++ b/apps/mobile/src/icons/youtube_cute_fi.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface YoutubeCuteFiIconProps { + width?: number + height?: number + color?: string +} + +export const YoutubeCuteFiIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: YoutubeCuteFiIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/initialize/dayjs.ts b/apps/mobile/src/initialize/dayjs.ts new file mode 100644 index 0000000000..353912f07d --- /dev/null +++ b/apps/mobile/src/initialize/dayjs.ts @@ -0,0 +1,10 @@ +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" +import localizedFormat from "dayjs/plugin/localizedFormat" +import relativeTime from "dayjs/plugin/relativeTime" +// Initialize dayjs +export const initializeDayjs = () => { + dayjs.extend(duration) + dayjs.extend(relativeTime) + dayjs.extend(localizedFormat) +} diff --git a/apps/mobile/src/initialize/hydrate.ts b/apps/mobile/src/initialize/hydrate.ts new file mode 100644 index 0000000000..de93760dbe --- /dev/null +++ b/apps/mobile/src/initialize/hydrate.ts @@ -0,0 +1,2 @@ +export { hydrateDatabaseToStore } from "../services" +export const hydrateSettings = () => {} diff --git a/apps/mobile/src/initialize/index.ts b/apps/mobile/src/initialize/index.ts new file mode 100644 index 0000000000..4a446b28b2 --- /dev/null +++ b/apps/mobile/src/initialize/index.ts @@ -0,0 +1,27 @@ +import { initializeDb } from "../database" +import { initializeDayjs } from "./dayjs" +import { hydrateDatabaseToStore, hydrateSettings } from "./hydrate" +import { migrateDatabase } from "./migration" +/* eslint-disable no-console */ +export const initializeApp = async () => { + console.log(`Initialize...`) + + const now = Date.now() + initializeDb() + await apm("migrateDatabase", migrateDatabase) + initializeDayjs() + + await apm("hydrateSettings", hydrateSettings) + await apm("hydrateDatabaseToStore", hydrateDatabaseToStore) + + const loadingTime = Date.now() - now + console.log(`Initialize done,`, `${loadingTime}ms`) +} + +const apm = async (label: string, fn: () => Promise | any) => { + const start = Date.now() + const result = await fn() + const end = Date.now() + console.log(`${label} took ${end - start}ms`) + return result +} diff --git a/apps/mobile/src/initialize/migration.ts b/apps/mobile/src/initialize/migration.ts new file mode 100644 index 0000000000..e444dec466 --- /dev/null +++ b/apps/mobile/src/initialize/migration.ts @@ -0,0 +1,44 @@ +import { migrate } from "drizzle-orm/expo-sqlite/migrator" +import { useSyncExternalStore } from "react" + +import migrations from "@/drizzle/migrations" + +import { db } from "../database" + +let storeChangeFn: () => void +const subscribe = (onStoreChange: () => void) => { + storeChangeFn = onStoreChange + + return () => { + storeChangeFn = () => {} + } +} +const migrateStore = { + success: false, + error: null as Error | null, +} + +export const migrateDatabase = () => { + return migrate(db, migrations) + .then(() => { + migrateStore.success = true + storeChangeFn?.() + }) + .catch((error) => { + migrateStore.error = error + + console.error(error) + + storeChangeFn?.() + }) +} + +const getSnapshot = () => { + return migrateStore +} +const getServerSnapshot = () => { + return migrateStore +} +export const useDatabaseMigration = () => { + return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) +} diff --git a/apps/mobile/src/initialize/op.ts b/apps/mobile/src/initialize/op.ts new file mode 100644 index 0000000000..0d912e273e --- /dev/null +++ b/apps/mobile/src/initialize/op.ts @@ -0,0 +1,9 @@ +import { OpenPanel } from "@openpanel/web" + +export const op = new OpenPanel({ + clientId: process.env.EXPO_PUBLIC_OPENPANEL_CLIENT_ID ?? "", + trackScreenViews: true, + trackOutgoingLinks: true, + trackAttributes: true, + apiUrl: process.env.EXPO_PUBLIC_OPENPANEL_API_URL, +}) diff --git a/apps/mobile/src/lib/api-fetch.ts b/apps/mobile/src/lib/api-fetch.ts new file mode 100644 index 0000000000..05e5f03bc6 --- /dev/null +++ b/apps/mobile/src/lib/api-fetch.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-console */ +import type { AppType } from "@follow/shared" +import { ofetch } from "ofetch" + +import { getApiUrl } from "./env" + +const { hc } = require("hono/dist/cjs/client/client") as typeof import("hono/client") + +export const apiFetch = ofetch.create({ + retry: false, + + baseURL: getApiUrl(), + onRequest: async ({ options, request }) => { + const header = new Headers(options.headers) + + header.set("x-app-name", "Follow Mobile") + + // const sessionToken = await getSessionToken() + // if (sessionToken.value) { + // // header.set("cookie", `better-auth.session_token=${sessionToken.value};`) + // } + if (__DEV__) { + // Logger + console.log(`---> ${options.method} ${request as string}`) + } + + options.headers = header + }, + onRequestError: ({ error, request, options }) => { + if (__DEV__) { + console.log(`[Error] ---> ${options.method} ${request as string}`) + } + console.error(error) + }, + + onResponse: ({ response, request, options }) => { + if (__DEV__) { + console.log(`<--- ${response.status} ${options.method} ${request as string}`) + } + }, + onResponseError: ({ error, request, options, response }) => { + if (__DEV__) { + console.log(`<--- [Error] ${response.status} ${options.method} ${request as string}`) + } + console.error(error) + }, +}) + +export const apiClient = hc(getApiUrl(), { + fetch: async (input: any, options = {}) => + apiFetch(input.toString(), options).catch((err) => { + throw err + }), + headers() { + return { + "X-App-Name": "Follow Mobile", + } + }, +}) diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts new file mode 100644 index 0000000000..eb6079190d --- /dev/null +++ b/apps/mobile/src/lib/auth.ts @@ -0,0 +1,105 @@ +import { useQuery } from "@tanstack/react-query" +import type * as better_call from "better-call" +import { parse } from "cookie-es" + +import { getApiUrl } from "./env" +import { kv } from "./kv" +import { queryClient } from "./query-client" + +const { expoClient } = + require("@better-auth/expo/dist/client.js") as typeof import("@better-auth/expo/client") + +const { createAuthClient } = + require("better-auth/dist/react.js") as typeof import("better-auth/react") + +const storagePrefix = "follow_auth" +const authClient = createAuthClient({ + baseURL: `${getApiUrl()}/better-auth`, + plugins: [ + { + id: "getProviders", + $InferServerPlugin: {} as (typeof authPlugins)[0], + }, + expoClient({ + scheme: "follow", + storagePrefix, + storage: { + setItem(key, value) { + kv.setSync(key, value) + if (key === `${storagePrefix}_cookie`) { + queryClient.invalidateQueries({ queryKey: ["cookie"] }) + } + }, + getItem(key) { + return kv.getSync(key) + }, + }, + }), + ], +}) + +// @keep-sorted +export const { getCookie, getProviders, signIn, signOut, useSession } = authClient + +export interface AuthProvider { + name: string + id: string + color: string + icon: string +} + +export const useAuthProviders = () => { + return useQuery({ + queryKey: ["providers"], + queryFn: async () => (await getProviders()).data, + }) +} + +export const useAuthToken = () => { + const query = useQuery({ + queryKey: ["cookie"], + queryFn: getCookie, + }) + return { + ...query, + data: query.data ? parse(query.data)["__Secure-better-auth.session_token"] : null, + } +} + +// eslint-disable-next-line unused-imports/no-unused-vars +declare const authPlugins: { + id: "getProviders" + endpoints: { + getProviders: { + < + C extends [ + ( + | better_call.Context< + "/get-providers", + { + method: "GET" + } + > + | undefined + )?, + ], + >( + ...ctx: C + ): Promise< + C extends [ + { + asResponse: true + }, + ] + ? Response + : Record + > + path: "/get-providers" + options: { + method: "GET" + } + method: better_call.Method | better_call.Method[] + headers: Headers + } + } +}[] diff --git a/apps/mobile/src/lib/cookie.ts b/apps/mobile/src/lib/cookie.ts new file mode 100644 index 0000000000..99e42ebdc0 --- /dev/null +++ b/apps/mobile/src/lib/cookie.ts @@ -0,0 +1,29 @@ +import CookieManager from "@react-native-cookies/cookies" + +import { getApiUrl } from "./env" +import { kv } from "./kv" + +const keys = { + sessionToken: "better-auth.session_token", +} +export const setSessionToken = (token: string) => { + kv.setSync(keys.sessionToken, token) + + return CookieManager.set(getApiUrl(), { + name: keys.sessionToken, + value: token, + httpOnly: true, + secure: true, + }) +} + +export const getSessionToken = async () => { + const cookies = await CookieManager.get(getApiUrl()) + + return cookies[keys.sessionToken] || kv.getSync(keys.sessionToken) +} + +export const clearSessionToken = async () => { + await kv.delete(keys.sessionToken) + return CookieManager.clearAll() +} diff --git a/apps/mobile/src/lib/env.ts b/apps/mobile/src/lib/env.ts new file mode 100644 index 0000000000..c5a0f0ddcc --- /dev/null +++ b/apps/mobile/src/lib/env.ts @@ -0,0 +1,12 @@ +import { getEnvironment } from "../atoms/env" +import { appEndpointMap } from "../constants/env" + +export const getApiUrl = () => { + const env = getEnvironment() + return appEndpointMap[env].api +} + +export const getWebUrl = () => { + const env = getEnvironment() + return appEndpointMap[env].web +} diff --git a/apps/mobile/src/lib/hooks/useOpenLink.ts b/apps/mobile/src/lib/hooks/useOpenLink.ts new file mode 100644 index 0000000000..06a90b25f2 --- /dev/null +++ b/apps/mobile/src/lib/hooks/useOpenLink.ts @@ -0,0 +1,17 @@ +import * as WebBrowser from "expo-web-browser" +import { useCallback } from "react" + +// Ported from https://github.com/bluesky-social/social-app/blob/0c71f8196faf401dc9847b0bfb6559e5d7024940/src/lib/hooks/useOpenLink.ts +// License: MIT +export function useOpenLink() { + const openLink = useCallback(async (url: string) => { + WebBrowser.openBrowserAsync(url, { + presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, + // toolbarColor: theme.atoms.bg.backgroundColor, + // controlsColor: theme.palette.primary_500, + createTask: false, + }) + }, []) + + return openLink +} diff --git a/apps/mobile/src/lib/jotai.ts b/apps/mobile/src/lib/jotai.ts new file mode 100644 index 0000000000..95325686ce --- /dev/null +++ b/apps/mobile/src/lib/jotai.ts @@ -0,0 +1,23 @@ +import Storage from "expo-sqlite/kv-store" +import type { SyncStorage } from "jotai/vanilla/utils/atomWithStorage" + +export { createAtomAccessor, createAtomHooks, jotaiStore } from "@follow/utils" + +export const JotaiPersistSyncStorage: SyncStorage = { + getItem: (key, defaultValue) => { + const res = Storage.getItemSync(key) + if (res === null) { + return defaultValue + } + return JSON.parse(res) + }, + setItem: (key, value) => { + return Storage.setItemSync(key, JSON.stringify(value)) + }, + removeItem: (key) => { + return Storage.removeItemSync(key) + }, + subscribe() { + return () => {} + }, +} diff --git a/apps/mobile/src/lib/kv.ts b/apps/mobile/src/lib/kv.ts new file mode 100644 index 0000000000..532d6eed7e --- /dev/null +++ b/apps/mobile/src/lib/kv.ts @@ -0,0 +1,37 @@ +import Storage from "expo-sqlite/kv-store" + +class KV { + setSync(key: string, value: string) { + Storage.setItemSync(key, value) + } + + getSync(key: string) { + return Storage.getItemSync(key) + } + + set(key: string, value: string) { + Storage.setItem(key, value) + } + + get(key: string) { + return Storage.getItem(key) + } + + delete(key: string) { + return Storage.removeItem(key) + } + + clear() { + return Storage.clear() + } + + keys() { + return Storage.getAllKeysSync() + } + + [Symbol.iterator]() { + return Storage.getAllKeysSync().values() + } +} + +export const kv = new KV() diff --git a/apps/mobile/src/lib/query-client.ts b/apps/mobile/src/lib/query-client.ts new file mode 100644 index 0000000000..944dbea12c --- /dev/null +++ b/apps/mobile/src/lib/query-client.ts @@ -0,0 +1,3 @@ +import { QueryClient } from "@tanstack/react-query" + +export const queryClient = new QueryClient() diff --git a/apps/mobile/src/lib/responsive.ts b/apps/mobile/src/lib/responsive.ts new file mode 100644 index 0000000000..886ecb5d49 --- /dev/null +++ b/apps/mobile/src/lib/responsive.ts @@ -0,0 +1,47 @@ +import { useCallback } from "react" +import { Dimensions, useWindowDimensions } from "react-native" + +const baseWidth = 375 +const baseHeight = 812 +const windowDim = Dimensions.get("window") + +Dimensions.addEventListener("change", ({ window }) => { + Object.assign(windowDim, window) +}) +/** + * This scaleWidth is not responsive, it's just a simple scale util + * @param size + * @returns + */ +export const scaleWidth = (size: number) => { + return (size / baseWidth) * windowDim.width +} +/** + * This scaleHeight is not responsive, it's just a simple scale util + * @param size + * @returns + */ +export const scaleHeight = (size: number) => { + return (size / baseHeight) * windowDim.height +} +export const useScaleWidth = () => { + const windowDim = useWindowDimensions() + + return useCallback( + (size: number) => { + return (size / baseWidth) * windowDim.width + }, + [windowDim.width], + ) +} + +export const useScaleHeight = () => { + const windowDim = useWindowDimensions() + + return useCallback( + (size: number) => { + return (size / baseHeight) * windowDim.height + }, + [windowDim.height], + ) +} diff --git a/apps/mobile/src/main.ts b/apps/mobile/src/main.ts new file mode 100644 index 0000000000..15ceea7665 --- /dev/null +++ b/apps/mobile/src/main.ts @@ -0,0 +1,5 @@ +import { initializeApp } from "./initialize" + +initializeApp().then(() => { + require("expo-router/entry") +}) diff --git a/apps/mobile/src/modules/context-menu/feeds.tsx b/apps/mobile/src/modules/context-menu/feeds.tsx new file mode 100644 index 0000000000..c8abae623b --- /dev/null +++ b/apps/mobile/src/modules/context-menu/feeds.tsx @@ -0,0 +1,222 @@ +import type { FeedViewType } from "@follow/constants" +import type { FC, PropsWithChildren } from "react" +import { useCallback, useMemo } from "react" +import type { NativeSyntheticEvent } from "react-native" +import { Alert, Clipboard } from "react-native" +import type { + ContextMenuAction, + ContextMenuOnPressNativeEvent, +} from "react-native-context-menu-view" +import { useEventCallback } from "usehooks-ts" + +import { ContextMenu } from "@/src/components/ui/context-menu" +import { views } from "@/src/constants/views" +import { getFeed } from "@/src/store/feed/getter" +import { getSubscription } from "@/src/store/subscription/getter" +import { useListSubscriptionCategory } from "@/src/store/subscription/hooks" +import { subscriptionSyncService } from "@/src/store/subscription/store" +import { unreadSyncService } from "@/src/store/unread/store" + +type Options = { + categories: string[] +} +const createFeedItemActions: (options: Options) => ContextMenuAction[] = (options) => { + return [ + { + title: "Mark All As Read", + }, + { + title: "Claim", + }, + { + title: "Boost", + }, + { + title: "Add To Category", + actions: [ + ...options.categories.map((category) => ({ + title: category, + })), + { + title: "Create New Category", + systemIcon: "plus", + }, + ], + }, + { + title: "Edit", + }, + { + title: "Copy Link", + }, + { + title: "Unsubscribe", + destructive: true, + }, + ] +} +export const SubscriptionFeedItemContextMenu: FC< + PropsWithChildren & { + id: string + view: FeedViewType + } +> = ({ id, children, view }) => { + const allCategories = useListSubscriptionCategory(view) + const actions = useMemo( + () => createFeedItemActions({ categories: allCategories }), + [allCategories], + ) + + return ( + ) => { + const [first, second] = e.nativeEvent.indexPath + + switch (first) { + case 0: { + unreadSyncService.markAsRead(id) + break + } + case 1: { + // TODO: implement logic + break + } + case 2: { + // TODO: implement logic + break + } + case 3: { + // add to category + + if (!actions[3].actions) break + const newCategory = second === actions[3].actions.length - 1 + const subscription = getSubscription(id) + + if (!subscription) return + if (newCategory) { + // create new category + Alert.prompt("Create New Category", "Enter the name of the new category", (text) => { + subscriptionSyncService.edit({ + ...subscription, + category: text, + }) + }) + } else { + // add to category + subscriptionSyncService.edit({ + ...subscription, + category: actions[3].actions[second].title, + }) + } + + break + } + case 4: { + // edit + break + } + case 5: { + // copy link + const subscription = getSubscription(id) + if (!subscription) return + + switch (subscription.type) { + case "feed": { + if (!subscription.feedId) return + const feed = getFeed(subscription.feedId) + if (!feed) return + Clipboard.setString(feed.url) + return + } + } + break + } + + case 6: { + // unsubscribe + Alert.alert("Unsubscribe?", "This will remove the feed from your list", [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "Unsubscribe", + style: "destructive", + onPress: () => { + subscriptionSyncService.unsubscribe(id) + }, + }, + ]) + break + } + } + })} + > + {children} + + ) +} + +export const SubscriptionFeedCategoryContextMenu: FC< + { + category: string + feedIds: string[] + } & PropsWithChildren +> = ({ category: _, feedIds, children }) => { + return ( + [ + { + title: "Mark All As Read", + }, + { + title: "Change To Other View", + actions: views.map((view) => ({ + title: view.name, + })), + }, + { + title: "Edit Category", + }, + { + title: "Delete Category", + destructive: true, + }, + ], + [], + )} + onPress={useCallback( + (e: NativeSyntheticEvent) => { + const { name } = e.nativeEvent + const [first, second] = e.nativeEvent.indexPath + + if (first === 1) { + void second + // TODO change to other view + return + } + switch (name) { + case "Mark All As Read": { + unreadSyncService.markAsReadMany(feedIds) + break + } + case "Change To Other View": { + // TODO: implement logic + break + } + + case "Delete Category": { + // TODO: implement logic + break + } + } + }, + [feedIds], + )} + > + {children} + + ) +} diff --git a/apps/mobile/src/modules/context-menu/lists.tsx b/apps/mobile/src/modules/context-menu/lists.tsx new file mode 100644 index 0000000000..f3efd9f101 --- /dev/null +++ b/apps/mobile/src/modules/context-menu/lists.tsx @@ -0,0 +1,78 @@ +import type { FC, PropsWithChildren } from "react" +import { useMemo } from "react" +import type { NativeSyntheticEvent } from "react-native" +import { Alert, Clipboard } from "react-native" +import type { + ContextMenuAction, + ContextMenuOnPressNativeEvent, +} from "react-native-context-menu-view" +import { useEventCallback } from "usehooks-ts" + +import { ContextMenu } from "@/src/components/ui/context-menu" +import { getWebUrl } from "@/src/lib/env" +import { getList } from "@/src/store/list/getters" +import { useIsOwnList } from "@/src/store/list/hooks" +import { subscriptionSyncService } from "@/src/store/subscription/store" + +export const SubscriptionListItemContextMenu: FC< + PropsWithChildren & { + id: string + } +> = ({ id, children }) => { + const isOwnList = useIsOwnList(id) + const actions = useMemo( + () => + [ + isOwnList && { + title: "Edit", + }, + { + title: "Copy Link", + }, + { + title: "Unsubscribe", + destructive: true, + }, + ].filter(Boolean) as ContextMenuAction[], + [isOwnList], + ) + return ( + ) => { + const { name } = e.nativeEvent + + switch (name) { + case "Edit": { + // TODO: implement + break + } + case "Copy Link": { + const list = getList(id) + if (!list) return + Clipboard.setString(`${getWebUrl()}/share/lists/${list.id}`) + break + } + case "Unsubscribe": { + Alert.alert("Unsubscribe", "Are you sure you want to unsubscribe?", [ + { + text: "Cancel", + style: "cancel", + }, + { + text: "Unsubscribe", + style: "destructive", + onPress: () => { + subscriptionSyncService.unsubscribe(id) + }, + }, + ]) + break + } + } + })} + > + {children} + + ) +} diff --git a/apps/mobile/src/modules/debug/index.tsx b/apps/mobile/src/modules/debug/index.tsx new file mode 100644 index 0000000000..11724b74ca --- /dev/null +++ b/apps/mobile/src/modules/debug/index.tsx @@ -0,0 +1,64 @@ +import { router } from "expo-router" +import { useRef, useState } from "react" +import { Animated, Dimensions, PanResponder } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +import { BugCuteReIcon } from "@/src/icons/bug_cute_re" + +export const DebugButton = () => { + const insets = useSafeAreaInsets() + const windowWidth = Dimensions.get("window").width + const pan = useRef( + new Animated.ValueXY({ + x: insets.left, + y: 50, + }), + ).current + const [position, setPosition] = useState({ x: 0, y: 50 }) + + const panResponder = PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onPanResponderMove: (_, gesture) => { + const newX = position.x + gesture.dx + const newY = position.y + gesture.dy + + pan.setValue({ x: newX, y: newY }) + }, + + onPanResponderRelease: (_, gesture) => { + if (Math.abs(gesture.dx) < 5 && Math.abs(gesture.dy) < 5) { + router.push("/debug") + + return + } + + const newY = position.y + gesture.dy + + const snapToLeft = true + const finalX = snapToLeft ? insets.left : windowWidth - 40 - insets.right + + Animated.spring(pan, { + toValue: { x: finalX, y: newY }, + useNativeDriver: false, + }).start() + + setPosition({ x: finalX, y: newY }) + }, + }) + + return ( + + + + ) +} diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx new file mode 100644 index 0000000000..7b44837959 --- /dev/null +++ b/apps/mobile/src/modules/login/email.tsx @@ -0,0 +1,91 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useMutation } from "@tanstack/react-query" +import type { Control } from "react-hook-form" +import { useController, useForm } from "react-hook-form" +import type { TextInputProps } from "react-native" +import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native" +import { z } from "zod" + +import { ThemedText } from "@/src/components/common/ThemedText" +import { signIn } from "@/src/lib/auth" + +const formSchema = z.object({ + email: z.string().email(), + password: z.string().min(8).max(128), +}) + +type FormValue = z.infer + +async function onSubmit(values: FormValue) { + await signIn.email({ + email: values.email, + password: values.password, + }) +} + +function Input({ + control, + name, + ...rest +}: TextInputProps & { + control: Control + name: keyof FormValue +}) { + const { field } = useController({ + control, + name, + }) + return +} + +export function EmailLogin() { + const { control, handleSubmit, formState } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }) + + const submitMutation = useMutation({ + mutationFn: onSubmit, + }) + + return ( + + + + + + submitMutation.mutate(values))} + className="disabled:bg-gray-3 rounded-lg bg-accent p-3" + > + {submitMutation.isPending ? ( + + ) : ( + Continue with Email + )} + + + ) +} diff --git a/apps/mobile/src/modules/login/index.tsx b/apps/mobile/src/modules/login/index.tsx new file mode 100644 index 0000000000..f98a3b6503 --- /dev/null +++ b/apps/mobile/src/modules/login/index.tsx @@ -0,0 +1,35 @@ +import { TouchableWithoutFeedback, View } from "react-native" +import { KeyboardController } from "react-native-keyboard-controller" + +import { ThemedText } from "@/src/components/common/ThemedText" +import { Logo } from "@/src/components/ui/logo" + +import { EmailLogin } from "./email" +import { SocialLogin } from "./social" + +export function Login() { + return ( + + { + KeyboardController.dismiss() + }} + accessible={false} + > + + + Login to Follow + + + + + + + or + + + + + + ) +} diff --git a/apps/mobile/src/modules/login/social.tsx b/apps/mobile/src/modules/login/social.tsx new file mode 100644 index 0000000000..631a88cd2c --- /dev/null +++ b/apps/mobile/src/modules/login/social.tsx @@ -0,0 +1,99 @@ +import * as AppleAuthentication from "expo-apple-authentication" +import { useColorScheme } from "nativewind" +import { Platform, TouchableOpacity, View } from "react-native" + +import { AppleCuteFiIcon } from "@/src/icons/apple_cute_fi" +import { GithubCuteFiIcon } from "@/src/icons/github_cute_fi" +import { GoogleCuteFiIcon } from "@/src/icons/google_cute_fi" +import { signIn, useAuthProviders } from "@/src/lib/auth" + +const provider: Record< + string, + { + id: string + color: string + darkColor: string + icon: (props: { width?: number; height?: number; color?: string }) => React.ReactNode + } +> = { + google: { + id: "google", + color: "#3b82f6", + darkColor: "#2563eb", + icon: GoogleCuteFiIcon, + }, + github: { + id: "github", + color: "#000000", + darkColor: "#ffffff", + icon: GithubCuteFiIcon, + }, + apple: { + id: "apple", + color: "#1f2937", + darkColor: "#ffffff", + icon: AppleCuteFiIcon, + }, +} + +export function SocialLogin() { + const { data } = useAuthProviders() + const { colorScheme } = useColorScheme() + + return ( + + {Object.keys(provider) + .filter((key) => key !== "apple" || (Platform.OS === "ios" && key === "apple")) + .map((key) => { + const providerInfo = provider[key] + return ( + { + if (!data?.[providerInfo.id]) return + + if (providerInfo.id === "apple") { + try { + const credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], + }) + + if (credential.identityToken) { + await signIn.social({ + provider: "apple", + idToken: { + token: credential.identityToken, + }, + }) + } else { + throw new Error("No identityToken.") + } + } catch (e) { + console.error(e) + // handle errors + } + return + } + + signIn.social({ + provider: providerInfo.id as any, + callbackURL: "/", + }) + }} + disabled={!data?.[providerInfo.id]} + > + + + ) + })} + + ) +} diff --git a/apps/mobile/src/modules/subscription/ViewTab.tsx b/apps/mobile/src/modules/subscription/ViewTab.tsx new file mode 100644 index 0000000000..30cf0029e1 --- /dev/null +++ b/apps/mobile/src/modules/subscription/ViewTab.tsx @@ -0,0 +1,182 @@ +import { useHeaderHeight } from "@react-navigation/elements" +import { useAtomValue } from "jotai" +import { memo, useEffect, useRef, useState } from "react" +import type { TouchableOpacityProps } from "react-native" +import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native" +import type { WithSpringConfig } from "react-native-reanimated" +import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated" + +import { ThemedBlurView } from "@/src/components/common/ThemedBlurView" +import { ContextMenu } from "@/src/components/ui/context-menu" +import { bottomViewTabHeight } from "@/src/constants/ui" +import type { ViewDefinition } from "@/src/constants/views" +import { views } from "@/src/constants/views" +import { useUnreadCountByView } from "@/src/store/unread/hooks" +import { unreadSyncService } from "@/src/store/unread/store" + +import { offsetAtom, setCurrentView, viewAtom } from "./atoms" + +const springConfig: WithSpringConfig = { + damping: 20, + mass: 1, + stiffness: 120, +} +export const ViewTab = () => { + const headerHeight = useHeaderHeight() + const offset = useAtomValue(offsetAtom) + const currentView = useAtomValue(viewAtom) + const tabRef = useRef(null) + + const indicatorPosition = useSharedValue(0) + + const [tabWidths, setTabWidths] = useState([]) + const [tabPositions, setTabPositions] = useState([]) + const scrollOffsetX = useRef(0) + + useEffect(() => { + if (tabWidths.length > 0) { + indicatorPosition.value = withSpring(tabPositions[currentView] || 0, springConfig) + + if (tabRef.current) { + const x = currentView > 0 ? tabPositions[currentView - 1] + tabWidths[currentView - 1] : 0 + + const isCurrentTabVisible = + scrollOffsetX.current < tabPositions[currentView] && + scrollOffsetX.current + tabWidths[currentView] > tabPositions[currentView] + + if (!isCurrentTabVisible) { + tabRef.current.scrollTo({ x, y: 0, animated: true }) + } + } + } + }, [currentView, indicatorPosition, tabPositions, tabWidths]) + + const indicatorStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateX: indicatorPosition.value + 10 + Math.abs(offset), + }, + ], + backgroundColor: views[currentView].activeColor, + width: (tabWidths[currentView] || 20) - 40 + Math.abs(offset), + } + }) + + return ( + + + { + scrollOffsetX.current = event.nativeEvent.contentOffset.x + }} + showsHorizontalScrollIndicator={false} + className="border-tertiary-system-background" + horizontal + ref={tabRef} + contentContainerStyle={styles.tabScroller} + style={styles.root} + > + {views.map((view, index) => { + const isSelected = currentView === view.view + return ( + { + const { width, x } = event.nativeEvent.layout + setTabWidths((prev) => { + const newWidths = [...prev] + newWidths[index] = width + return newWidths + }) + setTabPositions((prev) => { + const newPositions = [...prev] + newPositions[index] = x + return newPositions + }) + }} + /> + ) + })} + + + + + ) +} + +const TabItem = memo( + ({ + isSelected, + view, + onLayout, + }: { isSelected: boolean; view: ViewDefinition } & Pick) => { + const unreadCount = useUnreadCountByView(view.view) + return ( + { + switch (e.nativeEvent.index) { + case 0: { + unreadSyncService.markViewAsRead(view.view) + break + } + } + }} + onLayout={onLayout} + > + setCurrentView(view.view)} + className="relative mr-4 flex-row items-center justify-center" + > + + + {view.name} + + {unreadCount > 0 && ( + + + {unreadCount > 99 ? "99+" : unreadCount} + + + )} + + + ) + }, +) +const styles = StyleSheet.create({ + indicator: { + position: "absolute", + bottom: 0, + height: 2, + borderRadius: 1, + }, + tabContainer: { + backgroundColor: "transparent", + bottom: 0, + left: 0, + position: "absolute", + width: "100%", + top: 0, + }, + tabScroller: { + alignItems: "center", + flexDirection: "row", + paddingHorizontal: 4, + }, + + root: { paddingHorizontal: 6 }, +}) diff --git a/apps/mobile/src/modules/subscription/atoms.ts b/apps/mobile/src/modules/subscription/atoms.ts new file mode 100644 index 0000000000..d6d51c780c --- /dev/null +++ b/apps/mobile/src/modules/subscription/atoms.ts @@ -0,0 +1,44 @@ +import { FeedViewType } from "@follow/constants" +import { createAtomHooks, jotaiStore } from "@follow/utils" +import { atom, useAtomValue } from "jotai" +import { atomWithStorage } from "jotai/utils" + +import { JotaiPersistSyncStorage } from "@/src/lib/jotai" + +export const viewAtom = atom(FeedViewType.Articles) + +export const useCurrentView = () => { + return useAtomValue(viewAtom) +} + +export const offsetAtom = atom(0) + +export const setCurrentView = (view: FeedViewType) => { + jotaiStore.set(viewAtom, view) +} + +export const [ + , + , + useFeedListSortMethod, + useSetFeedListSortMethod, + getFeedListSortMethod, + setFeedListSortMethod, +] = createAtomHooks( + atomWithStorage<"alphabet" | "count">("listSortMethod", "alphabet", JotaiPersistSyncStorage, { + getOnInit: true, + }), +) + +export const [ + , + , + useFeedListSortOrder, + useSetFeedListSortOrder, + getFeedListSortOrder, + setFeedListSortOrder, +] = createAtomHooks( + atomWithStorage<"asc" | "desc">("listSortOrder", "asc", JotaiPersistSyncStorage, { + getOnInit: true, + }), +) diff --git a/apps/mobile/src/modules/subscription/ctx.ts b/apps/mobile/src/modules/subscription/ctx.ts new file mode 100644 index 0000000000..70d178c998 --- /dev/null +++ b/apps/mobile/src/modules/subscription/ctx.ts @@ -0,0 +1,6 @@ +import type { FeedViewType } from "@follow/constants" +import { createContext, useContext } from "react" + +const ViewPageCurrentViewContext = createContext(null!) +export const ViewPageCurrentViewProvider = ViewPageCurrentViewContext.Provider +export const useViewPageCurrentView = () => useContext(ViewPageCurrentViewContext) diff --git a/apps/mobile/src/modules/subscription/header-actions.tsx b/apps/mobile/src/modules/subscription/header-actions.tsx new file mode 100644 index 0000000000..b98156bfc2 --- /dev/null +++ b/apps/mobile/src/modules/subscription/header-actions.tsx @@ -0,0 +1,79 @@ +import { TouchableOpacity } from "react-native" +import type { ContextMenuAction } from "react-native-context-menu-view" + +import { ContextMenu } from "@/src/components/ui/context-menu" +import { AZSortAscendingLettersCuteReIcon } from "@/src/icons/AZ_sort_ascending_letters_cute_re" +import { AZSortDescendingLettersCuteReIcon } from "@/src/icons/AZ_sort_descending_letters_cute_re" +import { Numbers90SortAscendingCuteReIcon } from "@/src/icons/numbers_90_sort_ascending_cute_re" +import { Numbers90SortDescendingCuteReIcon } from "@/src/icons/numbers_90_sort_descending_cute_re" +import { accentColor } from "@/src/theme/colors" + +import { + setFeedListSortMethod, + setFeedListSortOrder, + useFeedListSortMethod, + useFeedListSortOrder, +} from "./atoms" + +const map = { + alphabet: { + asc: AZSortAscendingLettersCuteReIcon, + desc: AZSortDescendingLettersCuteReIcon, + }, + count: { + desc: Numbers90SortDescendingCuteReIcon, + asc: Numbers90SortAscendingCuteReIcon, + }, +} + +export const SortActionButton = () => { + const sortMethod = useFeedListSortMethod() + const sortOrder = useFeedListSortOrder() + + const orderActions: ContextMenuAction[] = [ + { title: "Ascending", selected: sortOrder === "asc" }, + { title: "Descending", selected: sortOrder === "desc" }, + ] + + const actions: ContextMenuAction[] = [ + { title: "Alphabet", actions: orderActions, selected: sortMethod === "alphabet" }, + { title: "Unread Count", actions: orderActions, selected: sortMethod === "count" }, + ] + + const Icon = map[sortMethod][sortOrder] + return ( + { + const [firstArgs, secondary] = e.nativeEvent.indexPath + + switch (firstArgs) { + case 0: { + setFeedListSortMethod("alphabet") + break + } + case 1: { + setFeedListSortMethod("count") + break + } + } + + switch (secondary) { + case 0: { + setFeedListSortOrder("asc") + break + } + case 1: { + setFeedListSortOrder("desc") + break + } + } + }} + > + + + + + ) +} diff --git a/apps/mobile/src/modules/subscription/list.tsx b/apps/mobile/src/modules/subscription/list.tsx new file mode 100644 index 0000000000..d272021abd --- /dev/null +++ b/apps/mobile/src/modules/subscription/list.tsx @@ -0,0 +1,455 @@ +import { FeedViewType } from "@follow/constants" +import { cn } from "@follow/utils" +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" +import { useHeaderHeight } from "@react-navigation/elements" +import { FlashList } from "@shopify/flash-list" +import { router } from "expo-router" +import { useAtom } from "jotai" +import { useColorScheme } from "nativewind" +import type { FC } from "react" +import { createContext, memo, useContext, useEffect, useRef } from "react" +import { + Animated, + Easing, + Image, + StyleSheet, + Text, + TouchableOpacity, + useAnimatedValue, + View, +} from "react-native" +import PagerView from "react-native-pager-view" +import { useSharedValue } from "react-native-reanimated" +import { useSafeAreaInsets } from "react-native-safe-area-context" + +import { AccordionItem } from "@/src/components/ui/accordion" +import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" +import { FeedIcon } from "@/src/components/ui/icon/feed-icon" +import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { bottomViewTabHeight } from "@/src/constants/ui" +import { InboxCuteFiIcon } from "@/src/icons/inbox_cute_fi" +import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" +import { StarCuteFiIcon } from "@/src/icons/star_cute_fi" +import { useFeed } from "@/src/store/feed/hooks" +import { useList } from "@/src/store/list/hooks" +import { + useGroupedSubscription, + useInboxSubscription, + useListSubscription, + usePrefetchSubscription, + useSortedGroupedSubscription, + useSortedListSubscription, + useSortedUngroupedSubscription, + useSubscription, +} from "@/src/store/subscription/hooks" +import { getInboxStoreId } from "@/src/store/subscription/utils" +import { useUnreadCount, useUnreadCounts } from "@/src/store/unread/hooks" + +import { + SubscriptionFeedCategoryContextMenu, + SubscriptionFeedItemContextMenu, +} from "../context-menu/feeds" +import { SubscriptionListItemContextMenu } from "../context-menu/lists" +import { useFeedListSortMethod, useFeedListSortOrder, viewAtom } from "./atoms" +import { useViewPageCurrentView, ViewPageCurrentViewProvider } from "./ctx" + +export const SubscriptionList = memo(() => { + const [currentView, setCurrentView] = useAtom(viewAtom) + + const pagerRef = useRef(null) + + useEffect(() => { + pagerRef.current?.setPage(currentView) + }, [currentView]) + + return ( + <> + + + { + setCurrentView(nativeEvent.position) + }} + scrollEnabled + style={style.flex} + initialPage={0} + ref={pagerRef} + offscreenPageLimit={3} + > + {[ + FeedViewType.Articles, + FeedViewType.SocialMedia, + FeedViewType.Pictures, + FeedViewType.Videos, + FeedViewType.Audios, + FeedViewType.Notifications, + ].map((view) => { + return ( + + + + ) + })} + + + ) +}) +const RecycleList = ({ view }: { view: FeedViewType }) => { + const headerHeight = useHeaderHeight() + const insets = useSafeAreaInsets() + const tabHeight = useBottomTabBarHeight() + + usePrefetchSubscription(view) + const { grouped, unGrouped } = useGroupedSubscription(view) + + const sortBy = useFeedListSortMethod() + const sortOrder = useFeedListSortOrder() + const sortedGrouped = useSortedGroupedSubscription(grouped, sortBy, sortOrder) + const sortedUnGrouped = useSortedUngroupedSubscription(unGrouped, sortBy, sortOrder) + const data = [...sortedGrouped, ...sortedUnGrouped] + + return ( + + ) +} + +const ItemRender = ({ + item, + index, + extraData, +}: { + item: string | { category: string; subscriptionIds: string[] } + index: number + extraData?: { + total: number + } +}) => { + if (typeof item === "string") { + return ( + + ) + } + const { category, subscriptionIds } = item + + return +} + +const ListHeaderComponent = () => { + const view = useViewPageCurrentView() + + return ( + <> + + {view === FeedViewType.Articles && } + + Feeds + + ) +} + +// This not used FlashList +// const ViewPage = memo(({ view }: { view: FeedViewType }) => { +// const { grouped, unGrouped } = useGroupedSubscription(view) +// usePrefetchSubscription(view) + +// return ( +// +// +// {view === FeedViewType.Articles && } +// +// Feeds +// +// +// +// ) +// }) + +const InboxList = () => { + const inboxes = useInboxSubscription(FeedViewType.Articles) + if (inboxes.length === 0) return null + return ( + + Inboxes + {inboxes.map((id) => { + return + })} + + ) +} + +const InboxItem = memo(({ id }: { id: string }) => { + const subscription = useSubscription(getInboxStoreId(id)) + const unreadCount = useUnreadCount(id) + const { colorScheme } = useColorScheme() + if (!subscription) return null + return ( + + + + + + {subscription.title} + {!!unreadCount && {unreadCount}} + + ) +}) + +const StarItem = () => { + return ( + { + // TODO + }} + className="mt-4 h-12 w-full flex-row items-center px-3" + > + + Collections + + ) +} + +const ListList = () => { + const currentView = useViewPageCurrentView() + const listIds = useListSubscription(currentView) + const sortedListIds = useSortedListSubscription(listIds, "alphabet") + if (sortedListIds.length === 0) return null + return ( + + Lists + {sortedListIds.map((id) => { + return + })} + + ) +} + +const ListSubscriptionItem = memo(({ id }: { id: string; className?: string }) => { + const list = useList(id) + const unreadCount = useUnreadCount(id) + if (!list) return null + return ( + + + + {!!list.image && ( + + )} + {!list.image && } + + + {list.title} + {!!unreadCount && } + + + ) +}) + +const UnGroupedList: FC<{ + subscriptionIds: string[] +}> = ({ subscriptionIds }) => { + const sortBy = useFeedListSortMethod() + const sortOrder = useFeedListSortOrder() + const sortedSubscriptionIds = useSortedUngroupedSubscription(subscriptionIds, sortBy, sortOrder) + const lastSubscriptionId = sortedSubscriptionIds.at(-1) + + return sortedSubscriptionIds.map((id) => { + return ( + + ) + }) +} + +const GroupedContext = createContext(null) + +const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) + +// const CategoryList: FC<{ +// grouped: Record +// }> = ({ grouped }) => { +// const sortedGrouped = useSortedGroupedSubscription(grouped, "alphabet") + +// return sortedGrouped.map(({ category, subscriptionIds }) => { +// return +// }) +// } + +const CategoryGrouped = memo( + ({ category, subscriptionIds }: { category: string; subscriptionIds: string[] }) => { + const unreadCounts = useUnreadCounts(subscriptionIds) + const isExpanded = useSharedValue(false) + const rotateValue = useAnimatedValue(1) + return ( + + { + // TODO navigate to category + }} + className="border-secondary-system-grouped-background border-b-hairline h-12 flex-row items-center px-3" + > + { + Animated.timing(rotateValue, { + toValue: isExpanded.value ? 1 : 0, + easing: Easing.linear, + + useNativeDriver: true, + }).start() + isExpanded.value = !isExpanded.value + }} + style={[ + { + transform: [ + { + rotate: rotateValue.interpolate({ + inputRange: [0, 1], + outputRange: ["90deg", "0deg"], + }), + }, + ], + }, + style.accordionIcon, + ]} + > + + + {category} + {!!unreadCounts && ( + {unreadCounts} + )} + + + + + + + + ) + }, +) + +// const renderRightActions = () => { +// return ( +// +// { +// // TODO: Handle unsubscribe +// }} +// > +// Unsubscribe +// +// +// ) +// } + +// const renderLeftActions = () => { +// return ( +// +// { +// // TODO: Handle unsubscribe +// }} +// > +// Read +// +// +// ) +// } + +// let prevOpenedRow: SwipeableMethods | null = null +const SubscriptionItem = memo(({ id, className }: { id: string; className?: string }) => { + const subscription = useSubscription(id) + const unreadCount = useUnreadCount(id) + const feed = useFeed(id) + const inGrouped = !!useContext(GroupedContext) + const view = useViewPageCurrentView() + // const swipeableRef: SwipeableRef = useRef(null) + + if (!subscription || !feed) return null + + return ( + // FIXME: Here leads to very serious performance issues, the frame rate of both the UI and JS threads has dropped + // { + // if (prevOpenedRow && prevOpenedRow !== swipeableRef.current) { + // prevOpenedRow.close() + // } + // prevOpenedRow = swipeableRef.current + // }} + // > + + { + router.push({ + pathname: `/feeds/[feedId]`, + params: { + feedId: id, + }, + }) + }} + > + + + + {subscription.title || feed.title} + {!!unreadCount && ( + {unreadCount} + )} + + + // + ) +}) + +const style = StyleSheet.create({ + flex: { + flex: 1, + }, + accordionIcon: { + height: 20, + width: 20, + alignItems: "center", + justifyContent: "center", + }, +}) diff --git a/apps/mobile/src/morph/db-store.ts b/apps/mobile/src/morph/db-store.ts new file mode 100644 index 0000000000..e54d126c53 --- /dev/null +++ b/apps/mobile/src/morph/db-store.ts @@ -0,0 +1,13 @@ +import type { SubscriptionSchema } from "../database/schemas/types" +import type { SubscriptionModel } from "../store/subscription/store" + +class DbStoreMorph { + toSubscriptionModel(subscription: SubscriptionSchema): SubscriptionModel { + return { + ...subscription, + isPrivate: subscription.isPrivate ? true : false, + } + } +} + +export const dbStoreMorph = new DbStoreMorph() diff --git a/apps/mobile/src/morph/hono.ts b/apps/mobile/src/morph/hono.ts new file mode 100644 index 0000000000..daba26a2dc --- /dev/null +++ b/apps/mobile/src/morph/hono.ts @@ -0,0 +1,84 @@ +import type { FeedSchema, InboxSchema } from "../database/schemas/types" +import type { ListModel } from "../store/list/store" +import type { SubscriptionModel } from "../store/subscription/store" +import type { HonoApiClient } from "./types" + +class Morph { + toSubscription(data: HonoApiClient.Subscription_Get) { + const subscriptions: SubscriptionModel[] = [] + + // TODO list inbox + const collections = { + feeds: [], + inboxes: [], + lists: [], + } as { + feeds: FeedSchema[] + inboxes: InboxSchema[] + lists: ListModel[] + } + + for (const item of data) { + const baseSubscription = { + category: item.category!, + + userId: item.userId, + view: item.view, + isPrivate: item.isPrivate, + title: item.title, + createdAt: item.createdAt, + } as SubscriptionModel + + if ("feeds" in item) { + baseSubscription.feedId = item.feedId + baseSubscription.type = "feed" + const feed = item.feeds + collections.feeds.push({ + description: feed.description!, + id: feed.id, + errorAt: feed.errorAt!, + errorMessage: feed.errorMessage!, + image: feed.image!, + ownerUserId: feed.ownerUserId!, + siteUrl: feed.siteUrl!, + title: feed.title!, + url: feed.url, + }) + } + + if ("inboxes" in item) { + baseSubscription.inboxId = item.inboxId + baseSubscription.type = "inbox" + const inbox = item.inboxes + + collections.inboxes.push({ + id: inbox.id, + title: inbox.title!, + }) + } + + if ("lists" in item) { + baseSubscription.listId = item.listId + baseSubscription.type = "list" + const list = item.lists + if (list.owner) + collections.lists.push({ + id: list.id, + title: list.title!, + userId: list.owner!.id, + description: list.description!, + view: list.view, + image: list.image!, + ownerUserId: list.owner.id, + feedIds: list.feedIds!, + fee: list.fee!, + }) + } + + subscriptions.push(baseSubscription) + } + return { subscriptions, ...collections } + } +} + +export const honoMorph = new Morph() diff --git a/apps/mobile/src/morph/store-db.ts b/apps/mobile/src/morph/store-db.ts new file mode 100644 index 0000000000..e1ab6b0f39 --- /dev/null +++ b/apps/mobile/src/morph/store-db.ts @@ -0,0 +1,28 @@ +import type { ListSchema, SubscriptionSchema } from "../database/schemas/types" +import type { ListModel } from "../store/list/store" +import type { SubscriptionModel } from "../store/subscription/store" + +class StoreDbMorph { + toListSchema(list: ListModel): ListSchema { + return { + ...list, + feedIds: JSON.stringify(list.feedIds), + } + } + toSubscriptionSchema(subscription: SubscriptionModel): SubscriptionSchema { + return { + ...subscription, + id: buildSubscriptionDbId(subscription), + isPrivate: subscription.isPrivate ? 1 : 0, + } + } +} + +export const storeDbMorph = new StoreDbMorph() + +const buildSubscriptionDbId = (subscription: SubscriptionModel) => { + if (subscription.feedId) return `${subscription.type}/${subscription.feedId}` + if (subscription.listId) return `${subscription.type}/${subscription.listId}` + if (subscription.inboxId) return `${subscription.type}/${subscription.inboxId}` + throw new Error("Invalid subscription") +} diff --git a/apps/mobile/src/morph/types.ts b/apps/mobile/src/morph/types.ts new file mode 100644 index 0000000000..7836b0b6b0 --- /dev/null +++ b/apps/mobile/src/morph/types.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import type { apiClient } from "../lib/api-fetch" + +// Add ExtractData type utility +type ExtractData any> = + Awaited> extends { data: infer D } ? D : never + +export namespace HonoApiClient { + export type Subscription_Get = ExtractData +} diff --git a/apps/mobile/src/providers/index.tsx b/apps/mobile/src/providers/index.tsx new file mode 100644 index 0000000000..d7a78aa8fb --- /dev/null +++ b/apps/mobile/src/providers/index.tsx @@ -0,0 +1,43 @@ +import { jotaiStore } from "@follow/utils" +import { ThemeProvider } from "@react-navigation/native" +import { QueryClientProvider } from "@tanstack/react-query" +import { useDrizzleStudio } from "expo-drizzle-studio-plugin" +import { Provider } from "jotai" +import { useColorScheme } from "nativewind" +import type { ReactNode } from "react" +import { StyleSheet, View } from "react-native" +import { GestureHandlerRootView } from "react-native-gesture-handler" +import { KeyboardProvider } from "react-native-keyboard-controller" + +import { sqlite } from "../database" +import { queryClient } from "../lib/query-client" +import { getCurrentColors } from "../theme/colors" +import { DarkTheme, DefaultTheme } from "../theme/navigation" +import { MigrationProvider } from "./migration" + +export const RootProviders = ({ children }: { children: ReactNode }) => { + useDrizzleStudio(sqlite) + const { colorScheme } = useColorScheme() + + const currentThemeColors = getCurrentColors()! + + return ( + + + + + + + {children} + + + + + + + ) +} + +const styles = StyleSheet.create({ + flex: { flex: 1 }, +}) diff --git a/apps/mobile/src/providers/migration.tsx b/apps/mobile/src/providers/migration.tsx new file mode 100644 index 0000000000..d620302c9b --- /dev/null +++ b/apps/mobile/src/providers/migration.tsx @@ -0,0 +1,46 @@ +import { deleteAsync } from "expo-file-system" +import type { ReactNode } from "react" +import { Button, Text, View } from "react-native" + +import { LoadingIndicator } from "../components/ui/loading" +import { getDbPath } from "../database" +import { BugCuteReIcon } from "../icons/bug_cute_re" +import { useDatabaseMigration } from "../initialize/migration" + +export const MigrationProvider = ({ children }: { children: ReactNode }) => { + const { success, error } = useDatabaseMigration() + + if (error) { + return ( + + + Oops, something went wrong... + + {error.message} + + +