fix: sửa lỗi notification và giao diện chat trên android
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-geolocation')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
implementation project(':codetrix-studio-capacitor-google-auth')
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 212 B |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 147 B |
|
After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 175 B |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 558 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 39 KiB |
@@ -8,5 +8,8 @@ project(':capacitor-camera').projectDir = new File('../../node_modules/@capacito
|
||||
include ':capacitor-geolocation'
|
||||
project(':capacitor-geolocation').projectDir = new File('../../node_modules/@capacitor/geolocation/android')
|
||||
|
||||
include ':capacitor-local-notifications'
|
||||
project(':capacitor-local-notifications').projectDir = new File('../../node_modules/@capacitor/local-notifications/android')
|
||||
|
||||
include ':codetrix-studio-capacitor-google-auth'
|
||||
project(':codetrix-studio-capacitor-google-auth').projectDir = new File('../../node_modules/@codetrix-studio/capacitor-google-auth/android')
|
||||
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 230 KiB |
@@ -21,8 +21,16 @@
|
||||
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
|
||||
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
|
||||
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
|
||||
<script type="module" crossorigin src="/assets/index-Do9UKwUA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D58kjk9H.css">
|
||||
<script type="module" crossorigin src="/assets/index-BximWk33.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Cyuzqnbw.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-others-VIU5qAPG.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-DOXkNaRS.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-maps-DY-S_hoR.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DQOuwrxr.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-IjT8x9OX.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-21UejyBs.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@capacitor/cli": "^7.6.7",
|
||||
"@capacitor/core": "^8.4.1",
|
||||
"@capacitor/geolocation": "^8.2.0",
|
||||
"@capacitor/local-notifications": "^8.2.0",
|
||||
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
||||
"date-fns": "^4.4.0",
|
||||
"jspdf": "^4.2.1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
|
||||
import { Send, MessageSquare, Loader2, Image as ImageIcon, MapPin, Download, X } from 'lucide-react';
|
||||
import { useNotification } from '@/hooks/useNotification';
|
||||
|
||||
@@ -255,6 +256,41 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
});
|
||||
};
|
||||
|
||||
// Handle Native Image Selection (Camera or Library)
|
||||
const handleNativePhotoSelect = async () => {
|
||||
try {
|
||||
const image = await Camera.getPhoto({
|
||||
quality: 90,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.Uri,
|
||||
source: CameraSource.Prompt, // Prompt selection
|
||||
saveToGallery: true, // Auto-save original photo to device gallery
|
||||
promptLabelHeader: 'Chọn hình ảnh',
|
||||
promptLabelPhoto: 'Chọn từ thư viện',
|
||||
promptLabelPicture: 'Chụp ảnh mới (Camera)'
|
||||
});
|
||||
|
||||
if (image && image.webPath) {
|
||||
setImagePreview(image.webPath);
|
||||
|
||||
// Convert Capacitor webPath resource back to standard File instance
|
||||
const response = await fetch(image.webPath);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], `photo.${image.format}`, { type: `image/${image.format}` });
|
||||
setSelectedImage(file);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Lỗi chọn ảnh native:', error);
|
||||
if (error?.message !== 'User cancelled photos app' && error?.message !== 'User cancelled camera') {
|
||||
notify({
|
||||
title: 'Lỗi',
|
||||
message: 'Không thể truy cập máy ảnh hoặc thư viện ảnh.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Image Selection
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -609,7 +645,7 @@ export const TourChat: React.FC<TourChatProps> = ({ tourId, embedded = false })
|
||||
{/* Attach photo button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onClick={Capacitor.isNativePlatform() ? handleNativePhotoSelect : () => fileInputRef.current?.click()}
|
||||
className="p-2.5 bg-white border border-gray-200 hover:bg-gray-50 text-gray-500 rounded-xl transition-all shadow-sm active:scale-95 flex items-center justify-center shrink-0"
|
||||
title="Đính kèm hình ảnh"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import { NotificationModal } from '../components/NotificationModal';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
|
||||
interface NotificationOptions {
|
||||
title: string;
|
||||
@@ -24,6 +26,24 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
message,
|
||||
type,
|
||||
});
|
||||
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
LocalNotifications.schedule({
|
||||
notifications: [
|
||||
{
|
||||
title: title || 'YoTrip',
|
||||
body: message,
|
||||
id: Math.floor(Math.random() * 1000000),
|
||||
schedule: { at: new Date(Date.now() + 100) },
|
||||
sound: 'default',
|
||||
actionTypeId: 'OPEN_APP',
|
||||
extra: null
|
||||
}
|
||||
]
|
||||
}).catch(err => {
|
||||
console.error('[LocalNotifications] Error scheduling native notification:', err);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
||||
@@ -1906,7 +1906,7 @@ export const TourDetailPage = ({
|
||||
<div className="min-h-dvh bg-[var(--background)] pb-20 overflow-x-hidden">
|
||||
{/* Top Navigation Bar */}
|
||||
<div className="sticky top-0 z-30 bg-[var(--surface)]/80 backdrop-blur-md border-b border-[var(--border)] px-4 py-3 flex items-center justify-between">
|
||||
<button onClick={onBack} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
|
||||
<button onClick={activeTab === 'chat' ? () => setActiveTab('plan') : onBack} className="p-2 hover:bg-[var(--background)] rounded-full transition-colors">
|
||||
<ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-secondary)]" />
|
||||
</button>
|
||||
<h1 className="text-base sm:text-lg font-bold text-[var(--text-primary)] truncate px-4 flex-1 text-center">
|
||||
@@ -2206,7 +2206,7 @@ export const TourDetailPage = ({
|
||||
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
|
||||
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full !px-0 !pb-0' : 'max-w-2xl mx-auto px-4 pb-24'}`}>
|
||||
{/* Tab Switcher */}
|
||||
{!(activeTab === 'plan' && viewMode === 'map') && (
|
||||
{!(activeTab === 'plan' && viewMode === 'map') && activeTab !== 'chat' && (
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-[60px] z-40">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
|
||||
function rewriteUrls(obj: any, backendUrl: string): any {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
@@ -37,6 +38,15 @@ if (Capacitor.isNativePlatform()) {
|
||||
console.error('[Native OAuth] Failed to initialize GoogleAuth client:', e);
|
||||
}
|
||||
|
||||
// Request native local notification permission on startup
|
||||
try {
|
||||
LocalNotifications.requestPermissions().then(result => {
|
||||
console.log('[LocalNotifications] Permissions requested:', result);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[LocalNotifications] Failed to request permissions:', e);
|
||||
}
|
||||
|
||||
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn';
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
@@ -23,6 +23,33 @@ export default defineConfig(({ mode }) => {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('react') || id.includes('scheduler')) {
|
||||
return 'vendor-react';
|
||||
}
|
||||
if (id.includes('leaflet')) {
|
||||
return 'vendor-maps';
|
||||
}
|
||||
if (id.includes('jspdf')) {
|
||||
return 'vendor-pdf';
|
||||
}
|
||||
if (id.includes('lucide-react')) {
|
||||
return 'vendor-icons';
|
||||
}
|
||||
if (id.includes('quill')) {
|
||||
return 'vendor-editor';
|
||||
}
|
||||
return 'vendor-others';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3002,
|
||||
host: true,
|
||||
|
||||