diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7280a5f..2b003d6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,10 @@ "Bash(bun tsc:*)", "Bash(ls:*)", "Bash(bun x tsc:*)", - "Bash(bun:*)" + "Bash(bun:*)", + "Bash(mkdir:*)", + "Bash(cp:*)", + "Bash(rg:*)" ], "deny": [] } diff --git a/README.md b/README.md index c4a5cd8..d5f889a 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,230 @@ -# React Telegram Bot ⚛️🤖 +# React Telegram ⚛️🤖 -Build interactive Telegram bots using React components with state management, just like a web app! +Build interactive Telegram bots using React components! This monorepo contains packages for creating Telegram bots with familiar React patterns, state management, and full TypeScript support. -    +   + +## 📦 Packages + +### [@react-telegram/core](./packages/core) +Core React reconciler for building Telegram bot interfaces. Translates React components into Telegram message structures. + +### [@react-telegram/mtcute-adapter](./packages/mtcute-adapter) +MTCute adapter that connects the React reconciler with the Telegram Bot API. + +### [Examples](./packages/examples) +Complete example bots demonstrating various features and patterns. + +## 🚀 Installation + +```bash +# Using bun (recommended) +bun add @react-telegram/core @react-telegram/mtcute-adapter + +# Using npm +npm install @react-telegram/core @react-telegram/mtcute-adapter + +# Using yarn +yarn add @react-telegram/core @react-telegram/mtcute-adapter +``` ## ✨ Features - **React Components**: Write bot interfaces using familiar React syntax -- **State Management**: Full React state with `useState`, `useEffect`, and more -- **Interactive Buttons**: onClick handlers that work just like web buttons -- **Rich Text Formatting**: Bold, italic, code blocks, spoilers, and more -- **Message Editing**: Efficient updates using Telegram's edit message API -- **TypeScript Support**: Full type safety throughout -- **Hot Reload**: Instant development feedback with Bun's hot reload +- **State Management**: Full React state with `useState`, `useEffect`, and hooks +- **Interactive Elements**: Buttons with onClick handlers, text inputs +- **Rich Formatting**: Bold, italic, code blocks, spoilers, links, and more +- **Message Updates**: Automatic message editing when state changes +- **TypeScript Support**: Full type safety with autocompletion +- **Line Breaks**: Use `` tags for clean formatting -## 🚀 Quick Start - -```bash -# Clone the repository -git clone https://github.com/your-username/react-telegram-bot.git -cd react-telegram-bot - -# Install dependencies -bun install - -# Set up environment variables -cp .env.example .env -# Edit .env with your Telegram bot credentials - -# Run the bot -bun run src/example-bot.tsx -``` - -## 📱 Example Bot - -Here's a complete interactive counter bot in just a few lines: +## 📱 Quick Example ```tsx import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; -const CounterApp = () => { +const CounterBot = () => { const [count, setCount] = useState(0); return ( <> - 🔢 Counter Bot - {'\n\n'} - Current count: {count} - {'\n\n'} + Counter Bot 🤖 + + + Current count: {count} + + - setCount(c => c - 1)}>➖ Decrease - setCount(c => c + 1)}>➕ Increase - - - setCount(0)}>🔄 Reset + setCount(count - 1)}>➖ Decrease + setCount(count + 1)}>➕ Increase > ); }; -// Set up the bot -const adapter = new MtcuteAdapter(config); -adapter.onCommand('counter', () => ); +// Initialize the bot +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + await adapter.start(process.env.BOT_TOKEN!); + + console.log('Bot is running!'); +} + +main().catch(console.error); ``` -## 🎯 What Makes This Special? +## 🛠️ Development Setup -### Real React State Management -Components maintain state between interactions, just like in a web app: +This project uses Bun workspaces for managing multiple packages. +```bash +# Clone the repository +git clone https://github.com/your-username/react-telegram.git +cd react-telegram + +# Install dependencies +bun install + +# Run examples +cd packages/examples +bun run start +``` + +### Environment Variables + +Create a `.env` file in the examples package: + +```env +API_ID=your_telegram_api_id +API_HASH=your_telegram_api_hash +BOT_TOKEN=your_bot_token_from_botfather +``` + +Get your credentials from: +- Bot token: [@BotFather](https://t.me/botfather) +- API credentials: [my.telegram.org](https://my.telegram.org) + +## 📖 Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-Specific +- `` - Hidden spoiler text +- `` - Custom emoji +- `` - Quotes +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## 🎯 Advanced Examples + +### Todo List Bot ```tsx -const TodoApp = () => { - const [todos, setTodos] = useState(['Buy milk', 'Learn React']); +const TodoBot = () => { + const [todos, setTodos] = useState([]); return ( <> 📝 Todo List - {todos.map(todo => <>{todo}{'\n'}>)} - setTodos([...todos, 'New task'])}> - ➕ Add Task - + + + {todos.length === 0 ? ( + No todos yet! + ) : ( + todos.map((todo, i) => ( + <> + {i + 1}. {todo} + + > + )) + )} + + + setTodos([...todos, `Task ${todos.length + 1}`])}> + ➕ Add Task + + setTodos(todos.slice(0, -1))}> + ➖ Remove Last + + > ); }; ``` -### Rich Text Formatting -Support for all Telegram formatting features: - +### Input Handling ```tsx -<> - Bold and italic text - inline code - Code blocks - Quotes - Hidden text - Links -> +const InputBot = () => { + const [name, setName] = useState(''); + + return ( + <> + What's your name? + + + {name && <>Hello, {name}!>} + setName(text)} + autoDelete // Automatically delete user's message + /> + > + ); +}; ``` -### Efficient Message Updates -The bot automatically uses Telegram's edit message API for updates instead of sending multiple messages: +## 🏗️ Project Structure -- ✅ First render: Sends new message -- ✅ State changes: Edits existing message -- ✅ No message spam in chats - -## 🛠️ Built With - -- **[MTCute](https://mtcute.dev/)** - Modern Telegram client library -- **[React](https://react.dev/)** - UI library with custom reconciler -- **[Bun](https://bun.sh/)** - Fast JavaScript runtime and package manager -- **[TypeScript](https://typescriptlang.org/)** - Type safety - -## 📖 How It Works - -This project implements a custom React reconciler that translates React components into Telegram messages: - -1. **React Components** → **Virtual DOM Tree** -2. **Custom Reconciler** → **Telegram Message Structure** -3. **Message Renderer** → **Rich Text + Inline Keyboards** -4. **State Updates** → **Message Edits** - -The reconciler handles: -- Text formatting (``, ``, ``, etc.) -- Interactive buttons with click handlers -- Message layout with rows and columns -- Efficient updates via message editing - -## 🎮 Example Bots Included - -### 🔢 Counter Bot -Interactive counter with increment/decrement buttons - -### 📝 Todo List Bot -Full todo list manager with add/remove/toggle functionality - -### ❓ Help Bot -Multi-section help system with navigation - -### 🎨 Formatting Demo -Showcase of all supported Telegram formatting features - -## 🚀 Getting Started - -### Prerequisites -- [Bun](https://bun.sh/) installed -- Telegram Bot Token from [@BotFather](https://t.me/botfather) -- Telegram API credentials from [my.telegram.org](https://my.telegram.org) - -### Environment Setup -```bash -# Required environment variables -API_ID=your_api_id -API_HASH=your_api_hash -BOT_TOKEN=your_bot_token -STORAGE_PATH=.mtcute # Optional ``` - -### Development -```bash -# Run with hot reload -bun --hot src/example-bot.tsx - -# Run tests -bun test - -# Type check -bun run tsc --noEmit +react-telegram/ +├── packages/ +│ ├── core/ # Core React reconciler +│ │ ├── src/ +│ │ │ ├── reconciler.ts +│ │ │ ├── jsx.d.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ ├── mtcute-adapter/ # MTCute Telegram adapter +│ │ ├── src/ +│ │ │ ├── mtcute-adapter.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ └── examples/ # Example bots +│ ├── src/ +│ │ ├── example-bot.tsx +│ │ ├── quiz-bot.tsx +│ │ └── ... +│ └── package.json +│ +└── package.json # Root workspace configuration ``` -## 📚 API Reference - -### MtcuteAdapter -```tsx -const adapter = new MtcuteAdapter({ - apiId: number, - apiHash: string, - botToken: string, - storage?: string -}); - -// Register command handlers -adapter.onCommand('start', (ctx) => ); - -// Start the bot -await adapter.start(); -``` - -### Supported Elements -- ``, ``, ``, `` - Text formatting -- ``, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-Specific +- `` - Hidden spoiler text +- `` - Custom emoji +- `` - Quotes +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## 🎯 Advanced Examples + +### Todo List Bot ```tsx -const TodoApp = () => { - const [todos, setTodos] = useState(['Buy milk', 'Learn React']); +const TodoBot = () => { + const [todos, setTodos] = useState([]); return ( <> 📝 Todo List - {todos.map(todo => <>{todo}{'\n'}>)} - setTodos([...todos, 'New task'])}> - ➕ Add Task - + + + {todos.length === 0 ? ( + No todos yet! + ) : ( + todos.map((todo, i) => ( + <> + {i + 1}. {todo} + + > + )) + )} + + + setTodos([...todos, `Task ${todos.length + 1}`])}> + ➕ Add Task + + setTodos(todos.slice(0, -1))}> + ➖ Remove Last + + > ); }; ``` -### Rich Text Formatting -Support for all Telegram formatting features: - +### Input Handling ```tsx -<> - Bold and italic text - inline code - Code blocks - Quotes - Hidden text - Links -> +const InputBot = () => { + const [name, setName] = useState(''); + + return ( + <> + What's your name? + + + {name && <>Hello, {name}!>} + setName(text)} + autoDelete // Automatically delete user's message + /> + > + ); +}; ``` -### Efficient Message Updates -The bot automatically uses Telegram's edit message API for updates instead of sending multiple messages: +## 🏗️ Project Structure -- ✅ First render: Sends new message -- ✅ State changes: Edits existing message -- ✅ No message spam in chats - -## 🛠️ Built With - -- **[MTCute](https://mtcute.dev/)** - Modern Telegram client library -- **[React](https://react.dev/)** - UI library with custom reconciler -- **[Bun](https://bun.sh/)** - Fast JavaScript runtime and package manager -- **[TypeScript](https://typescriptlang.org/)** - Type safety - -## 📖 How It Works - -This project implements a custom React reconciler that translates React components into Telegram messages: - -1. **React Components** → **Virtual DOM Tree** -2. **Custom Reconciler** → **Telegram Message Structure** -3. **Message Renderer** → **Rich Text + Inline Keyboards** -4. **State Updates** → **Message Edits** - -The reconciler handles: -- Text formatting (``, ``, ``, etc.) -- Interactive buttons with click handlers -- Message layout with rows and columns -- Efficient updates via message editing - -## 🎮 Example Bots Included - -### 🔢 Counter Bot -Interactive counter with increment/decrement buttons - -### 📝 Todo List Bot -Full todo list manager with add/remove/toggle functionality - -### ❓ Help Bot -Multi-section help system with navigation - -### 🎨 Formatting Demo -Showcase of all supported Telegram formatting features - -## 🚀 Getting Started - -### Prerequisites -- [Bun](https://bun.sh/) installed -- Telegram Bot Token from [@BotFather](https://t.me/botfather) -- Telegram API credentials from [my.telegram.org](https://my.telegram.org) - -### Environment Setup -```bash -# Required environment variables -API_ID=your_api_id -API_HASH=your_api_hash -BOT_TOKEN=your_bot_token -STORAGE_PATH=.mtcute # Optional ``` - -### Development -```bash -# Run with hot reload -bun --hot src/example-bot.tsx - -# Run tests -bun test - -# Type check -bun run tsc --noEmit +react-telegram/ +├── packages/ +│ ├── core/ # Core React reconciler +│ │ ├── src/ +│ │ │ ├── reconciler.ts +│ │ │ ├── jsx.d.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ ├── mtcute-adapter/ # MTCute Telegram adapter +│ │ ├── src/ +│ │ │ ├── mtcute-adapter.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ └── examples/ # Example bots +│ ├── src/ +│ │ ├── example-bot.tsx +│ │ ├── quiz-bot.tsx +│ │ └── ... +│ └── package.json +│ +└── package.json # Root workspace configuration ``` -## 📚 API Reference - -### MtcuteAdapter -```tsx -const adapter = new MtcuteAdapter({ - apiId: number, - apiHash: string, - botToken: string, - storage?: string -}); - -// Register command handlers -adapter.onCommand('start', (ctx) => ); - -// Start the bot -await adapter.start(); -``` - -### Supported Elements -- ``, ``, ``, `` - Text formatting -- ``, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Code blocks +- `` - Line breaks + +### Telegram-Specific +- `` - Hidden spoiler text +- `` - Custom emoji +- `` - Quotes +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## 🎯 Advanced Examples + +### Todo List Bot ```tsx -const TodoApp = () => { - const [todos, setTodos] = useState(['Buy milk', 'Learn React']); +const TodoBot = () => { + const [todos, setTodos] = useState([]); return ( <> 📝 Todo List - {todos.map(todo => <>{todo}{'\n'}>)} - setTodos([...todos, 'New task'])}> - ➕ Add Task - + + + {todos.length === 0 ? ( + No todos yet! + ) : ( + todos.map((todo, i) => ( + <> + {i + 1}. {todo} + + > + )) + )} + + + setTodos([...todos, `Task ${todos.length + 1}`])}> + ➕ Add Task + + setTodos(todos.slice(0, -1))}> + ➖ Remove Last + + > ); }; ``` -### Rich Text Formatting -Support for all Telegram formatting features: - +### Input Handling ```tsx -<> - Bold and italic text - inline code - Code blocks - Quotes - Hidden text - Links -> +const InputBot = () => { + const [name, setName] = useState(''); + + return ( + <> + What's your name? + + + {name && <>Hello, {name}!>} + setName(text)} + autoDelete // Automatically delete user's message + /> + > + ); +}; ``` -### Efficient Message Updates -The bot automatically uses Telegram's edit message API for updates instead of sending multiple messages: +## 🏗️ Project Structure -- ✅ First render: Sends new message -- ✅ State changes: Edits existing message -- ✅ No message spam in chats - -## 🛠️ Built With - -- **[MTCute](https://mtcute.dev/)** - Modern Telegram client library -- **[React](https://react.dev/)** - UI library with custom reconciler -- **[Bun](https://bun.sh/)** - Fast JavaScript runtime and package manager -- **[TypeScript](https://typescriptlang.org/)** - Type safety - -## 📖 How It Works - -This project implements a custom React reconciler that translates React components into Telegram messages: - -1. **React Components** → **Virtual DOM Tree** -2. **Custom Reconciler** → **Telegram Message Structure** -3. **Message Renderer** → **Rich Text + Inline Keyboards** -4. **State Updates** → **Message Edits** - -The reconciler handles: -- Text formatting (``, ``, ``, etc.) -- Interactive buttons with click handlers -- Message layout with rows and columns -- Efficient updates via message editing - -## 🎮 Example Bots Included - -### 🔢 Counter Bot -Interactive counter with increment/decrement buttons - -### 📝 Todo List Bot -Full todo list manager with add/remove/toggle functionality - -### ❓ Help Bot -Multi-section help system with navigation - -### 🎨 Formatting Demo -Showcase of all supported Telegram formatting features - -## 🚀 Getting Started - -### Prerequisites -- [Bun](https://bun.sh/) installed -- Telegram Bot Token from [@BotFather](https://t.me/botfather) -- Telegram API credentials from [my.telegram.org](https://my.telegram.org) - -### Environment Setup -```bash -# Required environment variables -API_ID=your_api_id -API_HASH=your_api_hash -BOT_TOKEN=your_bot_token -STORAGE_PATH=.mtcute # Optional ``` - -### Development -```bash -# Run with hot reload -bun --hot src/example-bot.tsx - -# Run tests -bun test - -# Type check -bun run tsc --noEmit +react-telegram/ +├── packages/ +│ ├── core/ # Core React reconciler +│ │ ├── src/ +│ │ │ ├── reconciler.ts +│ │ │ ├── jsx.d.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ ├── mtcute-adapter/ # MTCute Telegram adapter +│ │ ├── src/ +│ │ │ ├── mtcute-adapter.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ └── examples/ # Example bots +│ ├── src/ +│ │ ├── example-bot.tsx +│ │ ├── quiz-bot.tsx +│ │ └── ... +│ └── package.json +│ +└── package.json # Root workspace configuration ``` -## 📚 API Reference - -### MtcuteAdapter -```tsx -const adapter = new MtcuteAdapter({ - apiId: number, - apiHash: string, - botToken: string, - storage?: string -}); - -// Register command handlers -adapter.onCommand('start', (ctx) => ); - -// Start the bot -await adapter.start(); -``` - -### Supported Elements -- ``, ``, ``, `` - Text formatting -- ``, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Quotes +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## 🎯 Advanced Examples + +### Todo List Bot ```tsx -const TodoApp = () => { - const [todos, setTodos] = useState(['Buy milk', 'Learn React']); +const TodoBot = () => { + const [todos, setTodos] = useState([]); return ( <> 📝 Todo List - {todos.map(todo => <>{todo}{'\n'}>)} - setTodos([...todos, 'New task'])}> - ➕ Add Task - + + + {todos.length === 0 ? ( + No todos yet! + ) : ( + todos.map((todo, i) => ( + <> + {i + 1}. {todo} + + > + )) + )} + + + setTodos([...todos, `Task ${todos.length + 1}`])}> + ➕ Add Task + + setTodos(todos.slice(0, -1))}> + ➖ Remove Last + + > ); }; ``` -### Rich Text Formatting -Support for all Telegram formatting features: - +### Input Handling ```tsx -<> - Bold and italic text - inline code - Code blocks - Quotes - Hidden text - Links -> +const InputBot = () => { + const [name, setName] = useState(''); + + return ( + <> + What's your name? + + + {name && <>Hello, {name}!>} + setName(text)} + autoDelete // Automatically delete user's message + /> + > + ); +}; ``` -### Efficient Message Updates -The bot automatically uses Telegram's edit message API for updates instead of sending multiple messages: +## 🏗️ Project Structure -- ✅ First render: Sends new message -- ✅ State changes: Edits existing message -- ✅ No message spam in chats - -## 🛠️ Built With - -- **[MTCute](https://mtcute.dev/)** - Modern Telegram client library -- **[React](https://react.dev/)** - UI library with custom reconciler -- **[Bun](https://bun.sh/)** - Fast JavaScript runtime and package manager -- **[TypeScript](https://typescriptlang.org/)** - Type safety - -## 📖 How It Works - -This project implements a custom React reconciler that translates React components into Telegram messages: - -1. **React Components** → **Virtual DOM Tree** -2. **Custom Reconciler** → **Telegram Message Structure** -3. **Message Renderer** → **Rich Text + Inline Keyboards** -4. **State Updates** → **Message Edits** - -The reconciler handles: -- Text formatting (``, ``, ``, etc.) -- Interactive buttons with click handlers -- Message layout with rows and columns -- Efficient updates via message editing - -## 🎮 Example Bots Included - -### 🔢 Counter Bot -Interactive counter with increment/decrement buttons - -### 📝 Todo List Bot -Full todo list manager with add/remove/toggle functionality - -### ❓ Help Bot -Multi-section help system with navigation - -### 🎨 Formatting Demo -Showcase of all supported Telegram formatting features - -## 🚀 Getting Started - -### Prerequisites -- [Bun](https://bun.sh/) installed -- Telegram Bot Token from [@BotFather](https://t.me/botfather) -- Telegram API credentials from [my.telegram.org](https://my.telegram.org) - -### Environment Setup -```bash -# Required environment variables -API_ID=your_api_id -API_HASH=your_api_hash -BOT_TOKEN=your_bot_token -STORAGE_PATH=.mtcute # Optional ``` - -### Development -```bash -# Run with hot reload -bun --hot src/example-bot.tsx - -# Run tests -bun test - -# Type check -bun run tsc --noEmit +react-telegram/ +├── packages/ +│ ├── core/ # Core React reconciler +│ │ ├── src/ +│ │ │ ├── reconciler.ts +│ │ │ ├── jsx.d.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ ├── mtcute-adapter/ # MTCute Telegram adapter +│ │ ├── src/ +│ │ │ ├── mtcute-adapter.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ └── examples/ # Example bots +│ ├── src/ +│ │ ├── example-bot.tsx +│ │ ├── quiz-bot.tsx +│ │ └── ... +│ └── package.json +│ +└── package.json # Root workspace configuration ``` -## 📚 API Reference - -### MtcuteAdapter -```tsx -const adapter = new MtcuteAdapter({ - apiId: number, - apiHash: string, - botToken: string, - storage?: string -}); - -// Register command handlers -adapter.onCommand('start', (ctx) => ); - -// Start the bot -await adapter.start(); -``` - -### Supported Elements -- ``, ``, ``, `` - Text formatting -- ``, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
inline code
Code blocks
Quotes
`, etc.) -- Interactive buttons with click handlers -- Message layout with rows and columns -- Efficient updates via message editing - -## 🎮 Example Bots Included - -### 🔢 Counter Bot -Interactive counter with increment/decrement buttons - -### 📝 Todo List Bot -Full todo list manager with add/remove/toggle functionality - -### ❓ Help Bot -Multi-section help system with navigation - -### 🎨 Formatting Demo -Showcase of all supported Telegram formatting features - -## 🚀 Getting Started - -### Prerequisites -- [Bun](https://bun.sh/) installed -- Telegram Bot Token from [@BotFather](https://t.me/botfather) -- Telegram API credentials from [my.telegram.org](https://my.telegram.org) - -### Environment Setup -```bash -# Required environment variables -API_ID=your_api_id -API_HASH=your_api_hash -BOT_TOKEN=your_bot_token -STORAGE_PATH=.mtcute # Optional ``` - -### Development -```bash -# Run with hot reload -bun --hot src/example-bot.tsx - -# Run tests -bun test - -# Type check -bun run tsc --noEmit +react-telegram/ +├── packages/ +│ ├── core/ # Core React reconciler +│ │ ├── src/ +│ │ │ ├── reconciler.ts +│ │ │ ├── jsx.d.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ ├── mtcute-adapter/ # MTCute Telegram adapter +│ │ ├── src/ +│ │ │ ├── mtcute-adapter.ts +│ │ │ └── index.ts +│ │ └── package.json +│ │ +│ └── examples/ # Example bots +│ ├── src/ +│ │ ├── example-bot.tsx +│ │ ├── quiz-bot.tsx +│ │ └── ... +│ └── package.json +│ +└── package.json # Root workspace configuration ``` -## 📚 API Reference - -### MtcuteAdapter -```tsx -const adapter = new MtcuteAdapter({ - apiId: number, - apiHash: string, - botToken: string, - storage?: string -}); - -// Register command handlers -adapter.onCommand('start', (ctx) => ); - -// Start the bot -await adapter.start(); -``` - -### Supported Elements -- ``, ``, ``, `` - Text formatting -- ``, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
`, `` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Code formatting -- `` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Quotes -- `` - Spoiler text -- `` - Links -- `` - Interactive buttons -- `` - Button layout - ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. -1. Fork the project -2. Create your feature branch (`git checkout -b feature/AmazingFeature`) -3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the branch (`git push origin feature/AmazingFeature`) +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## 📄 License @@ -214,12 +233,12 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## 🙏 Acknowledgments -- [MTCute](https://mtcute.dev/) for the excellent Telegram client library -- [React team](https://react.dev/) for the reconciler architecture -- [Bun team](https://bun.sh/) for the amazing runtime +- [MTCute](https://mtcute.dev/) for the Telegram client library +- [React](https://react.dev/) for the component model +- [Bun](https://bun.sh/) for the fast runtime --- -**[⭐ Star this repo](https://github.com/your-username/react-telegram-bot)** if you find it useful! +**[⭐ Star this repo](https://github.com/your-username/react-telegram)** if you find it useful! -Built with ❤️ and React +Built with ❤️ and React \ No newline at end of file diff --git a/bun.lock b/bun.lock index f793ed7..bc0b265 100644 --- a/bun.lock +++ b/bun.lock @@ -3,75 +3,61 @@ "workspaces": { "": { "name": "react-telegram", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@react-telegram/core", + "version": "0.1.0", "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", "react": "^19.1.0", "react-reconciler": "^0.32.0", }, "devDependencies": { - "@types/bun": "latest", "@types/react": "npm:pure-react-types@^0.1.4", "@types/react-reconciler": "^0.32.0", - "vitest": "^3.2.4", + "typescript": "^5", }, "peerDependencies": { + "react": ">=18.0.0", + }, + }, + "packages/examples": { + "name": "@react-telegram/examples", + "version": "0.1.0", + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", "typescript": "^5", }, }, + "packages/mtcute-adapter": { + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0", + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5", + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0", + }, + }, }, "packages": { - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], - "@fuman/bun": ["@fuman/bun@0.0.15", "", { "dependencies": { "@fuman/io": "^0.0.15", "@fuman/net": "^0.0.15", "@fuman/utils": "^0.0.15" } }, "sha512-+NpJgXPVMsGf7t78PXTIA84a/DBCiN+/E51fEJUcI/JYr9wNn4LPlLZilIvFXluTWowgOXveC+AMg0dtCdsFAQ=="], "@fuman/io": ["@fuman/io@0.0.15", "", { "dependencies": { "@fuman/utils": "^0.0.15" } }, "sha512-e/WbbWKXUp5NUIP4uMx3u9tr2leXnLoo8tCu0HopgoWcdydS5Y4U/75hhm5MqBSZQXWgICzJuvz21O5J6/kTPA=="], @@ -80,8 +66,6 @@ "@fuman/utils": ["@fuman/utils@0.0.15", "", {}, "sha512-3H3WzkfG7iLKCa/yNV4s80lYD4yr5hgiNzU13ysLY2BcDqFjM08XGYuLd5wFVp4V8+DA/fe8gIDW96To/JwDyA=="], - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], - "@mtcute/bun": ["@mtcute/bun@0.24.3", "", { "dependencies": { "@fuman/bun": "0.0.15", "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/core": "^0.24.3", "@mtcute/html-parser": "^0.24.0", "@mtcute/markdown-parser": "^0.24.0", "@mtcute/wasm": "^0.24.3" } }, "sha512-voNLlACw7Su8+DKCiiBRi8F1yjl3r/AulUW8KFncPMILXo2H94qNengVg9KXUYx1mTLhJIqfRM3ouuuQQna9Lg=="], "@mtcute/core": ["@mtcute/core@0.24.4", "", { "dependencies": { "@fuman/io": "0.0.15", "@fuman/net": "0.0.15", "@fuman/utils": "0.0.15", "@mtcute/file-id": "^0.24.3", "@mtcute/tl": "^204.0.0", "@mtcute/tl-runtime": "^0.24.3", "@types/events": "3.0.0", "long": "5.2.3" } }, "sha512-4dQ1MhY1DmEUqpOLyssgv9WljDr/itpUTVanTg6pZsraU6Z+ohH4E3V2UOQcfmWCQbecmMZL7UeLlScfw0oDXQ=="], @@ -100,54 +84,14 @@ "@mtcute/wasm": ["@mtcute/wasm@0.24.3", "", {}, "sha512-6kI9r+g/4P6Dg6A1K1O0DC6KLIQp/ABeOxSWzBfH3YM2HR6Vc8RO/M0/6Ij7GwCvELNGAz9AWj/Qk8vM8c0ATg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + "@react-telegram/core": ["@react-telegram/core@workspace:packages/core"], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + "@react-telegram/examples": ["@react-telegram/examples@workspace:packages/examples"], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@react-telegram/mtcute-adapter": ["@react-telegram/mtcute-adapter@workspace:packages/mtcute-adapter"], "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], - "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/events": ["@types/events@3.0.0", "", {}, "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="], "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], @@ -156,34 +100,8 @@ "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], - - "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - - "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], - - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - - "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], - "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], - - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], - - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -194,84 +112,20 @@ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], - - "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], - "long": ["long@5.2.3", "", {}, "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], - - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="], - "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], - "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], - - "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], - - "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], - "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], - - "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - - "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], } } diff --git a/package.json b/package.json index 851da59..b29a8ba 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,19 @@ { - "name": "react-telegram", - "module": "index.ts", + "name": "react-telegram-monorepo", "type": "module", "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "build": "bun run build:core && bun run build:adapter", + "build:core": "cd packages/core && bun run build", + "build:adapter": "cd packages/mtcute-adapter && bun run build", + "test": "bun test", + "example": "cd packages/examples && bun run start" + }, "devDependencies": { "@types/bun": "latest", - "@types/react-reconciler": "^0.32.0", - "@types/react": "npm:pure-react-types@^0.1.4", - "vitest": "^3.2.4" - }, - "peerDependencies": { "typescript": "^5" - }, - "dependencies": { - "@mtcute/bun": "^0.24.3", - "@mtcute/dispatcher": "^0.24.3", - "@mtcute/html-parser": "^0.24.0", - "react": "^19.1.0", - "react-reconciler": "^0.32.0" } } diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..a722c59 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,58 @@ +# @react-telegram/core + +Core React reconciler for building Telegram bots with React components. + +## Installation + +```bash +bun add @react-telegram/core react +``` + +## Usage + +```tsx +import { createContainer } from '@react-telegram/core'; + +const { render, container } = createContainer(); + +const App = () => ( + <> + Hello Telegram! + + Built with React + > +); + +render(); +console.log(container.root); +``` + +## Supported Elements + +### Text Formatting +- ``, `` - Bold text +- ``, `` - Italic text +- ``, `` - Underlined text +- ``, ``, `` - Strikethrough text +- `` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Inline code +- `` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Code blocks +- `` - Line breaks + +### Telegram-specific +- `` - Spoiler text +- `` - Custom emoji +- `` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
` - Quotes (with optional `expandable` prop) +- `` - Links + +### Interactive Elements +- `` - Inline keyboard buttons +- `` - Button row container +- `` - Text input handler + +## TypeScript Support + +This package includes full TypeScript definitions. The JSX elements are automatically typed when you import the package. + +## License + +MIT \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..0f56525 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,55 @@ +{ + "name": "@react-telegram/core", + "version": "0.1.0", + "description": "Core React reconciler for Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "@types/react-reconciler": "^0.32.0", + "typescript": "^5" + }, + "peerDependencies": { + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "reconciler", + "react-telegram" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..4947c34 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,19 @@ +export { + createContainer, + TelegramReconciler, + type TextNode, + type FormattedNode, + type LinkNode, + type EmojiNode, + type CodeBlockNode, + type BlockQuoteNode, + type ButtonNode, + type RowNode, + type InputNode, + type RootNode, + type Node, + type InputCallbackData +} from './reconciler'; + +// Re-export JSX types +export type {} from './jsx'; \ No newline at end of file diff --git a/src/jsx.d.ts b/packages/core/src/jsx.d.ts similarity index 100% rename from src/jsx.d.ts rename to packages/core/src/jsx.d.ts diff --git a/src/reconciler.ts b/packages/core/src/reconciler.ts similarity index 100% rename from src/reconciler.ts rename to packages/core/src/reconciler.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/packages/examples/package.json b/packages/examples/package.json new file mode 100644 index 0000000..0e4aa7c --- /dev/null +++ b/packages/examples/package.json @@ -0,0 +1,25 @@ +{ + "name": "@react-telegram/examples", + "version": "0.1.0", + "private": true, + "description": "Examples for React Telegram", + "type": "module", + "scripts": { + "start": "bun run src/example-bot.tsx", + "counter": "bun run src/example.tsx", + "typed": "bun run src/typed-example.tsx", + "input": "bun run src/input-example.tsx", + "quiz": "bun run src/quiz-bot.tsx", + "autodelete": "bun run src/input-autodelete-demo.tsx", + "br-demo": "bun run src/br-demo.tsx" + }, + "dependencies": { + "@react-telegram/core": "workspace:*", + "@react-telegram/mtcute-adapter": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/br-demo.tsx b/packages/examples/src/br-demo.tsx similarity index 88% rename from src/br-demo.tsx rename to packages/examples/src/br-demo.tsx index c4251a4..a3c4917 100644 --- a/src/br-demo.tsx +++ b/packages/examples/src/br-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Demo showcasing tag support const BrDemo: React.FC = () => { diff --git a/src/example-bot.tsx b/packages/examples/src/example-bot.tsx similarity index 97% rename from src/example-bot.tsx rename to packages/examples/src/example-bot.tsx index ea820ea..dba5f85 100644 --- a/src/example-bot.tsx +++ b/packages/examples/src/example-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from './mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Example React components for Telegram const CounterApp = () => { diff --git a/src/example.tsx b/packages/examples/src/example.tsx similarity index 90% rename from src/example.tsx rename to packages/examples/src/example.tsx index a18e346..3946273 100644 --- a/src/example.tsx +++ b/packages/examples/src/example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; // Create container and render const { container, render, clickButton } = createContainer(); diff --git a/src/examples/input-autodelete-demo.tsx b/packages/examples/src/input-autodelete-demo.tsx similarity index 94% rename from src/examples/input-autodelete-demo.tsx rename to packages/examples/src/input-autodelete-demo.tsx index 49588ef..adcebef 100644 --- a/src/examples/input-autodelete-demo.tsx +++ b/packages/examples/src/input-autodelete-demo.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; const AutoDeleteDemo: React.FC = () => { const [mode, setMode] = useState<'normal' | 'secret'>('normal'); diff --git a/src/input-example.tsx b/packages/examples/src/input-example.tsx similarity index 94% rename from src/input-example.tsx rename to packages/examples/src/input-example.tsx index 17ff5b9..7949449 100644 --- a/src/input-example.tsx +++ b/packages/examples/src/input-example.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; +import { createContainer } from '@react-telegram/core'; const InputExample: React.FC = () => { const [messages, setMessages] = useState([]); diff --git a/src/examples/quiz-bot.tsx b/packages/examples/src/quiz-bot.tsx similarity index 96% rename from src/examples/quiz-bot.tsx rename to packages/examples/src/quiz-bot.tsx index 7a85d45..2631f49 100644 --- a/src/examples/quiz-bot.tsx +++ b/packages/examples/src/quiz-bot.tsx @@ -1,6 +1,6 @@ -/// +/// import React, { useState } from 'react'; -import { MtcuteAdapter } from '../mtcute-adapter'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; // Quiz questions const questions = [ diff --git a/src/typed-example.tsx b/packages/examples/src/typed-example.tsx similarity index 94% rename from src/typed-example.tsx rename to packages/examples/src/typed-example.tsx index 25c4776..d8eef53 100644 --- a/src/typed-example.tsx +++ b/packages/examples/src/typed-example.tsx @@ -1,7 +1,7 @@ -/// +/// import React, { useState } from 'react'; -import { createContainer } from './reconciler'; -import type { RootNode } from './reconciler'; +import { createContainer } from '@react-telegram/core'; +import type { RootNode } from '@react-telegram/core'; // This example demonstrates TypeScript support with custom JSX elements diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json new file mode 100644 index 0000000..71135b5 --- /dev/null +++ b/packages/examples/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/mtcute-adapter/README.md b/packages/mtcute-adapter/README.md new file mode 100644 index 0000000..f3acb2a --- /dev/null +++ b/packages/mtcute-adapter/README.md @@ -0,0 +1,76 @@ +# @react-telegram/mtcute-adapter + +MTCute adapter for React Telegram bots. This package provides the integration between React Telegram's core reconciler and the MTCute Telegram client library. + +## Installation + +```bash +bun add @react-telegram/mtcute-adapter @react-telegram/core react +``` + +## Usage + +```tsx +import React from 'react'; +import { MtcuteAdapter } from '@react-telegram/mtcute-adapter'; + +const Bot = () => ( + <> + Hello from React Telegram! + + + + console.log('clicked!')}>Click me + + > +); + +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + adapter.onCommand('start', () => ); + + await adapter.start(process.env.BOT_TOKEN!); + console.log('Bot is running!'); +} + +main().catch(console.error); +``` + +## Features + +- Full React component support for Telegram messages +- Automatic message updates when state changes +- Button click handling +- Text input support with auto-delete option +- Command handling +- TypeScript support + +## API + +### MtcuteAdapter + +```typescript +const adapter = new MtcuteAdapter({ + apiId: number, + apiHash: string, + botToken: string, + storage?: string // Default: '.mtcute' +}); +``` + +### Methods + +- `onCommand(command: string, handler: (ctx) => ReactElement)` - Register a command handler +- `start(botToken: string)` - Start the bot +- `sendReactMessage(chatId: number | string, app: ReactElement)` - Send a React-powered message +- `getClient()` - Get the underlying MTCute client +- `getDispatcher()` - Get the MTCute dispatcher + +## License + +MIT \ No newline at end of file diff --git a/packages/mtcute-adapter/package.json b/packages/mtcute-adapter/package.json new file mode 100644 index 0000000..a924b76 --- /dev/null +++ b/packages/mtcute-adapter/package.json @@ -0,0 +1,58 @@ +{ + "name": "@react-telegram/mtcute-adapter", + "version": "0.1.0", + "description": "MTCute adapter for React Telegram bots", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts", + "bun": "./src/index.ts" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "bun run clean && bun run build:types", + "build:types": "tsc", + "clean": "rm -rf dist", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@mtcute/bun": "^0.24.3", + "@mtcute/dispatcher": "^0.24.3", + "@mtcute/tl": "*", + "@react-telegram/core": "workspace:*", + "react": "^19.1.0" + }, + "devDependencies": { + "@types/react": "npm:pure-react-types@^0.1.4", + "typescript": "^5" + }, + "peerDependencies": { + "@react-telegram/core": "^0.1.0", + "react": ">=18.0.0" + }, + "keywords": [ + "react", + "telegram", + "bot", + "mtcute", + "adapter" + ], + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/your-username/react-telegram.git" + }, + "bugs": { + "url": "https://github.com/your-username/react-telegram/issues" + }, + "homepage": "https://github.com/your-username/react-telegram#readme" +} \ No newline at end of file diff --git a/packages/mtcute-adapter/src/index.ts b/packages/mtcute-adapter/src/index.ts new file mode 100644 index 0000000..f49da89 --- /dev/null +++ b/packages/mtcute-adapter/src/index.ts @@ -0,0 +1 @@ +export { MtcuteAdapter, type MtcuteAdapterConfig } from './mtcute-adapter'; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/packages/mtcute-adapter/src/mtcute-adapter.ts similarity index 99% rename from src/mtcute-adapter.ts rename to packages/mtcute-adapter/src/mtcute-adapter.ts index e83dbb0..7e56de1 100644 --- a/src/mtcute-adapter.ts +++ b/packages/mtcute-adapter/src/mtcute-adapter.ts @@ -11,8 +11,8 @@ import type { CodeBlockNode, BlockQuoteNode, RowNode -} from './reconciler'; -import { createContainer } from './reconciler'; +} from '@react-telegram/core'; +import { createContainer } from '@react-telegram/core'; import type { ReactElement } from 'react'; export interface MtcuteAdapterConfig { diff --git a/packages/mtcute-adapter/tsconfig.json b/packages/mtcute-adapter/tsconfig.json new file mode 100644 index 0000000..27f7bfb --- /dev/null +++ b/packages/mtcute-adapter/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020"], + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "types": ["bun-types"], + "composite": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 32b0bc2..0000000 --- a/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export { createContainer } from './reconciler'; -export type { - TextNode, - FormattedNode, - LinkNode, - EmojiNode, - CodeBlockNode, - BlockQuoteNode, - ButtonNode, - RowNode, - RootNode, - Node -} from './reconciler'; - -// Also export the Telegram-prefixed types from jsx.d.ts -export type { - TelegramTextNode, - TelegramFormattedNode, - TelegramLinkNode, - TelegramEmojiNode, - TelegramCodeBlockNode, - TelegramBlockQuoteNode, - TelegramButtonNode, - TelegramRowNode, - TelegramRootNode, - TelegramNode -} from './jsx'; \ No newline at end of file diff --git a/src/mtcute-adapter.test.tsx b/src/mtcute-adapter.test.tsx deleted file mode 100644 index 6d56e66..0000000 --- a/src/mtcute-adapter.test.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('MtcuteAdapter Integration Tests', () => { - it('should generate correct structure for a Telegram message', async () => { - const { container, render } = createContainer(); - - const App = () => ( - <> - Bold text - {' normal '} - italic - {'\n'} - Link - {'\n'} - - {}}>Yes - {}}>No - - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check text nodes (bold, ' normal ', italic, '\n', link, '\n') - const textNodes = container.root.children.filter(n => n.type !== 'row'); - expect(textNodes).toHaveLength(6); - - // Check button row - const rows = container.root.children.filter(n => n.type === 'row'); - expect(rows).toHaveLength(1); - expect(rows[0]?.type).toBe('row'); - if (rows[0]?.type === 'row') { - expect(rows[0].children).toHaveLength(2); - expect(rows[0].children[0]?.text).toBe('Yes'); - expect(rows[0].children[1]?.text).toBe('No'); - } - }); - - it('should handle interactive state changes', async () => { - const { container, render, clickButton } = createContainer(); - - const CounterApp = () => { - const [count, setCount] = useState(0); - - return ( - <> - Count: {count} - {'\n'} - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - // Click button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check updated state - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - }); - - it('should handle all Telegram formatting types', async () => { - const { container, render } = createContainer(); - - const FormattingApp = () => ( - <> - Bold - {'\n'} - Italic - {'\n'} - Underline - {'\n'} - Strikethrough - {'\n'} - Spoiler - {'\n'} - code - {'\n'} - code block - {'\n'} - Quote - {'\n'} - 👍 - > - ); - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - const formattedNodes = container.root.children.filter(n => n.type === 'formatted'); - const otherNodes = container.root.children.filter(n => n.type !== 'formatted' && n.type !== 'text'); - - // Check we have all formatting types - expect(formattedNodes).toHaveLength(6); // bold, italic, underline, strikethrough, spoiler, code - expect(otherNodes).toHaveLength(3); // codeblock, blockquote, emoji - - // Check specific nodes - const emoji = otherNodes.find(n => n.type === 'emoji'); - expect(emoji?.type).toBe('emoji'); - if (emoji?.type === 'emoji') { - expect(emoji.emojiId).toBe('123456'); - expect(emoji.fallback).toBe('👍'); - } - - const blockquote = otherNodes.find(n => n.type === 'blockquote'); - expect(blockquote?.type).toBe('blockquote'); - if (blockquote?.type === 'blockquote') { - expect(blockquote.expandable).toBe(true); - } - }); -}); \ No newline at end of file diff --git a/src/reconciler-persistence.test.tsx b/src/reconciler-persistence.test.tsx deleted file mode 100644 index d5500fb..0000000 --- a/src/reconciler-persistence.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -/// -import { describe, it, expect } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler - Persistence Mode', () => { - it('should preserve static content in formatted elements during re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - - return ( - <> - Static bold text - Count: {count} - Static quote content - - setCount(c => c + 1)}>Increment - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Check initial render - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '0' } - ] - }); - - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - - // Click button to trigger re-render - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Bold text should still have its content - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'Static bold text' }] - }); - - // Italic should update count but keep structure - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [ - { type: 'text', content: 'Count: ' }, - { type: 'text', content: '1' } - ] - }); - - // Blockquote should still have its content - expect(container.root.children[2]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Static quote content' }] - }); - }); - - it('should handle mixed static and dynamic content', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [show, setShow] = useState(true); - - return ( - <> - - Always here - {show && ' - Dynamic part'} - - - setShow(s => !s)}>Toggle - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state with dynamic part - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - - // Toggle to hide dynamic part - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should only have static part now - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' } - ] - }); - - // Toggle back - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should have both parts again - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'Always here' }, - { type: 'text', content: ' - Dynamic part' } - ] - }); - }); -}); \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx deleted file mode 100644 index 3cfe399..0000000 --- a/src/reconciler.test.tsx +++ /dev/null @@ -1,383 +0,0 @@ -/// -import { describe, it, expect, vi } from 'vitest'; -import React, { useState } from 'react'; -import { createContainer } from './reconciler'; - -describe('Telegram Reconciler', () => { - it('should render text formatting', async () => { - const { container, render } = createContainer(); - - render( - <> - bold - italic - underline - strikethrough - spoiler - > - ); - - // Wait for React to finish rendering - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(5); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [{ type: 'text', content: 'bold' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic' }] - }); - }); - - it('should render nested formatting', async () => { - const { container, render } = createContainer(); - - render( - - bold italic bold bold - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'bold', - children: [ - { type: 'text', content: 'bold ' }, - { - type: 'formatted', - format: 'italic', - children: [{ type: 'text', content: 'italic bold' }] - }, - { type: 'text', content: ' bold' } - ] - }); - }); - - it('should render links', async () => { - const { container, render } = createContainer(); - - render( - <> - inline URL - inline mention - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'link', - href: 'http://www.example.com/', - children: [{ type: 'text', content: 'inline URL' }] - }); - }); - - it('should render emoji', async () => { - const { container, render } = createContainer(); - - render( - 👍 - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - expect(container.root.children[0]).toEqual({ - type: 'emoji', - emojiId: '5368324170671202286', - fallback: '👍' - }); - }); - - it('should render code blocks', async () => { - const { container, render } = createContainer(); - - render( - <> - inline code - pre-formatted code - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'formatted', - format: 'code', - children: [{ type: 'text', content: 'inline code' }] - }); - expect(container.root.children[1]).toEqual({ - type: 'codeblock', - content: 'pre-formatted code', - language: undefined - }); - }); - - it('should render blockquotes', async () => { - const { container, render } = createContainer(); - - render( - <> - Regular quote - Expandable quote - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Regular quote' }], - expandable: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'blockquote', - children: [{ type: 'text', content: 'Expandable quote' }], - expandable: true - }); - }); - - it('should render buttons with IDs based on position', async () => { - const { container, render } = createContainer(); - const onClick1 = vi.fn(); - const onClick2 = vi.fn(); - - render( - - Button 1 - Button 2 - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(1); - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children).toHaveLength(2); - expect(row.children[0]?.id).toBe('0-0'); - expect(row.children[1]?.id).toBe('0-1'); - } - }); - - it('should handle button clicks', async () => { - const { render, clickButton } = createContainer(); - const onClick = vi.fn(); - - render( - - Click me - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - clickButton('0-0'); - expect(onClick).toHaveBeenCalledTimes(1); - }); - - it('should handle buttons with complex children', async () => { - const { container, render } = createContainer(); - const onClick = vi.fn(); - const mode = 'normal'; - - render( - - - Switch to {mode === 'normal' ? 'Secret' : 'Normal'} Mode - - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Switch to Secret Mode'); - } - }); - - it('should handle buttons with array children', async () => { - const { container, render } = createContainer(); - - render( - - {'Hello'}{' '}{'World'} - {['One', ' ', 'Two', ' ', 'Three']} - {123} items - - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - const row = container.root.children[0]; - expect(row?.type).toBe('row'); - if (row?.type === 'row') { - expect(row.children[0]?.text).toBe('Hello World'); - expect(row.children[1]?.text).toBe('One Two Three'); - expect(row.children[2]?.text).toBe('123 items'); - } - }); - - it('should work with React state', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [count, setCount] = useState(0); - return ( - <> - count {count} - - setCount(p => p - 1)}>Decrease - setCount(p => p + 1)}>Increase - - > - ); - }; - - render(); - - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - React creates separate text nodes - expect(container.root.children[0]).toEqual({ - type: 'text', - content: 'count ' - }); - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - - // The row is the third child (index 2) - expect(container.root.children[2]?.type).toBe('row'); - - // Click increase button - clickButton('0-1'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // After re-render, the text should update - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '1' - }); - - // Click decrease button - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children[1]).toEqual({ - type: 'text', - content: '0' - }); - }); - - it('should handle input elements', async () => { - const { container, render } = createContainer(); - const onSubmit = vi.fn(); - - render( - <> - - - > - ); - - await new Promise(resolve => setTimeout(resolve, 0)); - - expect(container.root.children).toHaveLength(2); - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.root.children[1]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - - // Check input callbacks - expect(container.inputCallbacks).toHaveLength(2); - expect(container.inputCallbacks[0]).toEqual({ - callback: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks[1]).toEqual({ - callback: expect.any(Function), - autoDelete: true - }); - - // Test callback execution - container.inputCallbacks[0]?.callback('test message'); - expect(onSubmit).toHaveBeenCalledWith('test message'); - }); - - it('should preserve input elements across re-renders', async () => { - const { container, render, clickButton } = createContainer(); - - const App = () => { - const [mode, setMode] = useState<'normal' | 'secret'>('normal'); - const handleNormal = vi.fn(); - const handleSecret = vi.fn(); - - return ( - <> - {mode === 'normal' ? ( - - ) : ( - - )} - - setMode(m => m === 'normal' ? 'secret' : 'normal')}> - Toggle - - - > - ); - }; - - render(); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Initial state - normal mode - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: undefined - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBeUndefined(); - - // Toggle to secret mode - clickButton('0-0'); - await new Promise(resolve => setTimeout(resolve, 0)); - - // Should still have input but with different props - expect(container.root.children[0]).toEqual({ - type: 'input', - onSubmit: expect.any(Function), - autoDelete: true - }); - expect(container.inputCallbacks).toHaveLength(1); - expect(container.inputCallbacks[0]?.autoDelete).toBe(true); - - // Verify the callback works - container.inputCallbacks[0]?.callback('test'); - - // Verify the structure is correct - expect(container.inputCallbacks[0]).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d167589..0fd7aab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,5 +28,10 @@ // Types "types": ["bun-types", "pure-react-types"] - } + }, + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/mtcute-adapter" }, + { "path": "./packages/examples" } + ] }
code
code block
Quote
Static quote content
pre-formatted code
Regular quote
Expandable quote