### 📱 تاثیر در UI: - iOS: Cupertino-style transitions - Android: Material ripples - Safe area handling مختلف - Font rendering --- ## 📦 ۶. JSON کامل Initial Config این JSON باید **یکبار هنگام load WebView** ارسال شود: ```json { "version": "1.0.0", "timestamp": 1234567890, "layout": { "breakpoint": "mobile", "screenWidth": 375, "screenHeight": 812, "isMobile": true, "isTablet": false, "isDesktop": false }, "safeArea": { "top": 44, "bottom": 34, "left": 0, "right": 0 }, "viewInsets": { "top": 0, "bottom": 0, "left": 0, "right": 0 }, "designSize": { "mobile": { "width": 375, "height": 814 }, "tablet": { "width": 834, "height": 1194 } }, "platform": { "os": "ios", "version": "17.2", "model": "iPhone 14 Pro", "isPhysicalDevice": true, "hasNotch": true, "hasDynamicIsland": true }, "locale": { "languageCode": "en", "countryCode": "US", "isRtl": false }, "theme": { "mode": "light", "primaryColor": "#FFB236" } } ``` --- ## 🔄 ۷. Events که باید به WebView ارسال شوند ### ۱. Layout Changed ```json { "type": "LAYOUT_CHANGED", "payload": { "breakpoint": "tablet", "screenWidth": 820, "isMobile": false } } ``` ### ۲. Keyboard Changed ```json { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336, "animationDuration": 250 } } ``` ### ۳. Safe Area Changed ```json { "type": "SAFE_AREA_CHANGED", "payload": { "top": 0, "bottom": 0, "left": 0, "right": 0 } } ``` ### ۴. Orientation Changed ```json { "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", "screenWidth": 812, "screenHeight": 375 } } ``` --- --- ## 💻 کد Flutter برای ارسال اطلاعات به WebView ```dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class TalkWebViewBridge { final WebViewController controller; BuildContext context; TalkWebViewBridge(this.controller, this.context) { _sendInitialConfig(); _listenToChanges(); } // ارسال اولیه config void _sendInitialConfig() { final config = jsonEncode({ 'version': '1.0.0', 'timestamp': DateTime.now().millisecondsSinceEpoch, 'layout': { 'breakpoint': _getBreakpoint(), 'screenWidth': MediaQuery.of(context).size.width, 'screenHeight': MediaQuery.of(context).size.height, 'isMobile': context.isMobile, 'isTablet': context.isTablet, 'isDesktop': context.isDesktop, }, 'safeArea': { 'top': MediaQuery.of(context).padding.top, 'bottom': MediaQuery.of(context).padding.bottom, 'left': MediaQuery.of(context).padding.left, 'right': MediaQuery.of(context).padding.right, }, 'viewInsets': { 'top': MediaQuery.of(context).viewInsets.top, 'bottom': MediaQuery.of(context).viewInsets.bottom, 'left': MediaQuery.of(context).viewInsets.left, 'right': MediaQuery.of(context).viewInsets.right, }, 'designSize': { 'mobile': {'width': 375, 'height': 814}, 'tablet': {'width': 834, 'height': 1194}, }, 'platform': { 'os': Platform.operatingSystem, 'version': Platform.operatingSystemVersion, }, 'locale': { 'languageCode': context.languageCode, 'isRtl': context.isRtl, }, }); controller.runJavaScript(''' window.dispatchEvent(new CustomEvent('flutterConfig', { detail: $config })); '''); } // لیسن به تغییرات keyboard void _listenToChanges() { // در هر frame چک می‌کنیم که آیا viewInsets تغییر کرده WidgetsBinding.instance.addPostFrameCallback((_) { final currentInsets = MediaQuery.of(context).viewInsets; if (_lastInsets?.bottom != currentInsets.bottom) { _sendKeyboardChanged(currentInsets.bottom); _lastInsets = currentInsets; } _listenToChanges(); // تکرار }); } EdgeInsets? _lastInsets; void _sendKeyboardChanged(double height) { final event = jsonEncode({ 'type': 'KEYBOARD_CHANGED', 'payload': { 'visible': height > 0, 'height': height, 'animationDuration': 250, }, }); controller.runJavaScript(''' window.dispatchEvent(new CustomEvent('flutterEvent', { detail: $event })); '''); } String _getBreakpoint() { if (context.isMobile) return 'mobile'; if (context.isTablet) return 'tablet'; return 'desktop'; } } ``` 📱 توضیح کامل با مثال‌های واقعی 🎯 Initial Config - ارسال یکبار هنگام Load این JSON فقط یکبار زمانی که WebView لود می‌شود ارسال می‌شود تا React بداند در چه محیطی اجرا می‌شود. ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 1️⃣ layout - اطلاعات صفحه "layout": { "breakpoint": "mobile", // نوع دستگاه "screenWidth": 375, // عرض صفحه (پیکسل) "screenHeight": 812, // ارتفاع صفحه (پیکسل) "isMobile": true, "isTablet": false, "isDesktop": false } 📱 مثال عملی: // در React استفاده می‌شود برای: function ChatPage({ layout }) { return (
{/* محتوای چت */}
); } // نتیجه: // iPhone (mobile): padding = 16px // iPad (tablet): padding = 64px ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2️⃣ safeArea - فضای امن صفحه "safeArea": { "top": 44, // ارتفاع status bar / notch "bottom": 34, // ارتفاع home indicator (خط سفید iPhone X) "left": 0, "right": 0 } 📱 چرا مهم است؟ مثال عملی: // ❌ بدون safe area // نتیجه: دکمه زیر home indicator مخفی می‌شود! // ✅ با safe area // نتیجه: دکمه بالای home indicator نمایش داده می‌شود ✅ مثال واقعی از کد فلاتر: // فلاتر: bottom: 16 + context.padding.bottom, // 16 + 34 = 50px // باید در React شود: bottom: 16 + safeArea.bottom // 16 + 34 = 50px ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 3️⃣ viewInsets - فضای اشغال شده توسط سیستم "viewInsets": { "top": 0, "bottom": 0, // این عدد تغییر می‌کند! "left": 0, "right": 0 } 📱 کاربرد اصلی: Keyboard Height ┌─────────────────┐ │ Chat Messages │ ← محتوا │ │ │ │ ├─────────────────┤ │ TextField │ ← ورودی متن ├─────────────────┤ │ │ │ Keyboard │ ← viewInsets.bottom = 336px │ ⌨️ │ └─────────────────┘ مثال عملی: // هنگام load اولیه: viewInsets.bottom = 0 // کیبورد بسته است // کاربر روی TextField کلیک می‌کند: // Flutter event می‌فرستد: { "type": "KEYBOARD_CHANGED", "payload": { "bottom": 336 } } // React باید TextField را بالا ببرد:
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 4️⃣ designSize - اندازه طراحی Figma "designSize": { "mobile": { "width": 375, "height": 814 }, "tablet": { "width": 834, "height": 1194 } } 📱 کاربرد: Responsive Scaling // اگر طراح در Figma با عرض 375 طراحی کرده: const figmaWidth = 375; const figmaFontSize = 16; // دستگاه واقعی: const deviceWidth = 414; // iPhone 14 Pro Max // محاسبه font size: const scaleFactor = deviceWidth / figmaWidth; // 414 / 375 = 1.104 const actualFontSize = figmaFontSize * scaleFactor; // 16 * 1.104 = 17.7px // نتیجه: فونت در گوشی بزرگتر به نسبت اندازه صفحه بزرگتر می‌شود ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 5️⃣ platform - اطلاعات دستگاه "platform": { "os": "ios", // ios | android "version": "17.2", "model": "iPhone 14 Pro", "isPhysicalDevice": true, // true = گوشی واقعی، false = emulator "hasNotch": true, // دارای notch "hasDynamicIsland": true // دارای Dynamic Island } 📱 کاربرد: // مثال 1: تنظیم safe area مختلف if (platform.hasNotch) { safeAreaTop = 44; } else { safeAreaTop = 20; } // مثال 2: استایل مختلف برای iOS/Android // مثال 3: تشخیص Dynamic Island {platform.hasDynamicIsland && (
{/* فضا برای Dynamic Island */} {/* محتوا */}
)} ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 6️⃣ locale - تنظیمات زبان "locale": { "languageCode": "en", // en | fa | ar "countryCode": "US", "isRtl": false // راست به چپ؟ } 📱 کاربرد: // مثال 1: تنظیم direction
{/* محتوا */}
// مثال 2: متن مناسب زبان const messages = { en: "Hello", fa: "سلام", ar: "مرحبا" };

{messages[locale.languageCode]}

// مثال 3: فرمت تاریخ const date = new Date(); date.toLocaleDateString(locale.languageCode); // en-US | fa-IR ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 7️⃣ theme - تنظیمات تم "theme": { "mode": "light", // light | dark "primaryColor": "#FFB236" } 📱 کاربرد: // تنظیم رنگ‌ها const colors = { light: { bg: '#F5F5F5', text: '#202227' }, dark: { bg: '#1A1A1A', text: '#FFFFFF' } };
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 🔄 Events - ارسال Real-time این eventها هر بار که چیزی تغییر می‌کند ارسال می‌شوند. ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 1️⃣ SCREEN_SIZE_CHANGED - تغییر اندازه صفحه 📱 چه موقع رخ می‌دهد؟ ┌─────────────┐ ┌──────────────────────┐ │ Portrait │ │ Landscape │ │ │ │ │ │ iPhone │ چرخش گوشی │ iPhone │ │ │ ──────────> │ │ │ 📱 │ │ 📱 │ │ │ │ │ └─────────────┘ └──────────────────────┘ 375 × 812 812 × 375 Event ارسالی: { "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "height": 375, "orientation": "landscape", "breakpoint": "tablet" // ممکن است تغییر کند! } } React چه کار باید بکند: useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SCREEN_SIZE_CHANGED') { const { width, orientation, breakpoint } = event.data.payload; // 1. تغییر layout if (breakpoint === 'tablet') { setPadding(64); // در landscape = tablet } else { setPadding(16); // در portrait = mobile } // 2. تنظیم مجدد video player if (orientation === 'landscape') { setVideoPlayerFullWidth(true); } } }); }, []); مثال واقعی: کاربر گوشی را می‌چرخاند (portrait → landscape) → عرض صفحه: 375 → 812 → breakpoint تغییر می‌کند: mobile → tablet → padding چت باید تغییر کند: 16px → 64px ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 2️⃣ KEYBOARD_CHANGED - باز/بسته شدن کیبورد 📱 چه موقع رخ می‌دهد؟ ┌─────────────────┐ ┌─────────────────┐ │ Chat │ │ Chat │ │ Messages │ │ Messages │ │ │ │ (کمتر فضا) │ │ │ ├─────────────────┤ │ │ کلیک → │ TextField │ ├─────────────────┤ ├─────────────────┤ │ TextField │ │ Keyboard │ └─────────────────┘ │ ⌨️ │ └─────────────────┘ Event ارسالی: // کیبورد باز شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336, // ارتفاع کیبورد "animationDuration": 250 // مدت زمان انیمیشن (ms) } } // کیبورد بسته شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0, "animationDuration": 250 } } React چه کار باید بکند: const [keyboardHeight, setKeyboardHeight] = useState(0); useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'KEYBOARD_CHANGED') { const { visible, height, animationDuration } = event.data.payload; // با انیمیشن smooth setKeyboardHeight(visible ? height : 0); // Scroll به آخرین پیام if (visible) { setTimeout(() => { chatListRef.current?.scrollToBottom(); }, animationDuration); } } }); }, []); // در UI:
مثال واقعی: کاربر روی TextField کلیک می‌کند → کیبورد باز می‌شود (336px) → لیست چت باید 336px بالا برود → با انیمیشن smooth (250ms) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 3️⃣ SAFE_AREA_CHANGED - تغییر فضای امن 📱 چه موقع رخ می‌دهد؟ ┌─────────────────┐ ┌─────────────────┐ │ Status Bar 44px │ │ (مخفی شده) │ ├─────────────────┤ ├─────────────────┤ │ │ │ │ │ Full Screen │ تمام صفحه│ Video/Game │ │ Video │ ──────→ │ Full Screen │ │ │ │ │ ├─────────────────┤ ├─────────────────┤ │ Home Bar 34px │ │ (مخفی شده) │ └─────────────────┘ └─────────────────┘ Event ارسالی: // حالت عادی: { "type": "SAFE_AREA_CHANGED", "payload": { "top": 44, "bottom": 34, "left": 0, "right": 0 } } // حالت full screen (video player): { "type": "SAFE_AREA_CHANGED", "payload": { "top": 0, // status bar مخفی شد "bottom": 0, // home indicator مخفی شد "left": 0, "right": 0 } } React چه کار باید بکند: useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SAFE_AREA_CHANGED') { const { top, bottom } = event.data.payload; // اگر safe area = 0 → یعنی full screen شده if (top === 0 && bottom === 0) { setIsFullScreen(true); hideHeader(); hideBottomNav(); } else { setIsFullScreen(false); showHeader(); showBottomNav(); } } }); }, []); مثال واقعی: کاربر video call را full screen می‌کند → status bar و home indicator مخفی می‌شوند → safe area: top=44 → 0, bottom=34 → 0 → ویدیو تمام صفحه را می‌پوشاند ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 4️⃣ ORIENTATION_CHANGED - تغییر orientation 📱 چه موقع رخ می‌دهد؟ Portrait → Landscape → Portrait 📱 → 📱 → 📱 Event ارسالی: { "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", // portrait | landscape "screenWidth": 812, "screenHeight": 375 } } React چه کار باید بکند: useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'ORIENTATION_CHANGED') { const { orientation } = event.data.payload; if (orientation === 'landscape') { // ویدیو پلیر را بزرگتر کن setVideoPlayerSize('large'); // دکمه‌ها را در کنار قرار بده (row) setButtonsLayout('horizontal'); } else { // ویدیو پلیر را عادی کن setVideoPlayerSize('normal'); // دکمه‌ها را زیر هم قرار بده (column) setButtonsLayout('vertical'); } } }); }, []); ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 📊 مثال کامل: سناریوی واقعی سناریو: کاربر در حال چت است و گوشی را می‌چرخاند. // 1. Initial Load { "layout": { "screenWidth": 375, "isMobile": true }, "safeArea": { "top": 44, "bottom": 34 } } → Chat با padding 16px نمایش داده می‌شود // 2. کاربر روی TextField کلیک می‌کند { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336 } } → Chat list 336px بالا می‌رود // 3. کاربر گوشی را می‌چرخاند (landscape) { "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", "screenWidth": 812 } } → Layout تغییر می‌کند { "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "breakpoint": "tablet" } } → Padding از 16px به 64px تغییر می‌کند { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 220 } // کیبورد کوچکتر در landscape } → Keyboard height جدید // 4. کاربر کیبورد را می‌بندد { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0 } } → Chat list به حالت عادی برمی‌گردد ✅ خلاصه ┌─────────────────────┬───────────────────┬────────────────────────────────────────────────┐ │ Event │ چه موقع؟ │ چرا مهم است؟ │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ Initial Config │ یکبار هنگام load │ تنظیمات اولیه (breakpoint, safeArea, platform) │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SCREEN_SIZE_CHANGED │ چرخش گوشی │ تغییر padding, layout │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ KEYBOARD_CHANGED │ باز/بسته کیبورد │ جا دادن TextField، scroll به پیام │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SAFE_AREA_CHANGED │ full screen video │ مخفی/نمایش header/footer │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ ORIENTATION_CHANGED │ چرخش گوشی │ تغییر UI (horizontal/vertical) │ └─────────────────────┴───────────────────┴────────────────────────────────────────────────┘