diff --git a/.gitignore b/.gitignore index 7cbf1ae2..a294c2c4 100644 --- a/.gitignore +++ b/.gitignore @@ -68,9 +68,11 @@ android/keystores/debug.keystore # generated by bob lib/ +dist/ coverage/ example/android/app/google-services.json example/package-lock.json example/ios/rnfirebasechatexample/GoogleService-Info.plist example/.env *service-account.json +*.tgz diff --git a/CLAUDE.md b/CLAUDE.md index 1d35253b..469e319c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,94 +4,139 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -React Native Firebase Chat library (`rn-firebase-chat`) — an npm package providing real-time chat UI components backed by Firebase Firestore. Built with TypeScript, published via react-native-builder-bob in CommonJS, ES module, and TypeScript declaration formats. +A monorepo containing a real-time Firebase chat library for React Native and Web. The monorepo uses **pnpm workspaces** with **Turborepo** for task orchestration. + +### Packages + +| Package | npm name | Description | +|---|---|---| +| `packages/rn-firebase-chat` | `rn-firebase-chat` | React Native chat UI (gifted-chat, RN Firebase) | +| `packages/react-firebase-chat` | `@saigontechnology/react-firebase-chat` | Web chat UI (plain React, Firebase JS SDK) | +| `packages/shared` | `@saigontechnology/firebase-chat-shared` | Shared business logic, `FirestoreServices`, `useChatScreen` hook | +| `packages/storage-providers` | `@saigontechnology/chat-storage-providers` | Firebase Storage + Cloudinary adapters | + +### Apps + +| App | Description | +|---|---| +| `apps/example` | Expo + React Native example (iOS/Android + web via Expo) | +| `apps/web-vite` | Vite + React web-only example | ## Common Commands +Run from the **monorepo root** (`rn-firebase-chat/`): + +```bash +yarn lint # ESLint all packages (turbo) +yarn lint --fix # Auto-fix lint/formatting +pnpm build # Build all packages (turbo) +pnpm install # Install all deps + update lockfile +``` + +Run from **`packages/rn-firebase-chat/`**: + ```bash yarn test # Run Jest tests yarn test src/__tests__/foo.test.ts # Run a specific test file yarn test -t "test name pattern" # Run tests matching a name yarn test --watch # Watch mode -yarn lint # ESLint check -yarn lint --fix # Auto-fix lint/formatting -yarn typecheck # TypeScript type-check (main) -yarn typecheck:build # TypeScript type-check (build config) -yarn typecheck:example # TypeScript type-check (example app) +yarn typecheck # TypeScript type-check yarn build:types # Generate .d.ts declarations -yarn bootstrap # Install all deps + example pods yarn example start # Start Metro for example app yarn example ios # Run example on iOS yarn example android # Run example on Android ``` -Use `yarn` (v1 classic, v1.22.22), not npm. Node >= 16 required; see `.nvmrc` for the pinned version. +Run from **`packages/react-firebase-chat/`**: + +```bash +pnpm build # tsc + postcss → dist/ +pnpm build:css # postcss only (CSS files to dist/) +pnpm typecheck # TypeScript type-check +pnpm lint # ESLint +``` + +Use `yarn` (v1 classic, v1.22.22) for root-level commands. Use `pnpm` for package-level commands. Node >= 16 required. ## Architecture -### Entry Point & Exports (`src/index.ts`) +### Shared Package (`packages/shared/`) + +Core business logic shared between mobile and web. **No React Native or DOM imports.** -The library exports: `FirestoreServices`, screen components (`ChatProvider`, `ChatScreen`, `ListConversationScreen`, `GalleryScreen`, `ErrorBoundary`), custom hooks, and reducer actions. +- **`FirestoreServices`** — singleton managing all Firestore operations: conversations CRUD, message send/fetch (cursor-based pagination), real-time `onSnapshot` listeners, typing indicators, read receipts. Lives in `src/services/firestore.ts`. +- **Platform adapter pattern** — `FirestoreServices` receives a `FirestoreClient` interface injected at runtime. Mobile uses `rn-adapter.ts` (wraps `@react-native-firebase/firestore`); web uses `web-firestore-adapter.ts` (wraps Firebase JS SDK). This keeps the shared service free of any platform SDK import. +- **`useChatScreen`** — shared React hook (`src/ui/hooks/useChatScreen.ts`) that drives both mobile and web chat screens. Handles message history, real-time listeners, optimistic sends, load-earlier pagination, and lazy conversation creation. Receives a `ChatScreenService` interface and a platform-specific `formatMessage` callback. +- **User services** (`src/services/user.ts`) — `createUserProfile` (upsert via `{ merge: true }`), `getUserById`, `getAllUsers`. All operate via the injected `FirestoreClient`. -### State Management +### `FirestoreServices` initialization order -Uses React Context + `useReducer` (not Redux). `ChatProvider` wraps the app and provides `ChatContext` with state/dispatch. The reducer lives in `src/reducer/` with actions: `SET_LIST_CONVERSATION`, `SET_CONVERSATION`, `CLEAR_CONVERSATION`, `UPDATE_CONVERSATION`. +1. `getInstance()` — lazy singleton +2. `setFirestoreClient(client)` — inject platform adapter (**required**) +3. `configuration({ userInfo, blackListWords, prefix })` — set user; validates userId format +4. `configurationEncryption({ encryptKey, encryptionOptions })` — optional AES encryption +5. `setStorageProvider(provider)` — required only for file uploads +6. `createEncryptionsFunction({ encrypt, decrypt, getKey })` — override crypto functions -### Firebase Services (`src/services/firebase/`) +### Conversation data model -`FirestoreServices` is a **singleton** (`getInstance()`) managing all Firestore operations: conversations CRUD, message sending/fetching with cursor-based pagination, real-time listeners via `onSnapshot`, typing indicators, and read receipts. `storage.ts` handles file uploads. +- `name` (string) — **group chats only**; do not set for 1:1 conversations +- `names` (Record\) — map of `{ [userId]: ownDisplayName }`; each user writes their own entry. To display the partner's name: `names[partnerId]` +- `setConversationInfo` writes `names[currentUserId] = currentUser.name` as a fire-and-forget update every time the conversation is opened, keeping names in sync -Initialization is split across multiple methods that must be called in order: -1. `getInstance()` — lazy-loads the singleton -2. `configuration({ userId, blackListWords, prefix })` — sets user info; validates userId format -3. `configurationEncryption({ key, ... })` — optional; generates PBKDF2 key for AES encryption -4. `setStorageProvider(storage)` — required only when using file uploads -5. `createEncryptionsFunction({ encrypt, decrypt, getKey })` — override with custom crypto functions +### Lazy conversation creation (`useChatScreen`) -Decrypted messages are cached in a `Map` (`decryptCache`) to avoid redundant decryption on re-renders. +For new conversations, no Firestore document is created until the first message is sent. `conversationId` starts as `undefined`; `sendMessage` calls `service.createConversation` on first send, sets the new ID on `resolvedConversationId`, and skips the history-reload effect via `conversationCreatedInternally` ref — so the optimistic message stays visible and real-time listeners activate immediately. -### Custom Hooks (`src/hooks.ts`) +### RN package (`packages/rn-firebase-chat/`) -- `useChat()` — full context + FirestoreServices access -- `useChatContext()` — raw context access -- `useChatSelector()` — selector pattern for state slices -- `useTypingIndicator()` — typing indicator with timeout +- Thin wrapper around the shared package; re-exports `FirestoreServices` from `@saigontechnology/firebase-chat-shared` +- `ChatScreen` — GiftedChat-based UI; calls `useChatScreen` with `formatMessageData` as the `formatMessage` adapter +- `ListConversationScreen` — FlatList conversation list; dispatches `setConversation` to context on item press +- `ChatProvider` — wraps the app; initialises `FirestoreServices` (configures + injects `RNFirestoreClient`) +- Platform adapter: `src/services/firebase/rn-adapter.ts` wraps `@react-native-firebase/firestore` into `FirestoreClient` +- State: React Context + `useReducer`; actions: `SET_LIST_CONVERSATION`, `SET_CONVERSATION`, `CLEAR_CONVERSATION`, `UPDATE_CONVERSATION` -### Key Interfaces (`src/interfaces/`) +### Web package (`packages/react-firebase-chat/`) -- `MessageProps` extends GiftedChat's `IMessage` with `senderId`, `readBy`, `status`, `type` (media), `path`, `extension` -- `ConversationProps` holds `latestMessage`, `members[]`, `typing`, `unRead` -- `MessageStatus` enum: sent, received, seen, failed -- `MessageTypes` enum: text, image, voice, video +- Built with `tsc` → `dist/` (not `react-native-builder-bob`) +- `WebChatProvider` — wraps the app; configures `FirestoreServices` and injects `WebFirestoreClient` +- `ChatScreen` — web chat UI; calls shared `useChatScreen` with web-specific formatMessage +- `ChatList` — sidebar conversation list +- Platform adapter: `src/services/web-firestore-adapter.ts` wraps Firebase JS SDK into `FirestoreClient` +- CSS: per-component CSS files in `src/components/` and `src/addons/`; bundled `dist/styles.css` via postcss -### Screen Components (`src/chat/`) +### Storage providers (`packages/storage-providers/`) -- `ChatScreen` — main chat UI built on `react-native-gifted-chat`, supports custom bubbles, media, encryption, typing indicators -- `ListConversationScreen` — FlatList-based conversation list -- `GalleryScreen` — media gallery viewer -- Components in `src/chat/components/` for input toolbar, bubbles, message status, etc. +- `FirebaseStorageProvider` — uploads to Firebase Storage (shared between RN and web) +- `CloudinaryStorageProvider` — uploads to Cloudinary (web-friendly) -### Camera Addon (`src/addons/camera/`) +### Key Interfaces (`packages/shared/src/types/`) -Optional camera integration using `react-native-vision-camera` with `useCamera()` and `useEnhancedCamera()` hooks. +- `MessageProps` — `senderId`, `readBy`, `status`, `type` (media), `path`, `extension` +- `ConversationProps` — `latestMessage`, `members[]`, `names`, `typing`, `unRead` +- `MessageStatus` enum: `sent`, `received`, `seen`, `failed` +- `MessageTypes` enum: `text`, `image`, `voice`, `video` -### Utilities (`src/utilities/`) +### Security utilities (`packages/shared/src/utils/`) -- **`aesCrypto.ts`** — AES encryption with a random 16-byte IV prepended to each ciphertext. `decryptedMessageData()` falls back to plaintext if decryption fails (handles mixed encrypted/plaintext history). -- **`security.ts`** — `sanitizeUserInput()`, `validateFilePath()` (blocks `../`), `validateEncryptionKey()`, `validateMessage()`, and a `RateLimiter` class (sliding-window, default 10 attempts/60 s). Called internally by `FirestoreServices`; not exported publicly. -- `messageFormatter.ts` — message formatting with decryption; `blackList.ts` — bad-word filtering; date and color helpers. +- `sanitizeUserInput()`, `validateFilePath()` (blocks `../`), `validateEncryptionKey()`, `validateMessage()` +- `RateLimiter` — sliding-window, default 10 attempts / 60 s +- All called internally by `FirestoreServices`; not exported publicly ## Code Style -- TypeScript strict mode, target ES2022 +- TypeScript strict mode, target ES2020 (shared/web) / ES2022 (RN) - Prettier: single quotes, 2-space indent, trailing commas (es5), no tabs - Conventional commits enforced by commitlint (`fix:`, `feat:`, `refactor:`, `docs:`, `test:`, `chore:`) - Pre-commit hooks (lefthook): ESLint + TypeScript check on staged files ## Testing -Tests live in `src/__tests__/`. Firebase SDK and `react-native-aes-crypto` are mocked — the Firebase mock only implements `collection`, `doc`, `get`, `set`, `add`, `onSnapshot`. CI runs tests with `--maxWorkers=2 --coverage`. +Tests live in `packages/rn-firebase-chat/src/__tests__/`. Firebase SDK and `react-native-aes-crypto` are mocked. CI runs with `--maxWorkers=2 --coverage`. ## Build -Uses `react-native-builder-bob` — source in `src/`, output to `lib/` with targets: commonjs, module, typescript. The `prepack`/`prepare` scripts run `bob build` automatically. CI also strips source maps from the build output. +- **RN package**: `react-native-builder-bob` — source `src/`, output `lib/` (commonjs + module + typescript) +- **Web package**: `tsc --project tsconfig.build.json` → `dist/`; CSS copied by `postcss` (`build:css` script) +- **Turbo** outputs: `lib/**` and `dist/**` diff --git a/README.md b/README.md index fb6ad0df..229447bd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # rn-firebase-chat -A real-time chat library for React Native backed by Firebase Firestore, built on `react-native-gifted-chat`. +A real-time Firebase chat monorepo providing ready-to-use UI components for both **React Native** and **Web**. -## Installation +## Packages + +| Package | Description | +| -------------------------------------------------------------------------- | -------------------------------------------------------- | +| [`rn-firebase-chat`](./packages/rn-firebase-chat) | React Native chat UI backed by `@react-native-firebase` | +| [`@saigontechnology/react-firebase-chat`](./packages/react-firebase-chat) | Web chat UI backed by the Firebase JS SDK | +| [`@saigontechnology/firebase-chat-shared`](./packages/shared) | Shared business logic (FirestoreServices, useChatScreen) | +| [`@saigontechnology/chat-storage-providers`](./packages/storage-providers) | Firebase Storage + Cloudinary adapters | + +--- + +## React Native + +### Installation ```sh npm install rn-firebase-chat @@ -10,7 +23,7 @@ npm install rn-firebase-chat yarn add rn-firebase-chat ``` -### React Native peer dependencies +### Peer dependencies ```sh npm install \ @@ -21,33 +34,30 @@ npm install \ react-native-safe-area-context \ react-native-gesture-handler \ react-native-reanimated \ - react-native-worklets \ - react-native-aes-crypto \ + react-native-aes-crypto ``` Optional (file upload, camera, video): ```sh -npm install react-native-image-picker react-native-vision-camera react-native-video -# Firebase Storage is required only when using file upload -npm install @react-native-firebase/storage +npm install @react-native-firebase/storage react-native-image-picker react-native-vision-camera ``` -> If you're using Expo, follow the [Expo Configuration Guide](./README.expo.md) to configure plugins and add Firebase files for Android/iOS. - ---- +> If you're using Expo, see the [Expo Configuration Guide](./packages/rn-firebase-chat/README.expo.md). -## React Native usage - -### 1. Wrap your app with `ChatProvider` +### Usage ```tsx -import { ChatProvider } from 'rn-firebase-chat'; +import { ChatProvider } from "rn-firebase-chat"; function App() { return ( @@ -55,84 +65,141 @@ function App() { } ``` -### 2. Set up navigation - ```tsx -export const ChatNavigator = () => ( - - - - -); -``` - -### 3. Render the screens - -```tsx -import { ListConversationScreen } from 'rn-firebase-chat'; +import { ListConversationScreen } from "rn-firebase-chat"; export const ListChatScreen = () => { const handleItemPress = useCallback((data) => { - navigate('Chat', data); + navigate("Chat", data); }, []); - return ; }; ``` ```tsx -import { ChatScreen as BaseChatScreen } from 'rn-firebase-chat'; +import { ChatScreen as BaseChatScreen } from "rn-firebase-chat"; -const partner = { id: 'xyz123', name: 'Tony', avatar: 'https://example.com/tony.jpg' }; +const partner = { + id: "xyz123", + name: "Tony", + avatar: "https://example.com/tony.jpg", +}; export const ChatScreen = () => ( ); ``` -### Camera & gallery addon (optional) +--- + +## Web + +### Installation + +```sh +npm install @saigontechnology/react-firebase-chat +# or +yarn add @saigontechnology/react-firebase-chat +``` + +### Peer dependencies + +```sh +npm install firebase react react-dom +``` + +Optional (animations, toasts, auto-sizing textarea): + +```sh +npm install framer-motion react-hot-toast react-textarea-autosize +``` + +### Usage ```tsx -import { ChatScreen as BaseChatScreen } from 'rn-firebase-chat'; -import { CameraView, useCamera } from 'rn-firebase-chat/src/addons/camera'; +import { + WebChatProvider, + ChatScreen, +} from "@saigontechnology/react-firebase-chat"; +import "@saigontechnology/react-firebase-chat/styles.css"; +import { initializeFirebase } from "@saigontechnology/react-firebase-chat"; -export const ChatScreen = () => { - const { onPressCamera, onPressGallery } = useCamera(); +initializeFirebase({ apiKey: "...", projectId: "..." /* ... */ }); + +function App() { + const currentUser = { id: "abc123", name: "John Doe" }; return ( - - {({ onSend }) => } - + + + ); -}; +} ``` ---- +See [`apps/web-vite/`](./apps/web-vite/) for a working example. + +## Running the Example Apps + +- **Mobile (iOS / Android):** [Getting Started — Mobile](./apps/example/GETTING_STARTED.md) +- **Web (Vite + React):** [Getting Started — Web](./apps/web-vite/GETTING_STARTED.md) ## Features -### Reply to messages +- Real-time messaging via Firestore `onSnapshot` +- Lazy conversation creation — Firestore document created on first send +- Typing indicators with configurable timeout +- Read receipts (`sent` → `received` → `seen`) +- Reply to messages with scroll-to-original +- Edit unseen messages +- Optional AES message encryption +- File / image / video upload (Firebase Storage or Cloudinary) +- Conversation name sync — each user writes their own display name into `names[userId]` +- Bad-word filtering via configurable word list + +## Firebase Deployment -Swipe a message left to quote it as a reply. The reply context appears above the input toolbar. Tapping the quoted preview inside a bubble scrolls to the original message. +Deploy Firestore security rules and indexes from the monorepo root: -Enabled by default — no extra props required. +```bash +# Deploy both rules and indexes +firebase deploy --only firestore --project -### Edit messages +# Deploy rules only +firebase deploy --only firestore:rules --project -Long-press one of your own messages that has **not yet been seen** by the other participant to enter edit mode. An editing banner appears above the input showing the original text. Submitting updates the Firestore document and sets `isEdited: true` on the message. +# Deploy indexes only +firebase deploy --only firestore:indexes --project +``` -Enabled by default — no extra props required. +The `firebase.json` at the repo root points to: +- Rules: `packages/rn-firebase-chat/firestore.rules` +- Indexes: `packages/rn-firebase-chat/firestore.indexes.json` -> Editing is restricted to messages owned by the current user whose `status` is not `seen`. +## Publishing to npm ---- +Only `rn-firebase-chat` and `@saigontechnology/react-firebase-chat` are published. The shared and storage-providers packages are bundled inside via `bundledDependencies`. + +```bash +# Build all packages first +pnpm build + +# Publish React Native package +cd packages/rn-firebase-chat && pnpm publish + +# Publish Web package +cd packages/react-firebase-chat && pnpm publish --access public +``` + +To preview a tarball without publishing: + +```bash +cd packages/rn-firebase-chat && pnpm pack +cd packages/react-firebase-chat && pnpm pack +``` ## Contributing -See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. +See the [contributing guide](./packages/rn-firebase-chat/CONTRIBUTING.md). ## License diff --git a/apps/example/.env.example b/apps/example/.env.example new file mode 100644 index 00000000..b7ed684c --- /dev/null +++ b/apps/example/.env.example @@ -0,0 +1,15 @@ +# Copy this file to .env and fill in your Firebase web config values + +EXPO_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key +EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com +EXPO_PUBLIC_FIREBASE_PROJECT_ID=your_project_id +EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET=your_project.appspot.com +EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id +EXPO_PUBLIC_FIREBASE_APP_ID=your_app_id + +# Cloudinary (used by CloudinaryStorageProvider for file uploads) +EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloud_name +EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET=your_unsigned_preset +EXPO_PUBLIC_CLOUDINARY_FOLDER=chat +EXPO_PUBLIC_CLOUDINARY_API_KEY=your_api_key +EXPO_PUBLIC_CLOUDINARY_API_SECRET=your_api_secret diff --git a/example/.gitignore b/apps/example/.gitignore similarity index 99% rename from example/.gitignore rename to apps/example/.gitignore index 3207b8fe..96b65dd3 100644 --- a/example/.gitignore +++ b/apps/example/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # Firebase config (keep out of source control) diff --git a/apps/example/App.tsx b/apps/example/App.tsx new file mode 100644 index 00000000..50e43fd9 --- /dev/null +++ b/apps/example/App.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from "react"; +import { + ActivityIndicator, + View, + TextInput, + TouchableOpacity, + Text, + StyleSheet, + LogBox, +} from "react-native"; + +// iOS UIKit internal warning because new React Native architecture (Fabric/JSI) on iOS +LogBox.ignoreLogs(["[UIKitCore] RCTScrollViewComponentView"]); + +import { NavigationContainer } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { ChatProvider, createUserProfile } from "rn-firebase-chat"; +import { getAuth, signInAnonymously } from "@react-native-firebase/auth"; +import Constants from "expo-constants"; +import { CloudinaryStorageProvider } from "@saigontechnology/chat-storage-providers"; + +import { ListChatScreen } from "./src/screens/ListChatScreen"; +import { ChatScreen } from "./src/screens/ChatScreen"; +import type { RootStackParamList } from "./src/navigation/RouteKey"; +import RouteKey from "./src/navigation/RouteKey"; + +const Stack = createNativeStackNavigator(); + +export default function App() { + const [uid, setUid] = useState(null); + const [nameInput, setNameInput] = useState(""); + const [userInfo, setUserInfo] = useState<{ + id: string; + name: string; + avatar: string; + } | null>(null); + + useEffect(() => { + signInAnonymously(getAuth()) + .then((credential) => { + setUid(credential.user.uid); + }) + .catch((e: unknown) => console.error("Auth failed:", e)); + }, []); + + const extra = (Constants.expoConfig ?? Constants.manifest)?.extra ?? {}; + const cloudinaryProvider = new CloudinaryStorageProvider({ + cloudName: extra.cloudinaryCloudName ?? "", + uploadPreset: extra.cloudinaryUploadPreset ?? "", + folder: extra.cloudinaryFolder ?? "chat", + apiKey: extra.cloudinaryApiKey ?? "", + apiSecret: extra.cloudinaryApiSecret ?? "", + }); + + const handleJoin = async () => { + if (!uid) return; + const name = nameInput.trim() || "User " + uid.slice(0, 6); + const avatar = + "https://i.pravatar.cc/150?img=" + Math.floor(Math.random() * 70 + 1); + setUserInfo({ id: uid, name, avatar }); + await createUserProfile(uid, name, avatar); + }; + + if (!uid) { + return ( + + + + ); + } + + if (!userInfo) { + return ( + + Enter your display name + + + Join + + + ); + } + + return ( + + + + + {() => ( + + )} + + ({ title: route.params.name ?? "Chat" })} + /> + + + + ); +} + +const styles = StyleSheet.create({ + center: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 24, + }, + label: { fontSize: 16, marginBottom: 12, color: "#333" }, + input: { + width: "100%", + borderWidth: 1, + borderColor: "#ccc", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: 15, + marginBottom: 12, + }, + button: { + backgroundColor: "#007AFF", + paddingHorizontal: 32, + paddingVertical: 12, + borderRadius: 8, + }, + buttonText: { color: "#fff", fontWeight: "600", fontSize: 15 }, +}); diff --git a/apps/example/App.web.tsx b/apps/example/App.web.tsx new file mode 100644 index 00000000..546d3543 --- /dev/null +++ b/apps/example/App.web.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from 'react'; +import Constants from 'expo-constants'; +import { getAuth, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; +import '@saigontechnology/react-firebase-chat/styles.css'; +import { initializeFirebase, IUser, UserService, WebChatProvider, ChatScreen } from '@saigontechnology/react-firebase-chat'; +import { CloudinaryStorageProvider } from '@saigontechnology/chat-storage-providers'; + +// Inject Material Icons font for icon buttons +if (typeof document !== 'undefined') { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; + document.head.appendChild(link); +} + +const expoConfig = Constants.expoConfig ?? Constants.manifest; +const extra = expoConfig?.extra ?? {}; + +const firebaseConfig = { + apiKey: + extra.firebaseApiKey || + process.env.EXPO_PUBLIC_FIREBASE_API_KEY || + process.env.FIREBASE_API_KEY || + '', + authDomain: + extra.firebaseAuthDomain || + process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN || + process.env.FIREBASE_AUTH_DOMAIN || + '', + projectId: + extra.firebaseProjectId || + process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID || + process.env.FIREBASE_PROJECT_ID || + '', + storageBucket: + extra.firebaseStorageBucket || + process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET || + process.env.FIREBASE_STORAGE_BUCKET || + '', + messagingSenderId: + extra.firebaseMessagingSenderId || + process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || + process.env.FIREBASE_MESSAGING_SENDER_ID || + '', + appId: + extra.firebaseAppId || + process.env.EXPO_PUBLIC_FIREBASE_APP_ID || + process.env.FIREBASE_APP_ID || + '', +}; + +initializeFirebase(firebaseConfig); + +export default function App() { + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); + + const cloudinaryProvider = new CloudinaryStorageProvider({ + cloudName: extra.cloudinaryCloudName || process.env.EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME || '', + uploadPreset: extra.cloudinaryUploadPreset || process.env.EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET || '', + folder: extra.cloudinaryFolder || process.env.EXPO_PUBLIC_CLOUDINARY_FOLDER || 'chat', + apiKey: extra.cloudinaryApiKey || process.env.EXPO_PUBLIC_CLOUDINARY_API_KEY || '', + apiSecret: extra.cloudinaryApiSecret || process.env.EXPO_PUBLIC_CLOUDINARY_API_SECRET || '', + }); + + useEffect(() => { + const auth = getAuth(); + const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => { + if (firebaseUser) { + const userService = UserService.getInstance(); + await userService.createUserIfNotExists(firebaseUser.uid, { name: 'Anonymous' }); + setCurrentUser({ id: firebaseUser.uid, name: 'Anonymous' }); + } else { + await signInAnonymously(auth); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + if (loading) { + return ( +
+ Signing in... +
+ ); + } + + return ( + <> + +
+ {currentUser ? ( + + + + ) : null} +
+ + ); +} diff --git a/apps/example/GETTING_STARTED.md b/apps/example/GETTING_STARTED.md new file mode 100644 index 00000000..be8b640c --- /dev/null +++ b/apps/example/GETTING_STARTED.md @@ -0,0 +1,68 @@ +# Getting Started — Mobile Example App + +This guide walks through running the example app on iOS and Android. + +## Prerequisites + +- Node.js >= 16 +- pnpm (monorepo package manager) +- Xcode (iOS) / Android Studio (Android) +- A Firebase project with Firestore enabled + +## 1. Install dependencies + +From the **monorepo root**: + +```bash +pnpm install +``` + +## 2. Configure environment variables + +Copy the example env file and fill in your Firebase project values: + +```bash +cp apps/example/.env.example apps/example/.env +``` + +Edit `apps/example/.env` with your Firebase web config (API key, project ID, etc.). See `.env.example` for all available options. + +## 3. Add Firebase service files + +Download your service files from the [Firebase Console](https://console.firebase.google.com/) using the package name `com.sts.demo.chatapp` (or update `app.config.ts` to match your own), then place them at: + +| Platform | File | Path | +|---|---|---| +| iOS | `GoogleService-Info.plist` | `apps/example/ios/rnfirebasechatexample/` | +| Android | `google-services.json` | `apps/example/android/app/` | + +These paths are configured in `apps/example/app.config.ts`. + +## 4. Generate native projects + +Run Expo prebuild to generate the `ios/` and `android/` directories: + +```bash +pnpm example prebuild +``` + +## 5. Run on iOS + +Install CocoaPods dependencies, then build and launch: + +```bash +cd apps/example/ios && pod install && cd ../../.. +pnpm example ios +``` + +> If you see an xcconfig error after a fresh checkout, it means `pod install` hasn't been run yet. + +## 6. Run on Android + +No extra native setup is needed — just run: + +```bash +pnpm example android +``` + +Ensure an emulator is running or a device is connected before launching. diff --git a/example/android/.gitignore b/apps/example/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to apps/example/android/.gitignore diff --git a/example/android/app/build.gradle b/apps/example/android/app/build.gradle similarity index 100% rename from example/android/app/build.gradle rename to apps/example/android/app/build.gradle diff --git a/example/android/app/debug.keystore b/apps/example/android/app/debug.keystore similarity index 100% rename from example/android/app/debug.keystore rename to apps/example/android/app/debug.keystore diff --git a/example/android/app/proguard-rules.pro b/apps/example/android/app/proguard-rules.pro similarity index 100% rename from example/android/app/proguard-rules.pro rename to apps/example/android/app/proguard-rules.pro diff --git a/example/android/app/src/debug/AndroidManifest.xml b/apps/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to apps/example/android/app/src/debug/AndroidManifest.xml diff --git a/example/android/app/src/debugOptimized/AndroidManifest.xml b/apps/example/android/app/src/debugOptimized/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debugOptimized/AndroidManifest.xml rename to apps/example/android/app/src/debugOptimized/AndroidManifest.xml diff --git a/example/android/app/src/main/AndroidManifest.xml b/apps/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to apps/example/android/app/src/main/AndroidManifest.xml diff --git a/example/android/app/src/main/java/com/sts/demo/chatapp/MainActivity.kt b/apps/example/android/app/src/main/java/com/sts/demo/chatapp/MainActivity.kt similarity index 100% rename from example/android/app/src/main/java/com/sts/demo/chatapp/MainActivity.kt rename to apps/example/android/app/src/main/java/com/sts/demo/chatapp/MainActivity.kt diff --git a/example/android/app/src/main/java/com/sts/demo/chatapp/MainApplication.kt b/apps/example/android/app/src/main/java/com/sts/demo/chatapp/MainApplication.kt similarity index 100% rename from example/android/app/src/main/java/com/sts/demo/chatapp/MainApplication.kt rename to apps/example/android/app/src/main/java/com/sts/demo/chatapp/MainApplication.kt diff --git a/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/apps/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png similarity index 100% rename from example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png rename to apps/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png diff --git a/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/apps/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png similarity index 100% rename from example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png rename to apps/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png diff --git a/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/apps/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png similarity index 100% rename from example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png rename to apps/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png diff --git a/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/apps/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png similarity index 100% rename from example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png rename to apps/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png diff --git a/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/apps/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png similarity index 100% rename from example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png rename to apps/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png diff --git a/example/android/app/src/main/res/drawable/ic_launcher_background.xml b/apps/example/android/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/ic_launcher_background.xml rename to apps/example/android/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/apps/example/android/app/src/main/res/drawable/rn_edit_text_material.xml similarity index 100% rename from example/android/app/src/main/res/drawable/rn_edit_text_material.xml rename to apps/example/android/app/src/main/res/drawable/rn_edit_text_material.xml diff --git a/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to apps/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to apps/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to apps/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to apps/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to apps/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to apps/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to apps/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/example/android/app/src/main/res/values-night/colors.xml b/apps/example/android/app/src/main/res/values-night/colors.xml similarity index 100% rename from example/android/app/src/main/res/values-night/colors.xml rename to apps/example/android/app/src/main/res/values-night/colors.xml diff --git a/example/android/app/src/main/res/values/colors.xml b/apps/example/android/app/src/main/res/values/colors.xml similarity index 100% rename from example/android/app/src/main/res/values/colors.xml rename to apps/example/android/app/src/main/res/values/colors.xml diff --git a/example/android/app/src/main/res/values/strings.xml b/apps/example/android/app/src/main/res/values/strings.xml similarity index 100% rename from example/android/app/src/main/res/values/strings.xml rename to apps/example/android/app/src/main/res/values/strings.xml diff --git a/example/android/app/src/main/res/values/styles.xml b/apps/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to apps/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/build.gradle b/apps/example/android/build.gradle similarity index 100% rename from example/android/build.gradle rename to apps/example/android/build.gradle diff --git a/example/android/gradle.properties b/apps/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to apps/example/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/apps/example/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.jar rename to apps/example/android/gradle/wrapper/gradle-wrapper.jar diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/apps/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to apps/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/gradlew b/apps/example/android/gradlew similarity index 100% rename from example/android/gradlew rename to apps/example/android/gradlew diff --git a/example/android/gradlew.bat b/apps/example/android/gradlew.bat similarity index 100% rename from example/android/gradlew.bat rename to apps/example/android/gradlew.bat diff --git a/example/android/settings.gradle b/apps/example/android/settings.gradle similarity index 100% rename from example/android/settings.gradle rename to apps/example/android/settings.gradle diff --git a/apps/example/app.config.ts b/apps/example/app.config.ts new file mode 100644 index 00000000..b7ebe569 --- /dev/null +++ b/apps/example/app.config.ts @@ -0,0 +1,71 @@ +import { ExpoConfig } from 'expo/config'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const config: ExpoConfig = { + name: 'rn-firebase-chat-example', + slug: 'rn-firebase-chat-example', + version: '1.0.0', + orientation: 'portrait', + icon: './assets/icon.png', + userInterfaceStyle: 'light', + splash: { + image: './assets/splash-icon.png', + resizeMode: 'contain', + backgroundColor: '#ffffff', + }, + ios: { + supportsTablet: true, + bundleIdentifier: 'com.sts.demo.chatapp', + googleServicesFile: './ios/rnfirebasechatexample/GoogleService-Info.plist', + }, + android: { + package: 'com.sts.demo.chatapp', + googleServicesFile: './android/app/google-services.json', + adaptiveIcon: { + foregroundImage: './assets/android-icon-foreground.png', + backgroundImage: './assets/android-icon-background.png', + monochromeImage: './assets/android-icon-monochrome.png', + backgroundColor: '#E6F4FE', + }, + }, + extra: { + firebaseApiKey: + process.env.FIREBASE_API_KEY ?? process.env.EXPO_PUBLIC_FIREBASE_API_KEY ?? '', + firebaseAuthDomain: + process.env.FIREBASE_AUTH_DOMAIN ?? process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN ?? '', + firebaseProjectId: + process.env.FIREBASE_PROJECT_ID ?? process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID ?? '', + firebaseStorageBucket: + process.env.FIREBASE_STORAGE_BUCKET ?? process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET ?? '', + firebaseMessagingSenderId: + process.env.FIREBASE_MESSAGING_SENDER_ID ?? process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ?? '', + firebaseAppId: + process.env.FIREBASE_APP_ID ?? process.env.EXPO_PUBLIC_FIREBASE_APP_ID ?? '', + cloudinaryCloudName: + process.env.CLOUDINARY_CLOUD_NAME ?? process.env.EXPO_PUBLIC_CLOUDINARY_CLOUD_NAME ?? '', + cloudinaryUploadPreset: + process.env.CLOUDINARY_UPLOAD_PRESET ?? process.env.EXPO_PUBLIC_CLOUDINARY_UPLOAD_PRESET ?? '', + cloudinaryFolder: + process.env.CLOUDINARY_FOLDER ?? process.env.EXPO_PUBLIC_CLOUDINARY_FOLDER ?? 'chat', + cloudinaryApiKey: + process.env.CLOUDINARY_API_KEY ?? process.env.EXPO_PUBLIC_CLOUDINARY_API_KEY ?? '', + cloudinaryApiSecret: + process.env.CLOUDINARY_API_SECRET ?? process.env.EXPO_PUBLIC_CLOUDINARY_API_SECRET ?? '', + }, + plugins: [ + '@react-native-firebase/app', + [ + 'expo-build-properties', + { + ios: { + useFrameworks: 'static', + buildReactNativeFromSource: true, + }, + }, + ], + ], +}; + +export default config; diff --git a/example/app.json b/apps/example/app.json similarity index 100% rename from example/app.json rename to apps/example/app.json diff --git a/example/assets/android-icon-background.png b/apps/example/assets/android-icon-background.png similarity index 100% rename from example/assets/android-icon-background.png rename to apps/example/assets/android-icon-background.png diff --git a/example/assets/android-icon-foreground.png b/apps/example/assets/android-icon-foreground.png similarity index 100% rename from example/assets/android-icon-foreground.png rename to apps/example/assets/android-icon-foreground.png diff --git a/example/assets/android-icon-monochrome.png b/apps/example/assets/android-icon-monochrome.png similarity index 100% rename from example/assets/android-icon-monochrome.png rename to apps/example/assets/android-icon-monochrome.png diff --git a/example/assets/favicon.png b/apps/example/assets/favicon.png similarity index 100% rename from example/assets/favicon.png rename to apps/example/assets/favicon.png diff --git a/example/assets/icon.png b/apps/example/assets/icon.png similarity index 100% rename from example/assets/icon.png rename to apps/example/assets/icon.png diff --git a/example/assets/splash-icon.png b/apps/example/assets/splash-icon.png similarity index 100% rename from example/assets/splash-icon.png rename to apps/example/assets/splash-icon.png diff --git a/example/index.ts b/apps/example/index.ts similarity index 100% rename from example/index.ts rename to apps/example/index.ts diff --git a/example/ios/.gitignore b/apps/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to apps/example/ios/.gitignore diff --git a/example/ios/.xcode.env b/apps/example/ios/.xcode.env similarity index 100% rename from example/ios/.xcode.env rename to apps/example/ios/.xcode.env diff --git a/example/ios/Podfile b/apps/example/ios/Podfile similarity index 100% rename from example/ios/Podfile rename to apps/example/ios/Podfile diff --git a/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock similarity index 88% rename from example/ios/Podfile.lock rename to apps/example/ios/Podfile.lock index 3651b53f..d74a3634 100644 --- a/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -3348,6 +3348,34 @@ PODS: - SocketRocket - react-native-aes (3.3.0): - React-Core + - react-native-image-picker (8.2.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - react-native-keyboard-controller (1.21.4): - boost - DoubleConversion @@ -4442,106 +4470,107 @@ PODS: - Yoga (0.0.0) DEPENDENCIES: - - boost (from `../../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - EXConstants (from `../../node_modules/expo-constants/ios`) - - Expo (from `../../node_modules/expo`) - - ExpoAsset (from `../../node_modules/expo-asset/ios`) - - "ExpoDomWebView (from `../../node_modules/@expo/dom-webview/ios`)" - - ExpoFileSystem (from `../../node_modules/expo-file-system/ios`) - - ExpoFont (from `../../node_modules/expo-font/ios`) - - ExpoKeepAwake (from `../../node_modules/expo-keep-awake/ios`) - - "ExpoLogBox (from `../../node_modules/@expo/log-box`)" - - ExpoModulesCore (from `../../node_modules/expo-modules-core`) - - ExpoModulesJSI (from `../../node_modules/expo-modules-core`) - - fast_float (from `../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - - FBLazyVector (from `../../node_modules/react-native/Libraries/FBLazyVector`) - - fmt (from `../../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`) - - hermes-engine (from `../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - RCT-Folly (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTDeprecation (from `../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - - RCTRequired (from `../../node_modules/react-native/Libraries/Required`) - - RCTSwiftUI (from `../../node_modules/react-native/ReactApple/RCTSwiftUI`) - - RCTSwiftUIWrapper (from `../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) - - RCTTypeSafety (from `../../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../../node_modules/react-native/`) - - React-callinvoker (from `../../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../../node_modules/react-native/`) - - React-CoreModules (from `../../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../../node_modules/react-native/ReactCommon/react/debug`) - - React-defaultsnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) - - React-domnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - - React-Fabric (from `../../node_modules/react-native/ReactCommon`) - - React-FabricComponents (from `../../node_modules/react-native/ReactCommon`) - - React-FabricImage (from `../../node_modules/react-native/ReactCommon`) - - React-featureflags (from `../../node_modules/react-native/ReactCommon/react/featureflags`) - - React-featureflagsnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - - React-graphics (from `../../node_modules/react-native/ReactCommon/react/renderer/graphics`) - - React-hermes (from `../../node_modules/react-native/ReactCommon/hermes`) - - React-idlecallbacksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - - React-ImageManager (from `../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-intersectionobservernativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) - - React-jserrorhandler (from `../../node_modules/react-native/ReactCommon/jserrorhandler`) - - React-jsi (from `../../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsinspectorcdp (from `../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) - - React-jsinspectornetwork (from `../../node_modules/react-native/ReactCommon/jsinspector-modern/network`) - - React-jsinspectortracing (from `../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) - - React-jsitooling (from `../../node_modules/react-native/ReactCommon/jsitooling`) - - React-jsitracing (from `../../node_modules/react-native/ReactCommon/hermes/executor/`) - - React-logger (from `../../node_modules/react-native/ReactCommon/logger`) - - React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`) - - React-microtasksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - react-native-aes (from `../../node_modules/react-native-aes-crypto`) - - react-native-keyboard-controller (from `../../node_modules/react-native-keyboard-controller`) - - react-native-safe-area-context (from `../../node_modules/react-native-safe-area-context`) - - React-NativeModulesApple (from `../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-networking (from `../../node_modules/react-native/ReactCommon/react/networking`) - - React-oscompat (from `../../node_modules/react-native/ReactCommon/oscompat`) - - React-perflogger (from `../../node_modules/react-native/ReactCommon/reactperflogger`) - - React-performancecdpmetrics (from `../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) - - React-performancetimeline (from `../../node_modules/react-native/ReactCommon/react/performance/timeline`) - - React-RCTActionSheet (from `../../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../../node_modules/react-native/Libraries/Blob`) - - React-RCTFabric (from `../../node_modules/react-native/React`) - - React-RCTFBReactNativeSpec (from `../../node_modules/react-native/React`) - - React-RCTImage (from `../../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../../node_modules/react-native/Libraries/Network`) - - React-RCTRuntime (from `../../node_modules/react-native/React/Runtime`) - - React-RCTSettings (from `../../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../../node_modules/react-native/Libraries/Vibration`) - - React-rendererconsistency (from `../../node_modules/react-native/ReactCommon/react/renderer/consistency`) - - React-renderercss (from `../../node_modules/react-native/ReactCommon/react/renderer/css`) - - React-rendererdebug (from `../../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-RuntimeApple (from `../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimeexecutor (from `../../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimescheduler (from `../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - - React-timing (from `../../node_modules/react-native/ReactCommon/react/timing`) - - React-utils (from `../../node_modules/react-native/ReactCommon/react/utils`) - - React-webperformancenativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXConstants (from `../../../node_modules/expo-constants/ios`) + - Expo (from `../../../node_modules/expo`) + - ExpoAsset (from `../../../node_modules/expo-asset/ios`) + - "ExpoDomWebView (from `../../../node_modules/@expo/dom-webview/ios`)" + - ExpoFileSystem (from `../../../node_modules/expo-file-system/ios`) + - ExpoFont (from `../../../node_modules/expo-font/ios`) + - ExpoKeepAwake (from `../../../node_modules/expo-keep-awake/ios`) + - "ExpoLogBox (from `../../../node_modules/@expo/log-box`)" + - ExpoModulesCore (from `../../../node_modules/expo-modules-core`) + - ExpoModulesJSI (from `../../../node_modules/expo-modules-core`) + - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../../../node_modules/react-native/Libraries/Required`) + - RCTSwiftUI (from `../../../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) + - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../../../node_modules/react-native/`) + - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../../../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`) + - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../../../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../../../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../../../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../../../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../../../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-intersectionobservernativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) + - React-jserrorhandler (from `../../../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-aes (from `../../../node_modules/react-native-aes-crypto`) + - react-native-image-picker (from `../../../node_modules/react-native-image-picker`) + - react-native-keyboard-controller (from `../../../node_modules/react-native-keyboard-controller`) + - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) + - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../../../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../../../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../../../node_modules/react-native/React`) + - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../../../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../../../node_modules/react-native/ReactCommon/react/utils`) + - React-webperformancenativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) - ReactCodegen (from `build/generated/ios/ReactCodegen`) - - ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`) - - "RNFBApp (from `../../node_modules/@react-native-firebase/app`)" - - "RNFBAuth (from `../../node_modules/@react-native-firebase/auth`)" - - "RNFBFirestore (from `../../node_modules/@react-native-firebase/firestore`)" - - "RNFBStorage (from `../../node_modules/@react-native-firebase/storage`)" - - RNGestureHandler (from `../../node_modules/react-native-gesture-handler`) - - RNReanimated (from `../../node_modules/react-native-reanimated`) - - RNScreens (from `../../node_modules/react-native-screens`) - - RNWorklets (from `../../node_modules/react-native-worklets`) + - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) + - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" + - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" + - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" + - "RNFBStorage (from `../../../node_modules/@react-native-firebase/storage`)" + - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../../../node_modules/react-native-reanimated`) + - RNScreens (from `../../../node_modules/react-native-screens`) + - RNWorklets (from `../../../node_modules/react-native-worklets`) - SocketRocket (~> 0.7.1) - - Yoga (from `../../node_modules/react-native/ReactCommon/yoga`) + - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: @@ -4569,202 +4598,204 @@ SPEC REPOS: EXTERNAL SOURCES: boost: - :podspec: "../../node_modules/react-native/third-party-podspecs/boost.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: - :podspec: "../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXConstants: - :path: "../../node_modules/expo-constants/ios" + :path: "../../../node_modules/expo-constants/ios" Expo: - :path: "../../node_modules/expo" + :path: "../../../node_modules/expo" ExpoAsset: - :path: "../../node_modules/expo-asset/ios" + :path: "../../../node_modules/expo-asset/ios" ExpoDomWebView: - :path: "../../node_modules/@expo/dom-webview/ios" + :path: "../../../node_modules/@expo/dom-webview/ios" ExpoFileSystem: - :path: "../../node_modules/expo-file-system/ios" + :path: "../../../node_modules/expo-file-system/ios" ExpoFont: - :path: "../../node_modules/expo-font/ios" + :path: "../../../node_modules/expo-font/ios" ExpoKeepAwake: - :path: "../../node_modules/expo-keep-awake/ios" + :path: "../../../node_modules/expo-keep-awake/ios" ExpoLogBox: - :path: "../../node_modules/@expo/log-box" + :path: "../../../node_modules/@expo/log-box" ExpoModulesCore: - :path: "../../node_modules/expo-modules-core" + :path: "../../../node_modules/expo-modules-core" ExpoModulesJSI: - :path: "../../node_modules/expo-modules-core" + :path: "../../../node_modules/expo-modules-core" fast_float: - :podspec: "../../node_modules/react-native/third-party-podspecs/fast_float.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: - :path: "../../node_modules/react-native/Libraries/FBLazyVector" + :path: "../../../node_modules/react-native/Libraries/FBLazyVector" fmt: - :podspec: "../../node_modules/react-native/third-party-podspecs/fmt.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/fmt.podspec" glog: - :podspec: "../../node_modules/react-native/third-party-podspecs/glog.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: - :podspec: "../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-v0.14.1 RCT-Folly: - :podspec: "../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: - :path: "../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + :path: "../../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: - :path: "../../node_modules/react-native/Libraries/Required" + :path: "../../../node_modules/react-native/Libraries/Required" RCTSwiftUI: - :path: "../../node_modules/react-native/ReactApple/RCTSwiftUI" + :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUI" RCTSwiftUIWrapper: - :path: "../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" + :path: "../../../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" RCTTypeSafety: - :path: "../../node_modules/react-native/Libraries/TypeSafety" + :path: "../../../node_modules/react-native/Libraries/TypeSafety" React: - :path: "../../node_modules/react-native/" + :path: "../../../node_modules/react-native/" React-callinvoker: - :path: "../../node_modules/react-native/ReactCommon/callinvoker" + :path: "../../../node_modules/react-native/ReactCommon/callinvoker" React-Core: - :path: "../../node_modules/react-native/" + :path: "../../../node_modules/react-native/" React-CoreModules: - :path: "../../node_modules/react-native/React/CoreModules" + :path: "../../../node_modules/react-native/React/CoreModules" React-cxxreact: - :path: "../../node_modules/react-native/ReactCommon/cxxreact" + :path: "../../../node_modules/react-native/ReactCommon/cxxreact" React-debug: - :path: "../../node_modules/react-native/ReactCommon/react/debug" + :path: "../../../node_modules/react-native/ReactCommon/react/debug" React-defaultsnativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" React-domnativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/dom" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/dom" React-Fabric: - :path: "../../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-FabricComponents: - :path: "../../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-FabricImage: - :path: "../../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-featureflags: - :path: "../../node_modules/react-native/ReactCommon/react/featureflags" + :path: "../../../node_modules/react-native/ReactCommon/react/featureflags" React-featureflagsnativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" React-graphics: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/graphics" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/graphics" React-hermes: - :path: "../../node_modules/react-native/ReactCommon/hermes" + :path: "../../../node_modules/react-native/ReactCommon/hermes" React-idlecallbacksnativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" React-ImageManager: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" React-intersectionobservernativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" React-jserrorhandler: - :path: "../../node_modules/react-native/ReactCommon/jserrorhandler" + :path: "../../../node_modules/react-native/ReactCommon/jserrorhandler" React-jsi: - :path: "../../node_modules/react-native/ReactCommon/jsi" + :path: "../../../node_modules/react-native/ReactCommon/jsi" React-jsiexecutor: - :path: "../../node_modules/react-native/ReactCommon/jsiexecutor" + :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: - :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" React-jsinspectorcdp: - :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" React-jsinspectornetwork: - :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern/network" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/network" React-jsinspectortracing: - :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" React-jsitooling: - :path: "../../node_modules/react-native/ReactCommon/jsitooling" + :path: "../../../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: - :path: "../../node_modules/react-native/ReactCommon/hermes/executor/" + :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: - :path: "../../node_modules/react-native/ReactCommon/logger" + :path: "../../../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: - :path: "../../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" React-microtasksnativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" react-native-aes: - :path: "../../node_modules/react-native-aes-crypto" + :path: "../../../node_modules/react-native-aes-crypto" + react-native-image-picker: + :path: "../../../node_modules/react-native-image-picker" react-native-keyboard-controller: - :path: "../../node_modules/react-native-keyboard-controller" + :path: "../../../node_modules/react-native-keyboard-controller" react-native-safe-area-context: - :path: "../../node_modules/react-native-safe-area-context" + :path: "../../../node_modules/react-native-safe-area-context" React-NativeModulesApple: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-networking: - :path: "../../node_modules/react-native/ReactCommon/react/networking" + :path: "../../../node_modules/react-native/ReactCommon/react/networking" React-oscompat: - :path: "../../node_modules/react-native/ReactCommon/oscompat" + :path: "../../../node_modules/react-native/ReactCommon/oscompat" React-perflogger: - :path: "../../node_modules/react-native/ReactCommon/reactperflogger" + :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" React-performancecdpmetrics: - :path: "../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + :path: "../../../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" React-performancetimeline: - :path: "../../node_modules/react-native/ReactCommon/react/performance/timeline" + :path: "../../../node_modules/react-native/ReactCommon/react/performance/timeline" React-RCTActionSheet: - :path: "../../node_modules/react-native/Libraries/ActionSheetIOS" + :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: - :path: "../../node_modules/react-native/Libraries/NativeAnimation" + :path: "../../../node_modules/react-native/Libraries/NativeAnimation" React-RCTAppDelegate: - :path: "../../node_modules/react-native/Libraries/AppDelegate" + :path: "../../../node_modules/react-native/Libraries/AppDelegate" React-RCTBlob: - :path: "../../node_modules/react-native/Libraries/Blob" + :path: "../../../node_modules/react-native/Libraries/Blob" React-RCTFabric: - :path: "../../node_modules/react-native/React" + :path: "../../../node_modules/react-native/React" React-RCTFBReactNativeSpec: - :path: "../../node_modules/react-native/React" + :path: "../../../node_modules/react-native/React" React-RCTImage: - :path: "../../node_modules/react-native/Libraries/Image" + :path: "../../../node_modules/react-native/Libraries/Image" React-RCTLinking: - :path: "../../node_modules/react-native/Libraries/LinkingIOS" + :path: "../../../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: - :path: "../../node_modules/react-native/Libraries/Network" + :path: "../../../node_modules/react-native/Libraries/Network" React-RCTRuntime: - :path: "../../node_modules/react-native/React/Runtime" + :path: "../../../node_modules/react-native/React/Runtime" React-RCTSettings: - :path: "../../node_modules/react-native/Libraries/Settings" + :path: "../../../node_modules/react-native/Libraries/Settings" React-RCTText: - :path: "../../node_modules/react-native/Libraries/Text" + :path: "../../../node_modules/react-native/Libraries/Text" React-RCTVibration: - :path: "../../node_modules/react-native/Libraries/Vibration" + :path: "../../../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/consistency" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" React-renderercss: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/css" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/debug" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" React-RuntimeApple: - :path: "../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" React-RuntimeCore: - :path: "../../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: - :path: "../../node_modules/react-native/ReactCommon/runtimeexecutor" + :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor" React-RuntimeHermes: - :path: "../../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: - :path: "../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" React-timing: - :path: "../../node_modules/react-native/ReactCommon/react/timing" + :path: "../../../node_modules/react-native/ReactCommon/react/timing" React-utils: - :path: "../../node_modules/react-native/ReactCommon/react/utils" + :path: "../../../node_modules/react-native/ReactCommon/react/utils" React-webperformancenativemodule: - :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" ReactAppDependencyProvider: :path: build/generated/ios/ReactAppDependencyProvider ReactCodegen: :path: build/generated/ios/ReactCodegen ReactCommon: - :path: "../../node_modules/react-native/ReactCommon" + :path: "../../../node_modules/react-native/ReactCommon" RNFBApp: - :path: "../../node_modules/@react-native-firebase/app" + :path: "../../../node_modules/@react-native-firebase/app" RNFBAuth: - :path: "../../node_modules/@react-native-firebase/auth" + :path: "../../../node_modules/@react-native-firebase/auth" RNFBFirestore: - :path: "../../node_modules/@react-native-firebase/firestore" + :path: "../../../node_modules/@react-native-firebase/firestore" RNFBStorage: - :path: "../../node_modules/@react-native-firebase/storage" + :path: "../../../node_modules/@react-native-firebase/storage" RNGestureHandler: - :path: "../../node_modules/react-native-gesture-handler" + :path: "../../../node_modules/react-native-gesture-handler" RNReanimated: - :path: "../../node_modules/react-native-reanimated" + :path: "../../../node_modules/react-native-reanimated" RNScreens: - :path: "../../node_modules/react-native-screens" + :path: "../../../node_modules/react-native-screens" RNWorklets: - :path: "../../node_modules/react-native-worklets" + :path: "../../../node_modules/react-native-worklets" Yoga: - :path: "../../node_modules/react-native/ReactCommon/yoga" + :path: "../../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: abseil: a05cc83bf02079535e17169a73c5be5ba47f714b @@ -4779,7 +4810,7 @@ SPEC CHECKSUMS: ExpoFont: cdd7a1d574a376fa003c713eff49e0a4df8672c7 ExpoKeepAwake: a1baf9810a2dee1905aa9bdd336852598f7220e9 ExpoLogBox: a3de999775d423ac9cb85d24bd47628e5392761f - ExpoModulesCore: 3666b65cb51435da09312089bbf78c47c9392d80 + ExpoModulesCore: 8e82f35e04c2d7e2bf2d48b90b7b29b29e8573c7 ExpoModulesJSI: 35c08bc40ba154ccc65af926c8e6629f33a67aad fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 82d1d7996af4c5850242966eb81e73f9a6dfab1e @@ -4800,7 +4831,7 @@ SPEC CHECKSUMS: "gRPC-C++": cc207623316fb041a7a3e774c252cf68a058b9e8 gRPC-Core: 860978b7db482de8b4f5e10677216309b5ff6330 GTMSessionFetcher: 904bdd2a82c635bcd6f44edf94cc8775c5d1d6e6 - hermes-engine: 071ba753d06e386df4a793173223aaae8680276d + hermes-engine: d657d910587705fddf015d3e42989a1b1260f933 leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 @@ -4840,6 +4871,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 0b0d3c3074187e72d8a6e8cacce120cb24581565 React-microtasksnativemodule: 175741856a8f6a31e20b973cb784db023256b259 react-native-aes: 0cad0a1f0087171b4af762c39d7199716d9335b5 + react-native-image-picker: 6051cfd030121b880a58f1cc0e5e33e9887804e4 react-native-keyboard-controller: 78f861c4dc73887b4cb7173a37e01f1634c9a58a react-native-safe-area-context: eda63a662750758c1fdd7e719c9f1026c8d161cb React-NativeModulesApple: 5f9aa5b3964c22ea10a13fbefc5b0e91285882b9 @@ -4873,7 +4905,7 @@ SPEC CHECKSUMS: React-utils: 7e1534af16a2bd9ff8b0a3caf83b68696d3c913f React-webperformancenativemodule: f61042f7803aedfe91bc688d5e3bb4323d7feb77 ReactAppDependencyProvider: 2b19d66e5ddfe8dc7afb6338a4626156cbf2bab1 - ReactCodegen: 746303d8945bb5a478b3bdcd137dcef5c699795a + ReactCodegen: ae1f4647ef0975469651d22c020d61f57247d3a4 ReactCommon: cbb4e1e2bff77892671eae8eb52154dcebf02280 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba RNFBApp: 66e966d59c12c1cad8822b69b4ac1be42e7c4964 @@ -4881,9 +4913,9 @@ SPEC CHECKSUMS: RNFBFirestore: eca96f2a3a5fee0ead6b219759bb1603b1e91b9b RNFBStorage: 87409821b59634af126889cee5354ace095b14ae RNGestureHandler: 4e8e22e958ec45421be56f02b6d7f76d08d4b210 - RNReanimated: 063794c72d090314d4db76b7da6a7bc97eff91fa + RNReanimated: 08c7972d203ed0f202adc163c54fa4dd9120206b RNScreens: 199799bdab32fa1e17ebf938b06fec52033e81e5 - RNWorklets: ef3337f995b99b85274778d393bfb2eb3fbb7872 + RNWorklets: 6842755a1353955d55ac03778ac42f334616eafa SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: c7fcecd3a5864321db161a74cf2cd735f800c119 diff --git a/example/ios/Podfile.properties.json b/apps/example/ios/Podfile.properties.json similarity index 100% rename from example/ios/Podfile.properties.json rename to apps/example/ios/Podfile.properties.json diff --git a/example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj b/apps/example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj similarity index 99% rename from example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj rename to apps/example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj index 69628987..22acf83f 100644 --- a/example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj +++ b/apps/example/ios/rnfirebasechatexample.xcodeproj/project.pbxproj @@ -332,6 +332,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( @@ -358,6 +359,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/leveldb_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -527,7 +529,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SWIFT_ENABLE_EXPLICIT_MODULES = NO; @@ -609,7 +611,7 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ENABLE_EXPLICIT_MODULES = NO; USE_HERMES = true; diff --git a/example/ios/rnfirebasechatexample.xcodeproj/xcshareddata/xcschemes/rnfirebasechatexample.xcscheme b/apps/example/ios/rnfirebasechatexample.xcodeproj/xcshareddata/xcschemes/rnfirebasechatexample.xcscheme similarity index 100% rename from example/ios/rnfirebasechatexample.xcodeproj/xcshareddata/xcschemes/rnfirebasechatexample.xcscheme rename to apps/example/ios/rnfirebasechatexample.xcodeproj/xcshareddata/xcschemes/rnfirebasechatexample.xcscheme diff --git a/example/ios/rnfirebasechatexample.xcworkspace/contents.xcworkspacedata b/apps/example/ios/rnfirebasechatexample.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/ios/rnfirebasechatexample.xcworkspace/contents.xcworkspacedata rename to apps/example/ios/rnfirebasechatexample.xcworkspace/contents.xcworkspacedata diff --git a/example/ios/rnfirebasechatexample/AppDelegate.swift b/apps/example/ios/rnfirebasechatexample/AppDelegate.swift similarity index 100% rename from example/ios/rnfirebasechatexample/AppDelegate.swift rename to apps/example/ios/rnfirebasechatexample/AppDelegate.swift diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/apps/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/Contents.json rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/Contents.json b/apps/example/ios/rnfirebasechatexample/Images.xcassets/Contents.json similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/Contents.json rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/Contents.json diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json b/apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image.png b/apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image.png similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image.png rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image.png diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png b/apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png diff --git a/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png b/apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png similarity index 100% rename from example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png rename to apps/example/ios/rnfirebasechatexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png diff --git a/apps/example/ios/rnfirebasechatexample/Info.plist b/apps/example/ios/rnfirebasechatexample/Info.plist new file mode 100644 index 00000000..5f074bfc --- /dev/null +++ b/apps/example/ios/rnfirebasechatexample/Info.plist @@ -0,0 +1,76 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + rn-firebase-chat-example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + com.sts.demo.chatapp + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + 12.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + RCTNewArchEnabled + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/example/ios/rnfirebasechatexample/PrivacyInfo.xcprivacy b/apps/example/ios/rnfirebasechatexample/PrivacyInfo.xcprivacy similarity index 100% rename from example/ios/rnfirebasechatexample/PrivacyInfo.xcprivacy rename to apps/example/ios/rnfirebasechatexample/PrivacyInfo.xcprivacy diff --git a/example/ios/rnfirebasechatexample/SplashScreen.storyboard b/apps/example/ios/rnfirebasechatexample/SplashScreen.storyboard similarity index 100% rename from example/ios/rnfirebasechatexample/SplashScreen.storyboard rename to apps/example/ios/rnfirebasechatexample/SplashScreen.storyboard diff --git a/example/ios/rnfirebasechatexample/Supporting/Expo.plist b/apps/example/ios/rnfirebasechatexample/Supporting/Expo.plist similarity index 100% rename from example/ios/rnfirebasechatexample/Supporting/Expo.plist rename to apps/example/ios/rnfirebasechatexample/Supporting/Expo.plist diff --git a/example/ios/rnfirebasechatexample/rnfirebasechatexample-Bridging-Header.h b/apps/example/ios/rnfirebasechatexample/rnfirebasechatexample-Bridging-Header.h similarity index 100% rename from example/ios/rnfirebasechatexample/rnfirebasechatexample-Bridging-Header.h rename to apps/example/ios/rnfirebasechatexample/rnfirebasechatexample-Bridging-Header.h diff --git a/example/ios/rnfirebasechatexample/rnfirebasechatexample.entitlements b/apps/example/ios/rnfirebasechatexample/rnfirebasechatexample.entitlements similarity index 100% rename from example/ios/rnfirebasechatexample/rnfirebasechatexample.entitlements rename to apps/example/ios/rnfirebasechatexample/rnfirebasechatexample.entitlements diff --git a/example/metro.config.js b/apps/example/metro.config.js similarity index 93% rename from example/metro.config.js rename to apps/example/metro.config.js index 516e0857..d38fdbfe 100644 --- a/example/metro.config.js +++ b/apps/example/metro.config.js @@ -2,7 +2,7 @@ const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); const projectRoot = __dirname; -const monorepoRoot = path.resolve(projectRoot, '..'); +const monorepoRoot = path.resolve(projectRoot, '../..'); const libraryRoot = path.resolve(monorepoRoot, 'packages', 'rn-firebase-chat'); const sharedRoot = path.resolve(monorepoRoot, 'packages', 'shared'); diff --git a/example/package.json b/apps/example/package.json similarity index 54% rename from example/package.json rename to apps/example/package.json index 0c1b2dec..34a59cbc 100644 --- a/example/package.json +++ b/apps/example/package.json @@ -4,9 +4,11 @@ "main": "index.ts", "scripts": { "start": "expo start", - "android": "expo run:android --active-arch-only", + "prebuild": "expo prebuild", + "android": "expo run:android", "ios": "expo run:ios", - "web": "expo start --web" + "web": "expo start --web", + "build:web": "expo build:web" }, "dependencies": { "@react-native-firebase/app": "^23.8.8", @@ -15,19 +17,31 @@ "@react-native-firebase/storage": "^23.8.8", "@react-navigation/native": "^7.2.0", "@react-navigation/native-stack": "^7.14.8", + "clsx": "^2.1.1", + "date-fns": "^2.30.0", + "framer-motion": "^11.18.2", + "react-hot-toast": "^2.4.1", + "react-textarea-autosize": "^8.5.9", "expo": "~55.0.12", "expo-build-properties": "^55.0.12", "expo-status-bar": "~55.0.4", - "randomcolor": "^0.6.2", + "firebase": "^10.14.1", "react": "19.2.0", + "react-dom": "19.2.0", "react-native": "0.83.4", "react-native-aes-crypto": "^3.3.0", + "react-native-image-picker": "^8.2.1", "react-native-screens": "^4.23.0", - "rn-firebase-chat": "workspace:*" + "react-native-web": "~0.19.13", + "rn-firebase-chat": "workspace:*", + "@saigontechnology/chat-storage-providers": "workspace:*", + "@saigontechnology/react-firebase-chat": "workspace:*" }, "devDependencies": { "@types/react": "~19.2.2", + "@types/react-dom": "~19.2.2", + "dotenv": "^16.1.4", "typescript": "~5.9.2" }, "private": true -} \ No newline at end of file +} diff --git a/example/src/navigation/RouteKey.ts b/apps/example/src/navigation/RouteKey.ts similarity index 100% rename from example/src/navigation/RouteKey.ts rename to apps/example/src/navigation/RouteKey.ts diff --git a/apps/example/src/screens/ChatScreen.tsx b/apps/example/src/screens/ChatScreen.tsx new file mode 100644 index 00000000..7b598e00 --- /dev/null +++ b/apps/example/src/screens/ChatScreen.tsx @@ -0,0 +1,62 @@ +import React, { useCallback } from 'react'; +import { + ChatScreen as BaseChatScreen, + MessageTypes, + type ImagePickerValue, +} from 'rn-firebase-chat'; +import { launchImageLibrary } from 'react-native-image-picker'; +import { useRoute } from '@react-navigation/native'; +import type { RouteProp } from '@react-navigation/native'; +import type { RootStackParamList } from '../navigation/RouteKey'; +import RouteKey from '../navigation/RouteKey'; + +type Route = RouteProp; + +export const ChatScreen: React.FC = () => { + const route = useRoute(); + const { conversationId, otherUserId, memberIds, names, name } = route.params; + + const partner = { + id: otherUserId ?? '', + name: name ?? 'User ' + (otherUserId ?? '').slice(0, 6), + avatar: 'https://i.pravatar.cc/150?img=2', + }; + + const resolvedMemberIds = memberIds ?? (partner.id ? [partner.id] : []); + + const onPressGallery = useCallback(async (): Promise< + ImagePickerValue | void + > => { + const result = await launchImageLibrary({ + mediaType: 'mixed', + selectionLimit: 1, + }); + + if (result.didCancel || !result.assets?.length) return; + const asset = result.assets[0]; + if (!asset?.uri) return; + + const isVideo = asset.type?.startsWith('video') ?? false; + const extension = + asset.fileName?.split('.').pop()?.toLowerCase() ?? + (isVideo ? 'mp4' : 'jpg'); + + return { + type: isVideo ? MessageTypes.video : MessageTypes.image, + path: asset.uri, + extension, + }; + }, []); + + return ( + + ); +}; diff --git a/example/src/screens/ListChatScreen.tsx b/apps/example/src/screens/ListChatScreen.tsx similarity index 84% rename from example/src/screens/ListChatScreen.tsx rename to apps/example/src/screens/ListChatScreen.tsx index f71507d6..058a62b1 100644 --- a/example/src/screens/ListChatScreen.tsx +++ b/apps/example/src/screens/ListChatScreen.tsx @@ -32,10 +32,13 @@ export const ListChatScreen: React.FC = ({ currentUserId, currentUserName const handleItemPress = useCallback( (data: any) => { chatDispatch?.(setConversation(data)); + const partnerId: string = data.members?.find((id: string) => id !== currentUserId) ?? ''; + // names map: { [userId]: ownDisplayName } — look up the partner's entry for the title + const partnerName: string = (partnerId ? data.names?.[partnerId] : undefined) ?? ''; navigation.navigate(RouteKey.ChatScreen, { conversationId: data.id, - name: data.names?.[currentUserId], - otherUserId: data.members?.find((id: string) => id !== currentUserId), + name: partnerName, + otherUserId: partnerId, }); }, [navigation, chatDispatch, currentUserId] @@ -50,12 +53,12 @@ export const ListChatScreen: React.FC = ({ currentUserId, currentUserName const conversationId = [currentUserId, targetId].sort().join('_'); navigation.navigate(RouteKey.ChatScreen, { conversationId, - name: targetId, + name: '', // partner name unknown until they open the conversation otherUserId: targetId, memberIds: [targetId], + // Seed only our own name; partner's name is written by their setConversationInfo names: { - [currentUserId]: targetId, - [targetId]: currentUserName, + [currentUserId]: currentUserName, }, }); }, [otherUserId, currentUserId, currentUserName, navigation]); diff --git a/apps/example/src/web/ChatScreen.tsx b/apps/example/src/web/ChatScreen.tsx new file mode 100644 index 00000000..24c587d4 --- /dev/null +++ b/apps/example/src/web/ChatScreen.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { IUser, ChatScreen as LibChatScreen, WebChatProvider } from '@saigontechnology/react-firebase-chat'; + +interface Props { + currentUser: IUser; +} + +export default function ChatScreen({ currentUser }: Props) { + const [conversationId, setConversationId] = useState(''); + const [recipientId, setRecipientId] = useState(''); + const [activeConversation, setActiveConversation] = useState(null); + + const handleStart = (e: React.FormEvent) => { + e.preventDefault(); + if (!recipientId.trim()) return; + const ids = [currentUser.id, recipientId.trim()].sort(); + const convId = conversationId.trim() || ids.join('_'); + setActiveConversation(convId); + }; + + return ( +
+
+ Your ID: {currentUser.id} +
+ + {!activeConversation ? ( +
+
+

Start a conversation

+ setRecipientId(e.target.value)} + style={{ padding: 8, border: '1px solid #ddd', borderRadius: 8 }} + /> + setConversationId(e.target.value)} + style={{ padding: 8, border: '1px solid #ddd', borderRadius: 8 }} + /> + +
+
+ ) : ( +
+
+ + Conversation: {activeConversation} +
+ + + +
+ )} +
+ ); +} diff --git a/apps/example/tsconfig.json b/apps/example/tsconfig.json new file mode 100644 index 00000000..41242236 --- /dev/null +++ b/apps/example/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "baseUrl": ".", + "paths": { + "rn-firebase-chat": ["../../packages/rn-firebase-chat/src/index"], + "@saigontechnology/react-firebase-chat": ["../../packages/react-firebase-chat/src/index"] + } + }, + "include": ["**/*.ts", "**/*.tsx", "declarations.d.ts"] +} diff --git a/apps/web-vite/.env.example b/apps/web-vite/.env.example new file mode 100644 index 00000000..dfaea31d --- /dev/null +++ b/apps/web-vite/.env.example @@ -0,0 +1,33 @@ +# Copy this file to .env and fill in your Firebase project values. +# Get these from Firebase Console → Project Settings → Your apps → Web app. +VITE_FIREBASE_API_KEY= +VITE_FIREBASE_AUTH_DOMAIN= +VITE_FIREBASE_PROJECT_ID= +VITE_FIREBASE_STORAGE_BUCKET= +VITE_FIREBASE_MESSAGING_SENDER_ID= +VITE_FIREBASE_APP_ID= + +# Cloudinary (used by CloudinaryStorageProvider for file uploads) +VITE_CLOUDINARY_CLOUD_NAME= +VITE_CLOUDINARY_UPLOAD_PRESET= +VITE_CLOUDINARY_FOLDER=chat +VITE_CLOUDINARY_API_KEY= +VITE_CLOUDINARY_API_SECRET= + +# ── Firebase Console setup required ────────────────────────────────────────── +# +# 1. Authentication → Sign-in method → Enable "Anonymous" +# The example signs in anonymously so Firestore rules can check request.auth. +# +# 2. Firestore Database → Rules → publish the following: +# +# rules_version = '2'; +# service cloud.firestore { +# match /databases/{database}/documents { +# match /{document=**} { +# allow read, write: if request.auth != null; +# } +# } +# } +# +# For production, tighten these rules to match your data model. diff --git a/apps/web-vite/.gitignore b/apps/web-vite/.gitignore new file mode 100644 index 00000000..deed335b --- /dev/null +++ b/apps/web-vite/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git a/apps/web-vite/GETTING_STARTED.md b/apps/web-vite/GETTING_STARTED.md new file mode 100644 index 00000000..b33ba18e --- /dev/null +++ b/apps/web-vite/GETTING_STARTED.md @@ -0,0 +1,82 @@ +# Getting Started — Web Example App + +This guide walks through running the Vite + React web example app. + +## Prerequisites + +- Node.js >= 16 +- pnpm (monorepo package manager) +- A Firebase project with Firestore enabled and a **Web app** registered + +## 1. Install dependencies + +From the **monorepo root**: + +```bash +pnpm install +``` + +## 2. Configure environment variables + +Copy the example env file and fill in your Firebase web config: + +```bash +cp apps/web-vite/.env.example apps/web-vite/.env +``` + +Get your Firebase config values from **Firebase Console > Project Settings > Your apps > Web app**, then add them to `.env` with the `VITE_FIREBASE_` prefix: + +``` +VITE_FIREBASE_API_KEY=... +VITE_FIREBASE_AUTH_DOMAIN=... +VITE_FIREBASE_PROJECT_ID=... +VITE_FIREBASE_STORAGE_BUCKET=... +VITE_FIREBASE_MESSAGING_SENDER_ID=... +VITE_FIREBASE_APP_ID=... +``` + +## 3. Run the dev server + +From the **monorepo root**: + +```bash +pnpm example:web +``` + +## Storage Provider Add-ons + +The library supports two media upload integrations. Pass a `storageProvider` to `WebChatProvider` to enable file/image uploads. + +### Cloudinary + +Add Cloudinary config to your `.env` file with the `VITE_CLOUDINARY_` prefix (see `.env.example`), then: + +```tsx +import { CloudinaryStorageProvider } from '@saigontechnology/react-firebase-chat'; + +const cloudinaryProvider = new CloudinaryStorageProvider({ + cloudName: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME ?? '', + uploadPreset: import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET ?? '', + folder: import.meta.env.VITE_CLOUDINARY_FOLDER ?? 'chat', + apiKey: import.meta.env.VITE_CLOUDINARY_API_KEY ?? '', + apiSecret: import.meta.env.VITE_CLOUDINARY_API_SECRET ?? '', +}); + + + {/* ... */} + +``` + +### Firebase Storage + +Uses the pre-integrated Firebase Storage service — no extra config needed: + +```tsx +import { FirebaseStorageProvider } from '@saigontechnology/react-firebase-chat'; + +const firebaseStorageProvider = new FirebaseStorageProvider(); + + + {/* ... */} + +``` diff --git a/apps/web-vite/index.html b/apps/web-vite/index.html new file mode 100644 index 00000000..8e4c38fa --- /dev/null +++ b/apps/web-vite/index.html @@ -0,0 +1,14 @@ + + + + + + rn-firebase-chat — Web Vite Example + + + + +
+ + + diff --git a/apps/web-vite/package.json b/apps/web-vite/package.json new file mode 100644 index 00000000..a95875ff --- /dev/null +++ b/apps/web-vite/package.json @@ -0,0 +1,28 @@ +{ + "name": "web-vite-example", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "../../node_modules/.bin/tsc && ../../node_modules/.bin/vite build", + "preview": "vite preview" + }, + "dependencies": { + "clsx": "^2.1.1", + "date-fns": "^3.6.0", + "firebase": "^10.14.1", + "framer-motion": "^11.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-textarea-autosize": "^8.5.4", + "@saigontechnology/react-firebase-chat": "workspace:*" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.3", + "vite": "^5.4.1" + } +} diff --git a/apps/web-vite/src/App.tsx b/apps/web-vite/src/App.tsx new file mode 100644 index 00000000..43d09b81 --- /dev/null +++ b/apps/web-vite/src/App.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import { getAuth, signInAnonymously, onAuthStateChanged } from 'firebase/auth'; +import { + WebChatProvider, + ChatScreen, + FirestoreServices, + UserService, + // CloudinaryStorageProvider, + // FirebaseStorageProvider, + type IUser, +} from '@saigontechnology/react-firebase-chat'; + +// const cloudinaryProvider = new CloudinaryStorageProvider({ +// cloudName: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME ?? '', +// uploadPreset: import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET ?? '', +// folder: import.meta.env.VITE_CLOUDINARY_FOLDER ?? 'chat', +// apiKey: import.meta.env.VITE_CLOUDINARY_API_KEY ?? '', +// apiSecret: import.meta.env.VITE_CLOUDINARY_API_SECRET ?? '', +// }); +// const firebaseStorageProvider = new FirebaseStorageProvider(); + +export default function App() { + const [currentUser, setCurrentUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const auth = getAuth(); + const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => { + if (firebaseUser) { + const { uid } = firebaseUser; + const userInfo: IUser = { id: uid, name: firebaseUser.displayName ?? 'Anonymous' }; + + // Ensure Firestore user document exists. + await UserService.getInstance().createUserIfNotExists(uid, { + name: userInfo.name ?? 'Anonymous', + }); + + // Configure shared FirestoreServices with the signed-in user. + await FirestoreServices.getInstance().configuration({ + userInfo: { id: uid, name: userInfo.name ?? 'Anonymous', avatar: firebaseUser.photoURL ?? '' }, + prefix: import.meta.env.VITE_FIREBASE_PREFIX ?? '', + }); + + setCurrentUser(userInfo); + } else { + signInAnonymously(auth).catch((err) => + console.error('Anonymous sign-in failed:', err) + ); + } + setLoading(false); + }); + + return () => unsubscribe(); + }, []); + + if (loading) { + return
Signing in…
; + } + + if (!currentUser) return null; + + return ( +
+ +
+ +
+
+
+ ); +} + +const styles: Record = { + center: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: '100vh', + }, + root: { + display: 'flex', + flexDirection: 'column', + height: '100vh', + background: '#f0f2f5', + }, + chatWrapper: { + flex: 1, + overflow: 'hidden', + display: 'flex', + maxWidth: 900, + width: '100%', + margin: '0 auto', + padding: '16px', + }, + chatScreen: { + flex: 1, + borderRadius: 12, + overflow: 'hidden', + boxShadow: '0 2px 12px rgba(0,0,0,0.1)', + }, +}; diff --git a/apps/web-vite/src/index.css b/apps/web-vite/src/index.css new file mode 100644 index 00000000..8ccf3eb3 --- /dev/null +++ b/apps/web-vite/src/index.css @@ -0,0 +1,14 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + margin: 0; + padding: 0; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} diff --git a/apps/web-vite/src/main.tsx b/apps/web-vite/src/main.tsx new file mode 100644 index 00000000..13544d4a --- /dev/null +++ b/apps/web-vite/src/main.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + type FirebaseConfig, + initializeFirebase, + getFirebaseFirestore, + getFirebaseApp, + FirestoreServices, + WebCryptoProvider, + createWebFirestoreClient, + WebFirebaseStorageProvider, +} from '@saigontechnology/react-firebase-chat'; +import App from './App'; +import '@saigontechnology/react-firebase-chat/styles.css'; +import './index.css'; + +const firebaseConfig: FirebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_API_KEY ?? '', + authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN ?? '', + projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID ?? '', + storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET ?? '', + messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID ?? '', + appId: import.meta.env.VITE_FIREBASE_APP_ID ?? '', +}; + +// Initialize Firebase before the React tree mounts. +initializeFirebase(firebaseConfig); + +// Wire the shared FirestoreServices with the web Firestore adapter + Web Crypto. +const firestoreServices = FirestoreServices.getInstance(); +firestoreServices.setFirestoreClient(createWebFirestoreClient(getFirebaseFirestore())); +firestoreServices.setCryptoProvider(new WebCryptoProvider()); + +// Wire Firebase Storage for file uploads. +firestoreServices.setStorageProvider(new WebFirebaseStorageProvider(getFirebaseApp())); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/apps/web-vite/tsconfig.json b/apps/web-vite/tsconfig.json new file mode 100644 index 00000000..c718a862 --- /dev/null +++ b/apps/web-vite/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "allowArbitraryExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "types": ["vite/client"], + "paths": { + "rn-firebase-chat/web": ["../../packages/rn-firebase-chat/lib/typescript/rn-firebase-chat/src/web.d.ts"], + "rn-firebase-chat": ["../../packages/rn-firebase-chat/lib/typescript/rn-firebase-chat/src/index.d.ts"] + } + }, + "include": ["src"] +} diff --git a/apps/web-vite/vite.config.ts b/apps/web-vite/vite.config.ts new file mode 100644 index 00000000..0466183a --- /dev/null +++ b/apps/web-vite/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/example/App.tsx b/example/App.tsx deleted file mode 100644 index 00c8c213..00000000 --- a/example/App.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - ActivityIndicator, - View, - TextInput, - TouchableOpacity, - Text, - StyleSheet, - LogBox, -} from 'react-native'; - -// iOS UIKit internal warning because new React Native architecture (Fabric/JSI) on iOS -LogBox.ignoreLogs([ - '[UIKitCore] RCTScrollViewComponentView', -]); - -import { NavigationContainer } from '@react-navigation/native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { ChatProvider } from 'rn-firebase-chat'; -import { getAuth, signInAnonymously } from '@react-native-firebase/auth'; -import { ListChatScreen } from './src/screens/ListChatScreen'; -import { ChatScreen } from './src/screens/ChatScreen'; -import type { RootStackParamList } from './src/navigation/RouteKey'; -import RouteKey from './src/navigation/RouteKey'; - -const Stack = createNativeStackNavigator(); - -export default function App() { - const [uid, setUid] = useState(null); - const [nameInput, setNameInput] = useState(''); - const [userInfo, setUserInfo] = useState<{ - id: string; - name: string; - avatar: string; - } | null>(null); - - useEffect(() => { - signInAnonymously(getAuth()) - .then((credential) => { - setUid(credential.user.uid); - }) - .catch((e: unknown) => console.error('Auth failed:', e)); - }, []); - - const handleJoin = () => { - if (!uid) return; - const name = nameInput.trim() || 'User ' + uid.slice(0, 6); - setUserInfo({ - id: uid, - name, - avatar: 'https://i.pravatar.cc/150?img=' + Math.floor(Math.random() * 70 + 1), - }); - }; - - if (!uid) { - return ( - - - - ); - } - - if (!userInfo) { - return ( - - Enter your display name - - - Join - - - ); - } - - return ( - - - - - {() => } - - ({ title: route.params.name ?? 'Chat' })} - /> - - - - ); -} - -const styles = StyleSheet.create({ - center: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 24, - }, - label: { fontSize: 16, marginBottom: 12, color: '#333' }, - input: { - width: '100%', - borderWidth: 1, - borderColor: '#ccc', - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 10, - fontSize: 15, - marginBottom: 12, - }, - button: { - backgroundColor: '#007AFF', - paddingHorizontal: 32, - paddingVertical: 12, - borderRadius: 8, - }, - buttonText: { color: '#fff', fontWeight: '600', fontSize: 15 }, -}); diff --git a/example/app.config.ts b/example/app.config.ts deleted file mode 100644 index e80774d4..00000000 --- a/example/app.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ExpoConfig } from 'expo/config'; - -const config: ExpoConfig = { - name: 'rn-firebase-chat-example', - slug: 'rn-firebase-chat-example', - version: '1.0.0', - orientation: 'portrait', - icon: './assets/icon.png', - userInterfaceStyle: 'light', - splash: { - image: './assets/splash-icon.png', - resizeMode: 'contain', - backgroundColor: '#ffffff', - }, - ios: { - supportsTablet: true, - bundleIdentifier: 'com.sts.demo.chatapp', - googleServicesFile: './ios/rnfirebasechatexample/GoogleService-Info.plist', - }, - android: { - package: 'com.sts.demo.chatapp', - googleServicesFile: './android/app/google-services.json', - adaptiveIcon: { - foregroundImage: './assets/android-icon-foreground.png', - backgroundImage: './assets/android-icon-background.png', - monochromeImage: './assets/android-icon-monochrome.png', - backgroundColor: '#E6F4FE', - }, - }, - plugins: [ - '@react-native-firebase/app', - [ - 'expo-build-properties', - { - ios: { - useFrameworks: 'static', - buildReactNativeFromSource: true, - }, - }, - ], - ], -}; - -export default config; diff --git a/example/ios/rnfirebasechatexample/Info.plist b/example/ios/rnfirebasechatexample/Info.plist deleted file mode 100644 index b73dfc9e..00000000 --- a/example/ios/rnfirebasechatexample/Info.plist +++ /dev/null @@ -1,76 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - rn-firebase-chat-example - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0.0 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLSchemes - - com.sts.demo.chatapp - - - - CFBundleVersion - 1 - LSMinimumSystemVersion - 12.0 - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSAllowsLocalNetworking - - - RCTNewArchEnabled - - UILaunchStoryboardName - SplashScreen - UIRequiredDeviceCapabilities - - arm64 - - UIRequiresFullScreen - - UIStatusBarStyle - UIStatusBarStyleDefault - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Light - UIViewControllerBasedStatusBarAppearance - - - diff --git a/example/src/screens/ChatScreen.tsx b/example/src/screens/ChatScreen.tsx deleted file mode 100644 index 2345991d..00000000 --- a/example/src/screens/ChatScreen.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { ChatScreen as BaseChatScreen } from 'rn-firebase-chat'; -import { useRoute } from '@react-navigation/native'; -import type { RouteProp } from '@react-navigation/native'; -import type { RootStackParamList } from '../navigation/RouteKey'; -import RouteKey from '../navigation/RouteKey'; - -type Route = RouteProp; - -export const ChatScreen: React.FC = () => { - const route = useRoute(); - const { conversationId, otherUserId, memberIds, names, name } = route.params; - - const partner = { - id: otherUserId ?? '', - name: name ?? 'User ' + (otherUserId ?? '').slice(0, 6), - avatar: 'https://i.pravatar.cc/150?img=2', - }; - - const resolvedMemberIds = memberIds ?? (partner.id ? [partner.id] : []); - - return ( - - ); -}; diff --git a/example/tsconfig.json b/example/tsconfig.json deleted file mode 100644 index edbd2ce4..00000000 --- a/example/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "baseUrl": ".", - "paths": { - "rn-firebase-chat": ["../packages/rn-firebase-chat/src/index"] - } - } -} diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..1b4e5356 --- /dev/null +++ b/firebase.json @@ -0,0 +1,6 @@ +{ + "firestore": { + "rules": "packages/rn-firebase-chat/firestore.rules", + "indexes": "packages/rn-firebase-chat/firestore.indexes.json" + } +} diff --git a/package.json b/package.json index bea99b54..3369f146 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "release": "pnpm --filter=rn-firebase-chat release", "release:ci": "pnpm --filter=rn-firebase-chat release:ci", "release:check": "pnpm --filter=rn-firebase-chat release:check", - "example": "pnpm -C example", - "bootstrap": "pnpm install && pnpm -C example" + "example": "pnpm -C apps/example", + "example:web": "pnpm --filter=web-vite-example dev", + "bootstrap": "pnpm install && pnpm -C apps/example" }, "devDependencies": { "turbo": "^2.5.4" diff --git a/packages/react-firebase-chat/README.md b/packages/react-firebase-chat/README.md new file mode 100644 index 00000000..0fb3fd3a --- /dev/null +++ b/packages/react-firebase-chat/README.md @@ -0,0 +1,98 @@ +# @saigontechnology/react-firebase-chat + +A web chat UI library backed by the Firebase JS SDK, providing ready-to-use React components for real-time messaging. + +## Installation + +```sh +npm install @saigontechnology/react-firebase-chat firebase react react-dom +# or +yarn add @saigontechnology/react-firebase-chat firebase react react-dom +``` + +### Optional peer dependencies + +```sh +npm install framer-motion react-hot-toast react-textarea-autosize +``` + +## Usage + +```tsx +import { + WebChatProvider, + ChatScreen, + FirestoreServices, + UserService, +} from "@saigontechnology/react-firebase-chat"; +import "@saigontechnology/react-firebase-chat/styles.css"; +import { initializeApp } from "firebase/app"; +import { getAuth, signInAnonymously } from "firebase/auth"; + +initializeApp({ apiKey: "...", projectId: "..." /* ... */ }); + +async function setup(uid: string, name: string) { + await UserService.getInstance().createUserIfNotExists(uid, { name }); + await FirestoreServices.getInstance().configuration({ + userInfo: { id: uid, name }, + }); +} + +function App() { + const currentUser = { id: "abc123", name: "John Doe" }; + return ( + + + + ); +} +``` + +## Vite configuration + +Vite needs a small plugin to stub React Native imports that leak through transitive dependencies: + +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import type { Plugin } from "vite"; + +export default defineConfig({ plugins: [react()] }); +``` + +A working example is in [`apps/web-vite/`](../../apps/web-vite/). + +## File uploads + +Pass a `storageProvider` to `WebChatProvider` to enable file/image uploads: + +```tsx +import { CloudinaryStorageProvider } from "@saigontechnology/react-firebase-chat"; + +const storage = new CloudinaryStorageProvider({ + cloudName: "my-cloud", + uploadPreset: "my-preset", + folder: "chat", +}); + + + +; +``` + +## Encryption + +```tsx + + + +``` + +## License + +MIT diff --git a/packages/react-firebase-chat/WEB_MIGRATION_GUIDE.md b/packages/react-firebase-chat/WEB_MIGRATION_GUIDE.md new file mode 100644 index 00000000..010cc5a7 --- /dev/null +++ b/packages/react-firebase-chat/WEB_MIGRATION_GUIDE.md @@ -0,0 +1,291 @@ +# Migration Guide: react-firebase-chat v1.0.0+ — WebChatProvider Architecture + +This guide covers migrating the web chat from the legacy `ChatProvider` (with `firebaseConfig`) to the new `WebChatProvider` architecture, aligning with the monorepo-based shared core in v1.0.0+. + +## Summary of Changes + +### Architecture + +| Area | Old | New | +|------|-----|-----| +| Provider | `ChatProvider` with `firebaseConfig` prop | `WebChatProvider` with `encryptionKey` + `enableEncrypt` | +| Firebase init | Handled inside `ChatProvider` | Handled externally by the host app | +| Styles import | `@saigontechnology/react-firebase-chat/styles` (no extension) | `@saigontechnology/react-firebase-chat/styles.css` | +| Conversation names (1:1) | `conversation.name` | `conversation.names?.[partnerId]` | +| Typing indicator names | Derived from `selectedPartners` only | Derived from `conversation.names` map with `selectedPartners` fallback | +| Partner resolution | Used `conversation.name` for all chats | Uses `conversation.names?.[partnerId]` for 1:1, `conversation.name` for groups | + +### Bug Fixes + +- **Partner names showing "Unknown"** — 1:1 chats now resolve display names from `conversation.names?.[partnerId]` instead of `conversation.name` +- **Typing indicator showing User ID** — `partnerNames` map now prefers the `names` field from the conversation document +- **Encryption race condition** — `WebChatProvider` waits for encryption configuration before fetching conversations (previously `ChatProvider` could fetch messages before encryption was ready) +- **Conversation timestamp resolution** — `listenConversationUpdate` now correctly resolves Firestore Timestamps to millisecond numbers, fixing missing datetime display and incorrect sort order after sending messages + +--- + +## Breaking Changes + +### 1. ChatProvider → WebChatProvider + +The `ChatProvider` component has been replaced by `WebChatProvider`. The `firebaseConfig` prop is removed — Firebase must be initialized externally by the host app before rendering the provider. + +```diff +import { +- ChatProvider, ++ WebChatProvider, + ChatScreen, +} from '@saigontechnology/react-firebase-chat' + +- + +- ++ +``` + +**Migration:** Initialize Firebase in your app before rendering `WebChatProvider`. Pass `encryptionKey` and `enableEncrypt` separately (previously encryption was implicitly enabled when `encryptionKey` was present). + +### 2. Styles Import Path Changed + +The CSS import path now requires the `.css` extension: + +```diff +- import '@saigontechnology/react-firebase-chat/styles' ++ import '@saigontechnology/react-firebase-chat/styles.css' +``` + +If you had a `@ts-expect-error` comment for the old import, remove it: + +```diff +- // @ts-expect-error -- CSS import has no type declarations +- import '@saigontechnology/react-firebase-chat/styles' ++ import '@saigontechnology/react-firebase-chat/styles.css' +``` + +### 3. Conversation Name Resolution for 1:1 Chats + +The `conversation.name` field is a **conversation-level** value (used for group chat names). For 1:1 chats, it may be empty or contain the creator's info. + +**Old:** `conversation.name` was used for all display names. + +**New:** Use `conversation.names?.[partnerId]` for 1:1 chats, falling back to `conversation.name` for groups. + +```diff +- const displayName = c.name || 'Unknown' ++ const isGroup = (c.members ?? []).length > 2 ++ const partnerId = isGroup ++ ? '' ++ : (c.members ?? []).find((m) => m !== currentUserId) || '' ++ const displayName = isGroup ++ ? (c.name ?? 'Group') ++ : (c.names?.[partnerId] ?? c.name ?? 'Unknown') +``` + +This applies to: +- Conversation list items +- Chat header +- Search filtering +- Avatar display + +### 4. Search Filtering Updated + +If you have custom search/filtering on conversations, update it to search by the correct name field: + +```diff +const filtered = searchQuery +- ? conversations.filter((c) => +- (c.name || '').toLowerCase().includes(searchQuery.toLowerCase()), +- ) ++ ? conversations.filter((c) => { ++ const isGroup = (c.members ?? []).length > 2 ++ const partnerId = isGroup ++ ? '' ++ : (c.members ?? []).find((m) => m !== user?.id) || '' ++ const name = isGroup ++ ? (c.name ?? '') ++ : (c.names?.[partnerId] ?? c.name ?? '') ++ return name.toLowerCase().includes(searchQuery.toLowerCase()) ++ }) + : conversations +``` + +### 5. Wait for Chat Settings Before Rendering + +If encryption is enabled, ensure the encryption key is loaded before rendering the provider to avoid the encryption race condition: + +```diff +- if (!chatUser) return null ++ if (!chatUser || isChatSettingsLoading) return null +``` + +--- + +## Internal Library Changes (No App Code Changes Needed) + +These changes are internal to `@saigontechnology/react-firebase-chat` and `@saigontechnology/firebase-chat-shared`. They are listed here for awareness. + +### Partner Resolution in ChatScreen + +`resolvePartners` now uses `conversation.names?.[partnerId]` for 1:1 chats instead of `conversation.name`: + +```typescript +// Old: all chats used conversation.name +name: conversation.name + +// New: 1:1 chats use per-member names map +const getPartnerName = (pid: string) => + isGroup + ? conversation.name + : (conversation.names?.[pid] ?? conversation.name); +``` + +### Typing Indicator Name Resolution + +The `partnerNames` map (used by `useTyping` to show display names in the typing indicator) now prefers the `names` field from the conversation document, with `selectedPartners` as a fallback: + +```typescript +// Old: only from selectedPartners (could be empty before resolvePartners completes) +const partnerNames = useMemo(() => { + const map: Record = {}; + selectedPartners.forEach((p) => { + if (p.name) map[p.id] = p.name; + }); + return map; +}, [selectedPartners]); + +// New: prefers conversation.names, falls back to selectedPartners +const partnerNames = useMemo(() => { + const convId = selectedConversationId || effectiveConversationId; + const conv = convId ? conversations.find((c) => c.id === convId) : undefined; + const map: Record = {}; + if (conv?.names) { + Object.entries(conv.names).forEach(([uid, name]) => { + if (uid !== `${currentUser.id}` && name) map[uid] = name; + }); + } + selectedPartners.forEach((p) => { + if (p.name && !map[p.id]) map[p.id] = p.name; + }); + return map; +}, [selectedPartners, conversations, selectedConversationId, effectiveConversationId, currentUser.id]); +``` + +### Conversation Timestamp Resolution in Real-time Listener + +`listenConversationUpdate` now resolves `updatedAt` from Firestore Timestamps to millisecond numbers, matching the behavior of `getListConversation`: + +```typescript +const rawTs = data.updatedAt as { toMillis?: () => number } | number | undefined; +const updatedAt = typeof rawTs === 'number' ? rawTs : (rawTs?.toMillis?.() ?? Date.now()); +``` + +--- + +## Step-by-Step Migration + +### 1. Update Dependencies + +```bash +pnpm install @saigontechnology/react-firebase-chat@latest +``` + +Or if using a local link: + +```bash +# Rebuild the library +cd /path/to/rn-firebase-chat/packages/react-firebase-chat +pnpm build + +# Reinstall in your project +cd /path/to/your-web-project +pnpm install +``` + +### 2. Update Provider + +Replace `ChatProvider` with `WebChatProvider` and remove `firebaseConfig`: + +```tsx +import { + WebChatProvider, + ChatScreen, +} from '@saigontechnology/react-firebase-chat' +import '@saigontechnology/react-firebase-chat/styles.css' + +// Firebase must be initialized before this component renders + + null} + renderChatList={renderChatList} + /> + +``` + +### 3. Update Custom Chat List (if using `renderChatList`) + +If you provide a custom `renderChatList`, update name resolution for each conversation item: + +```tsx +renderChatList={({ conversations, selectedConversationId, handleSelectConversation }) => { + return conversations.map((c) => { + const isGroup = (c.members ?? []).length > 2 + const partnerId = isGroup + ? '' + : (c.members ?? []).find((m) => m !== currentUserId) || '' + const displayName = isGroup + ? (c.name ?? 'Group') + : (c.names?.[partnerId] ?? c.name ?? 'Unknown') + + return ( +
handleSelectConversation(c)}> + + {displayName} +
+ ) + }) +}} +``` + +### 4. Update Styles Import + +```diff +- // @ts-expect-error -- CSS import has no type declarations +- import '@saigontechnology/react-firebase-chat/styles' ++ import '@saigontechnology/react-firebase-chat/styles.css' +``` + +--- + +## FAQ + +**Q: Do I need to initialize Firebase myself now?** +A: Yes. `WebChatProvider` no longer accepts `firebaseConfig`. Initialize Firebase in your app's entry point (e.g., `firebase.ts` config file) before rendering the provider. + +**Q: Will existing Firestore data work?** +A: Yes. The Firestore document structure is backward-compatible. The `names` field is additive — old conversations without it will continue to work, and it will be populated as users open conversations via `setConversationInfo`. + +**Q: Why does my conversation list still show "Unknown" for some conversations?** +A: The `names` field is populated when each user opens a conversation (via `setConversationInfo`). If a partner has never opened a conversation on the new version, their name won't be in the `names` map yet. The fallback chain is: `conversation.names?.[partnerId]` → `conversation.name` → `'Unknown'`. + +**Q: The typing indicator shows a user ID instead of a name. Why?** +A: This happens when the `names` map in the conversation document doesn't contain the typing user's ID. Ensure both users have opened the conversation at least once on the updated version so `setConversationInfo` can write their names. The library now reads from `conversation.names` first, then falls back to `selectedPartners`. + +**Q: Can I use `ChatProvider` and `WebChatProvider` interchangeably?** +A: No. `ChatProvider` is deprecated for web use. `WebChatProvider` is the replacement with proper encryption initialization ordering and no Firebase config coupling. + +**Q: Where is the React Native migration guide?** +A: See [`packages/rn-firebase-chat/MOBILE_MIGRATION_GUIDE.md`](../rn-firebase-chat/MOBILE_MIGRATION_GUIDE.md) for the mobile-specific migration guide. diff --git a/packages/react-firebase-chat/eslint.config.js b/packages/react-firebase-chat/eslint.config.js new file mode 100644 index 00000000..3841293d --- /dev/null +++ b/packages/react-firebase-chat/eslint.config.js @@ -0,0 +1,62 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import prettierPlugin from 'eslint-plugin-prettier'; + +export default [ + { + ignores: ['node_modules/**', 'lib/**', 'coverage/**', '**/*.d.ts'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 'latest', + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + 'react': reactPlugin, + 'react-hooks': reactHooksPlugin, + 'prettier': prettierPlugin, + }, + rules: { + ...reactPlugin.configs.recommended.rules, + ...reactHooksPlugin.configs.recommended.rules, + 'prettier/prettier': [ + 'error', + { + quoteProps: 'consistent', + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + }, + ], + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-empty-interface': 'warn', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-require-imports': 'off', + 'react-hooks/exhaustive-deps': 'warn', + 'react/prop-types': 'off', + 'react/display-name': 'off', + 'no-shadow': 'off', + 'no-undef': 'off', + 'semi': 'off', + 'comma-dangle': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, + }, +]; diff --git a/packages/react-firebase-chat/package.json b/packages/react-firebase-chat/package.json new file mode 100644 index 00000000..d7282af9 --- /dev/null +++ b/packages/react-firebase-chat/package.json @@ -0,0 +1,118 @@ +{ + "name": "@saigontechnology/react-firebase-chat", + "version": "1.1.0", + "description": "A comprehensive ReactJS Firebase Chat library with real-time messaging, user authentication, and modern UI components", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "style": "dist/styles.css", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./styles": "./dist/styles.css", + "./styles.css": "./dist/styles.css", + "./package.json": "./package.json" + }, + "files": ["dist/**/*", "README.md"], + "repository": { + "type": "git", + "url": "git+https://github.com/saigontechnology/rn-firebase-chat.git" + }, + "author": "Saigon Technology Solutions", + "license": "MIT", + "bugs": { + "url": "https://github.com/saigontechnology/rn-firebase-chat/issues" + }, + "homepage": "https://github.com/saigontechnology/rn-firebase-chat#readme", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "pnpm run clean && ../../node_modules/.bin/tsc --project tsconfig.build.json && pnpm run build:css", + "build:css": "../../node_modules/.bin/postcss src/styles/index.css -o dist/styles.css && ../../node_modules/.bin/postcss src/components/*.css -d dist/components && ../../node_modules/.bin/postcss 'src/addons/**/*.css' --base src/addons -d dist/addons", + "clean": "rm -rf dist", + "typecheck": "../../node_modules/.bin/tsc --noEmit", + "lint": "../../node_modules/.bin/eslint \"src/**/*.{ts,tsx}\"", + "prepack": "pnpm build", + "prepare": "pnpm build", + "release": "release-it", + "release:dry": "release-it --dry-run", + "release:ci": "release-it --ci", + "release:patch": "pnpm version patch && pnpm publish", + "release:minor": "pnpm version minor && pnpm publish", + "release:major": "pnpm version major && pnpm publish" + }, + "keywords": [ + "react", + "web", + "chat", + "firebase", + "firestore", + "messaging", + "real-time", + "ui", + "components", + "typescript", + "tailwind" + ], + "dependencies": { + "@saigontechnology/firebase-chat-shared": "workspace:*", + "@saigontechnology/chat-storage-providers": "workspace:*", + "clsx": "^2.1.1", + "date-fns": "^2.30.0" + }, + "bundledDependencies": [ + "@saigontechnology/firebase-chat-shared", + "@saigontechnology/chat-storage-providers" + ], + "peerDependencies": { + "firebase": ">=10", + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "framer-motion": { "optional": true }, + "react-hot-toast": { "optional": true }, + "react-textarea-autosize": { "optional": true } + }, + "devDependencies": { + "@release-it/conventional-changelog": "^10.0.1", + "@types/react": "~19.2.2", + "@types/react-dom": "^18.3.7", + "@tailwindcss/postcss": "^4", + "autoprefixer": "^10.4.16", + "firebase": "^10.14.1", + "framer-motion": "^11.18.2", + "postcss": "^8.4.32", + "postcss-cli": "^11.0.0", + "react": "19.2.0", + "react-dom": "19.2.0", + "react-hot-toast": "^2.4.1", + "react-textarea-autosize": "^8.5.9", + "release-it": "^19.0.3", + "tailwindcss": "^3.3.6", + "typescript": "^5.8.3" + }, + "release-it": { + "git": { + "commitMessage": "chore: release react-firebase-chat ${version}", + "tagName": "react-firebase-chat-v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true, + "releaseName": "@saigontechnology/react-firebase-chat v${version}" + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": "angular", + "infile": "CHANGELOG.md" + } + } + } +} diff --git a/packages/react-firebase-chat/postcss.config.cjs b/packages/react-firebase-chat/postcss.config.cjs new file mode 100644 index 00000000..c9dbc3a7 --- /dev/null +++ b/packages/react-firebase-chat/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: [ + require('postcss-import'), + require('tailwindcss'), + require('autoprefixer'), + ], +} diff --git a/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.css b/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.css new file mode 100644 index 00000000..c9276956 --- /dev/null +++ b/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.css @@ -0,0 +1,211 @@ +/* File Uploader Styles */ +.file-uploader { + width: 100%; + border: 2px dashed #ccc; + border-radius: 8px; + background: #fafafa; + transition: all 0.2s ease; +} + +.file-uploader:hover { + border-color: #007bff; + background: #f0f8ff; +} + +.file-uploader.drag-over { + border-color: #007bff; + background: #e7f3ff; + transform: scale(1.02); +} + +.file-uploader.disabled { + opacity: 0.6; + pointer-events: none; + background: #f5f5f5; +} + +.upload-area { + padding: 40px 20px; + text-align: center; + cursor: pointer; + transition: background 0.2s ease; +} + +.upload-placeholder { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.upload-icon { + font-size: 48px; + opacity: 0.6; + margin-bottom: 8px; +} + +.upload-placeholder p { + margin: 0; + font-size: 16px; + font-weight: 500; + color: #333; +} + +.upload-placeholder small { + color: #666; + font-size: 14px; +} + +.upload-progress { + padding: 20px; + text-align: center; +} + +.progress-bar { + width: 100%; + height: 8px; + background: #e9ecef; + border-radius: 4px; + overflow: hidden; + margin-bottom: 12px; +} + +.progress-fill { + height: 100%; + background: #007bff; + border-radius: 4px; + transition: width 0.3s ease; +} + +.upload-progress p { + margin: 0; + font-size: 14px; + font-weight: 500; + color: #333; +} + +.selected-files { + padding: 16px; + border-top: 1px solid #e9ecef; + background: #fff; +} + +.selected-files h4 { + margin: 0 0 12px 0; + font-size: 14px; + font-weight: 600; + color: #333; +} + +.file-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #f0f0f0; +} + +.file-item:last-child { + border-bottom: none; +} + +.file-name { + flex: 1; + font-size: 14px; + color: #333; + margin-right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-size { + font-size: 12px; + color: #666; + margin-right: 8px; +} + +.remove-button { + width: 20px; + height: 20px; + border: none; + background: #dc3545; + color: white; + border-radius: 50%; + font-size: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; +} + +.remove-button:hover { + background: #c82333; +} + +.remove-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.upload-button { + width: 100%; + padding: 12px; + background: #007bff; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease; +} + +.upload-button:hover { + background: #0056b3; +} + +.upload-button:disabled { + background: #6c757d; + cursor: not-allowed; +} + +.upload-error { + padding: 12px; + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + border-radius: 4px; + margin-top: 12px; + font-size: 14px; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .upload-area { + padding: 30px 15px; + } + + .upload-icon { + font-size: 40px; + } + + .upload-placeholder p { + font-size: 14px; + } + + .upload-placeholder small { + font-size: 12px; + } + + .file-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .file-name { + margin-right: 0; + } +} \ No newline at end of file diff --git a/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.tsx b/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.tsx new file mode 100644 index 00000000..9de58b0c --- /dev/null +++ b/packages/react-firebase-chat/src/addons/fileUpload/FileUploader.tsx @@ -0,0 +1,243 @@ +import React, { useRef, useState, useCallback } from 'react'; +import { useFileUpload } from './useFileUpload'; +import { FileUploaderProps } from './types'; +import './FileUploader.css'; + +export const FileUploader: React.FC = ({ + onFileSelect, + onUploadProgress, + onUploadComplete, + onError, + accept = '*/*', + multiple = false, + maxFiles = 5, + maxFileSize = 10 * 1024 * 1024, // 10MB + className = '', + disabled = false, + children, + customUploadFn, +}) => { + const fileInputRef = useRef(null); + const [isDragOver, setIsDragOver] = useState(false); + const [selectedFiles, setSelectedFiles] = useState([]); + + const { + uploadFiles, + uploading, + progress, + error: uploadError, + } = useFileUpload({ + onUploadProgress, + onUploadComplete: (url: string) => { + onUploadComplete?.([url]); + }, + onError, + maxFileSize, + customUploadFn, + }); + + const validateFiles = useCallback( + (files: FileList | File[]): File[] => { + const fileArray = Array.from(files); + + if (fileArray.length > maxFiles) { + onError?.(new Error(`Maximum ${maxFiles} files allowed`)); + return []; + } + + const validFiles = fileArray.filter((file) => { + if (file.size > maxFileSize) { + onError?.( + new Error( + `File "${file.name}" exceeds ${maxFileSize / (1024 * 1024)}MB limit` + ) + ); + return false; + } + return true; + }); + + return validFiles; + }, + [maxFiles, maxFileSize, onError] + ); + + const handleFileSelect = useCallback( + (files: File[]) => { + const validFiles = validateFiles(files); + if (validFiles.length > 0) { + setSelectedFiles(validFiles); + onFileSelect(validFiles); + } + }, + [validateFiles, onFileSelect] + ); + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + const files = event.target.files; + if (files && files.length > 0) { + handleFileSelect(Array.from(files)); + } + }, + [handleFileSelect] + ); + + const handleDragEnter = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!disabled) { + setIsDragOver(true); + } + }, + [disabled] + ); + + const handleDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragOver(false); + }, []); + + const handleDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + }, []); + + const handleDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragOver(false); + + if (disabled) return; + + const files = event.dataTransfer.files; + if (files && files.length > 0) { + handleFileSelect(Array.from(files)); + } + }, + [disabled, handleFileSelect] + ); + + const handleClick = useCallback(() => { + if (!disabled && fileInputRef.current) { + fileInputRef.current.click(); + } + }, [disabled]); + + const handleUpload = useCallback(async () => { + if (selectedFiles.length > 0) { + try { + const urls = await uploadFiles(selectedFiles); + onUploadComplete?.(urls); + setSelectedFiles([]); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } catch (error) { + onError?.(error as Error); + } + } + }, [selectedFiles, uploadFiles, onUploadComplete, onError]); + + const removeFile = useCallback( + (index: number) => { + const newFiles = selectedFiles.filter((_, i) => i !== index); + setSelectedFiles(newFiles); + onFileSelect(newFiles); + }, + [selectedFiles, onFileSelect] + ); + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return ( +
+ + + {children ? ( +
{children}
+ ) : ( +
+ {uploading ? ( +
+
+
+
+

Uploading... {Math.round(progress)}%

+
+ ) : ( +
+
📁
+

Click to select files or drag and drop

+ + Max {maxFiles} files, {formatFileSize(maxFileSize)} each + +
+ )} +
+ )} + + {/* Selected Files List */} + {selectedFiles.length > 0 && ( +
+

Selected Files:

+ {selectedFiles.map((file, index) => ( +
+ {file.name} + ({formatFileSize(file.size)}) + +
+ ))} + + {!uploading && ( + + )} +
+ )} + + {/* Error Display */} + {uploadError &&
{uploadError}
} +
+ ); +}; diff --git a/packages/react-firebase-chat/src/addons/fileUpload/index.ts b/packages/react-firebase-chat/src/addons/fileUpload/index.ts new file mode 100644 index 00000000..416fb0a4 --- /dev/null +++ b/packages/react-firebase-chat/src/addons/fileUpload/index.ts @@ -0,0 +1,9 @@ +export { FileUploader } from './FileUploader'; +export { useFileUpload } from './useFileUpload'; +export type { + FileUploaderProps, + UseFileUploadProps, + UseFileUploadReturn, + UploadedFile, + FileUploadOptions, +} from './types'; diff --git a/packages/react-firebase-chat/src/addons/fileUpload/types.ts b/packages/react-firebase-chat/src/addons/fileUpload/types.ts new file mode 100644 index 00000000..4cfcbb65 --- /dev/null +++ b/packages/react-firebase-chat/src/addons/fileUpload/types.ts @@ -0,0 +1,50 @@ +export interface FileUploaderProps { + onFileSelect: (files: File[]) => void; + onUploadProgress?: (progress: number) => void; + onUploadComplete?: (urls: string[]) => void; + onError?: (error: Error) => void; + accept?: string; + multiple?: boolean; + maxFiles?: number; + maxFileSize?: number; // in bytes + className?: string; + disabled?: boolean; + children?: React.ReactNode; + /** Upload function — typically wraps a StorageProvider (Cloudinary, WebFirebase, etc.). */ + customUploadFn?: (file: File) => Promise; +} + +export interface UseFileUploadProps { + onUploadProgress?: (progress: number) => void; + onUploadComplete?: (url: string) => void; + onError?: (error: Error) => void; + maxFileSize?: number; + allowedTypes?: string[]; + /** + * Upload function — required. Typically wraps a StorageProvider + * (WebFirebaseStorageProvider, CloudinaryStorageProvider, etc.) and + * returns the public download URL. + */ + customUploadFn?: (file: File) => Promise; +} + +export interface UseFileUploadReturn { + uploadFile: (file: File) => Promise; + uploadFiles: (files: File[]) => Promise; + uploading: boolean; + progress: number; + error: string | null; +} + +export interface UploadedFile { + file: File; + url: string; + uploadedAt: Date; +} + +export interface FileUploadOptions { + compress?: boolean; + quality?: number; // 0-1 for image compression + maxWidth?: number; + maxHeight?: number; +} diff --git a/packages/react-firebase-chat/src/addons/fileUpload/useFileUpload.ts b/packages/react-firebase-chat/src/addons/fileUpload/useFileUpload.ts new file mode 100644 index 00000000..3973b70c --- /dev/null +++ b/packages/react-firebase-chat/src/addons/fileUpload/useFileUpload.ts @@ -0,0 +1,172 @@ +import { useState, useCallback } from 'react'; +import { + UseFileUploadProps, + UseFileUploadReturn, + FileUploadOptions, +} from './types'; + +export const useFileUpload = ({ + onUploadProgress, + onUploadComplete, + onError, + maxFileSize = 10 * 1024 * 1024, // 10MB default + allowedTypes = [ + 'image/*', + 'video/*', + 'application/pdf', + '.doc', + '.docx', + '.txt', + ], + customUploadFn, +}: UseFileUploadProps = {}): UseFileUploadReturn => { + const [uploading, setUploading] = useState(false); + const [progress, setProgress] = useState(0); + const [error, setError] = useState(null); + + const validateFile = useCallback( + (file: File): boolean => { + // Check file size + if (file.size > maxFileSize) { + setError(`File size exceeds ${maxFileSize / (1024 * 1024)}MB limit`); + return false; + } + + // Check file type + const isAllowed = allowedTypes.some((type) => { + if (type.includes('*')) { + const baseType = type.split('/')[0] ?? ''; + return file.type.startsWith(baseType); + } + return file.type === type || file.name.toLowerCase().endsWith(type); + }); + + if (!isAllowed) { + setError( + `File type not allowed. Supported types: ${allowedTypes.join(', ')}` + ); + return false; + } + + return true; + }, + [maxFileSize, allowedTypes] + ); + + const compressImage = useCallback( + (file: File, options: FileUploadOptions): Promise => { + return new Promise((resolve) => { + if (!file.type.startsWith('image/') || !options.compress) { + resolve(file); + return; + } + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = () => { + const { maxWidth = 1920, maxHeight = 1080, quality = 0.8 } = options; + + let { width, height } = img; + + // Calculate new dimensions + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = width * ratio; + height = height * ratio; + } + + canvas.width = width; + canvas.height = height; + + ctx?.drawImage(img, 0, 0, width, height); + + canvas.toBlob( + (blob) => { + if (blob) { + const compressedFile = new File([blob], file.name, { + type: file.type, + lastModified: Date.now(), + }); + resolve(compressedFile); + } else { + resolve(file); + } + }, + file.type, + quality + ); + }; + + img.src = URL.createObjectURL(file); + }); + }, + [] + ); + + const uploadFile = useCallback( + async (file: File, options: FileUploadOptions = {}): Promise => { + try { + setError(null); + setUploading(true); + setProgress(0); + + if (!validateFile(file)) { + throw new Error('File validation failed'); + } + + // Compress image if needed + const processedFile = await compressImage(file, options); + + // Use custom upload function when provided (e.g. Cloudinary provider) + if (customUploadFn) { + setProgress(50); + onUploadProgress?.(50); + const downloadURL = await customUploadFn(processedFile); + setUploading(false); + setProgress(100); + onUploadComplete?.(downloadURL); + return downloadURL; + } + + throw new Error( + 'No upload method configured. Provide a customUploadFn (e.g. wrapping ' + + 'WebFirebaseStorageProvider or CloudinaryStorageProvider) to useFileUpload.' + ); + } catch (error) { + setUploading(false); + setProgress(0); + const errorMessage = + error instanceof Error ? error.message : 'Upload failed'; + setError(errorMessage); + onError?.(new Error(errorMessage)); + throw error; + } + }, + [ + customUploadFn, + validateFile, + compressImage, + onUploadProgress, + onUploadComplete, + onError, + ] + ); + + const uploadFiles = useCallback( + async (files: File[]): Promise => { + const uploadPromises = files.map((file) => uploadFile(file)); + return Promise.all(uploadPromises); + }, + [uploadFile] + ); + + return { + uploadFile, + uploadFiles, + uploading, + progress, + error, + }; +}; diff --git a/packages/react-firebase-chat/src/addons/gallery/GalleryView.css b/packages/react-firebase-chat/src/addons/gallery/GalleryView.css new file mode 100644 index 00000000..8223eef4 --- /dev/null +++ b/packages/react-firebase-chat/src/addons/gallery/GalleryView.css @@ -0,0 +1,177 @@ +/* Gallery View Styles */ +.gallery-view { + padding: 16px; + background: #f8f9fa; + border-radius: 8px; +} + +.gallery-view.empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +} + +.empty-state { + text-align: center; + color: #6c757d; +} + +.empty-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.empty-state p { + margin: 0; + font-size: 16px; +} + +.gallery-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); +} + +.gallery-item { + position: relative; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + cursor: pointer; + background: #f5f5f5; + transition: transform 0.2s ease; +} + +.gallery-item:hover { + transform: scale(1.05); +} + +.media-thumbnail { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; +} + +.video-thumbnail { + position: relative; + width: 100%; + height: 100%; +} + +.play-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; +} + +.play-button { + font-size: 24px; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +.audio-thumbnail, +.file-thumbnail { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 8px; + text-align: center; + height: 100%; + background: #e9ecef; +} + +.audio-icon, +.file-icon { + font-size: 32px; + margin-bottom: 8px; +} + +.audio-thumbnail p, +.file-thumbnail p { + margin: 0 0 4px 0; + font-size: 12px; + font-weight: 500; + word-break: break-word; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.audio-thumbnail small, +.file-thumbnail small { + font-size: 10px; + color: #6c757d; +} + +.delete-button { + position: absolute; + top: 4px; + right: 4px; + width: 24px; + height: 24px; + border: none; + background: rgba(220, 53, 69, 0.9); + color: white; + border-radius: 50%; + font-size: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease; +} + +.gallery-item:hover .delete-button { + opacity: 1; +} + +.delete-button:hover { + background: #dc3545; +} + +.gallery-footer { + margin-top: 16px; + text-align: center; + color: #6c757d; + font-size: 14px; +} + +.gallery-footer p { + margin: 0; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .gallery-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 12px; + } + + .gallery-view { + padding: 12px; + } + + .audio-thumbnail p, + .file-thumbnail p { + font-size: 11px; + } + + .audio-icon, + .file-icon { + font-size: 28px; + } +} \ No newline at end of file diff --git a/packages/react-firebase-chat/src/addons/gallery/GalleryView.tsx b/packages/react-firebase-chat/src/addons/gallery/GalleryView.tsx new file mode 100644 index 00000000..9adb917b --- /dev/null +++ b/packages/react-firebase-chat/src/addons/gallery/GalleryView.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { useGallery } from './useGallery'; +import { GalleryViewProps, MediaFile } from './types'; +import './GalleryView.css'; + +export const GalleryView: React.FC = ({ + files, + onFileSelect, + onFileDelete, + className = '', + maxItems = 20, + gridColumns = 4, + showDeleteButton = true, +}) => { + const { openViewer, deleteFile } = useGallery({ + files, + onFileSelect, + onFileDelete, + }); + + const displayFiles = files.slice(0, maxItems); + + const getFileIcon = (type: string) => { + switch (type) { + case 'image': + return '🖼️'; + case 'video': + return '🎥'; + case 'audio': + return '🎵'; + case 'document': + return '📄'; + default: + return '📎'; + } + }; + + const handleFileClick = (file: MediaFile) => { + openViewer(file); + onFileSelect?.(file); + }; + + const handleDelete = (e: React.MouseEvent, fileId: string) => { + e.stopPropagation(); + if (window.confirm('Are you sure you want to delete this file?')) { + deleteFile(fileId); + } + }; + + const formatFileSize = (bytes?: number): string => { + if (!bytes) return ''; + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + if (files.length === 0) { + return ( +
+
+
📁
+

No files to display

+
+
+ ); + } + + return ( +
+
+ {displayFiles.map((file) => ( +
handleFileClick(file)} + > + {file.type === 'image' ? ( + {file.name} + ) : file.type === 'video' ? ( +
+
+ ) : file.type === 'audio' ? ( +
+
🎵
+

{file.name}

+ {formatFileSize(file.size)} +
+ ) : ( +
+
{getFileIcon(file.type)}
+

{file.name}

+ {formatFileSize(file.size)} +
+ )} + + {showDeleteButton && ( + + )} +
+ ))} +
+ + {files.length > maxItems && ( +
+

+ Showing {maxItems} of {files.length} files +

+
+ )} +
+ ); +}; diff --git a/packages/react-firebase-chat/src/addons/gallery/MediaViewer.css b/packages/react-firebase-chat/src/addons/gallery/MediaViewer.css new file mode 100644 index 00000000..02da456a --- /dev/null +++ b/packages/react-firebase-chat/src/addons/gallery/MediaViewer.css @@ -0,0 +1,210 @@ +/* Media Viewer Styles */ +.media-viewer { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.9); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; +} + +.media-viewer-content { + position: relative; + max-width: 90vw; + max-height: 90vh; + border-radius: 8px; + overflow: hidden; +} + +.media-content { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border-radius: 8px; +} + +.media-viewer-controls { + position: absolute; + top: 20px; + right: 20px; + display: flex; + gap: 12px; +} + +.viewer-button { + width: 44px; + height: 44px; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + color: white; + font-size: 18px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease; + backdrop-filter: blur(4px); +} + +.viewer-button:hover { + background: rgba(255, 255, 255, 0.3); +} + +.close-button { + background: rgba(220, 53, 69, 0.8); +} + +.close-button:hover { + background: rgba(220, 53, 69, 1); +} + +.nav-button { + background: rgba(0, 123, 255, 0.8); +} + +.nav-button:hover { + background: rgba(0, 123, 255, 1); +} + +.download-button { + background: rgba(40, 167, 69, 0.8); + text-decoration: none; + color: white; +} + +.download-button:hover { + background: rgba(40, 167, 69, 1); +} + +.media-info { + position: absolute; + bottom: 20px; + left: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 16px; + border-radius: 8px; + backdrop-filter: blur(4px); +} + +.media-info h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + word-break: break-word; +} + +.media-info p { + margin: 0; + font-size: 14px; + color: rgba(255, 255, 255, 0.8); +} + +/* Audio Player Styles */ +.audio-player { + background: #2a2a2a; + color: white; + padding: 40px; + border-radius: 8px; + text-align: center; + min-width: 300px; +} + +.audio-info h3 { + margin: 0 0 8px 0; + font-size: 20px; + color: white; +} + +.audio-info p { + margin: 0 0 20px 0; + color: #ccc; +} + +.audio-player audio { + width: 100%; + margin-top: 20px; +} + +/* Document Viewer Styles */ +.document-viewer { + background: #2a2a2a; + color: white; + padding: 40px; + border-radius: 8px; + text-align: center; + min-width: 300px; +} + +.document-info h3 { + margin: 0 0 8px 0; + font-size: 20px; + color: white; +} + +.document-info p { + margin: 0 0 20px 0; + color: #ccc; +} + +.download-link { + display: inline-block; + padding: 12px 24px; + background: #007bff; + color: white; + text-decoration: none; + border-radius: 6px; + font-weight: 500; + transition: background 0.2s ease; +} + +.download-link:hover { + background: #0056b3; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .media-viewer-content { + max-width: 95vw; + max-height: 95vh; + } + + .media-viewer-controls { + top: 10px; + right: 10px; + gap: 8px; + } + + .viewer-button { + width: 36px; + height: 36px; + font-size: 16px; + } + + .media-info { + bottom: 10px; + left: 10px; + right: 10px; + padding: 12px; + } + + .media-info h3 { + font-size: 16px; + } + + .media-info p { + font-size: 12px; + } + + .audio-player, + .document-viewer { + padding: 30px 20px; + min-width: 250px; + } +} \ No newline at end of file diff --git a/packages/react-firebase-chat/src/addons/gallery/MediaViewer.tsx b/packages/react-firebase-chat/src/addons/gallery/MediaViewer.tsx new file mode 100644 index 00000000..523c6ab5 --- /dev/null +++ b/packages/react-firebase-chat/src/addons/gallery/MediaViewer.tsx @@ -0,0 +1,148 @@ +import React, { useEffect } from 'react'; +import { MediaViewerProps } from './types'; +import './MediaViewer.css'; + +export const MediaViewer: React.FC = ({ + file, + isOpen, + onClose, + onNext, + onPrevious, + className = '', +}) => { + // Handle keyboard navigation + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case 'Escape': + onClose(); + break; + case 'ArrowLeft': + onPrevious?.(); + break; + case 'ArrowRight': + onNext?.(); + break; + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose, onNext, onPrevious]); + + // Prevent body scroll when viewer is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + + return () => { + document.body.style.overflow = 'auto'; + }; + }, [isOpen]); + + if (!isOpen || !file) return null; + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const renderMediaContent = () => { + switch (file.type) { + case 'image': + return {file.name}; + + case 'video': + return ( +