feat: PWA push notifications — service worker, VAPID, subscription management
- Add web-push VAPID backend with push_subscriptions table (migration 078) - Backend push module with sendPushToUser/sendPushToRoom helpers - Push routes: GET vapid-key, POST subscribe, DELETE unsubscribe - Trigger push on new chat messages in direct/group rooms - PWA manifest, service worker, and icons (72–512px) - Frontend push lib: SW registration, subscribe/unsubscribe helpers - Push API in api.ts, SW registered in main.tsx - usePushSubscription hook for auto-subscribe on permission grant - PushPermissionRow component in ChatWidget settings view Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { api, type ChatRoom, type ChatMessage, type ChatSearchResult, type ChatR
|
||||
import type { User } from '../../types'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { subscribeToPush, isPushSupported } from '../../lib/push'
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -788,6 +789,9 @@ export function ChatWidget() {
|
||||
label="Показывать текст в уведомлении" checked={settings.notifShowText !== false}
|
||||
onChange={v => updateSettings({ notifShowText: v })} />
|
||||
</SettingsSection>
|
||||
<SettingsSection title="Push-уведомления">
|
||||
<PushPermissionRow />
|
||||
</SettingsSection>
|
||||
<SettingsSection title="Подсказка">
|
||||
<p className="text-xs text-slate-500 leading-relaxed">
|
||||
Правый клик на сообщении — реакции, редактирование, удаление.<br />
|
||||
@@ -1358,6 +1362,59 @@ function ToggleRow({ icon, label, checked, onChange }: {
|
||||
)
|
||||
}
|
||||
|
||||
// ── PushPermissionRow ─────────────────────────────────────────────────────
|
||||
|
||||
function PushPermissionRow() {
|
||||
const [permission, setPermission] = useState<NotificationPermission>(
|
||||
typeof Notification !== 'undefined' ? Notification.permission : 'default'
|
||||
)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (!isPushSupported()) {
|
||||
return (
|
||||
<p className="text-xs text-slate-400 py-2">
|
||||
Ваш браузер не поддерживает push-уведомления.
|
||||
{typeof navigator !== 'undefined' && navigator.userAgent.includes('iPhone') ? ' Добавьте сайт на домашний экран (iOS 16.4+).' : ''}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const enable = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await Notification.requestPermission()
|
||||
setPermission(result)
|
||||
if (result === 'granted') {
|
||||
const { publicKey } = await api.push.getVapidKey()
|
||||
if (!publicKey) return
|
||||
const sub = await subscribeToPush(publicKey)
|
||||
if (!sub) return
|
||||
const json = sub.toJSON()
|
||||
if (json.endpoint && json.keys?.p256dh && json.keys?.auth) {
|
||||
await api.push.subscribe({ endpoint: json.endpoint, p256dh: json.keys.p256dh, auth: json.keys.auth })
|
||||
}
|
||||
}
|
||||
} catch { /**/ }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<span className="text-slate-500 shrink-0"><Bell size={15} /></span>
|
||||
<span className="flex-1 text-sm text-slate-700 dark:text-slate-300 leading-tight">
|
||||
{permission === 'granted' ? 'Push включены' : permission === 'denied' ? 'Заблокированы в браузере' : 'Push-уведомления'}
|
||||
</span>
|
||||
{permission === 'default' && (
|
||||
<button onClick={enable} disabled={loading}
|
||||
className="text-xs px-2.5 py-1 rounded-lg bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-50 transition-colors shrink-0">
|
||||
{loading ? '...' : 'Включить'}
|
||||
</button>
|
||||
)}
|
||||
{permission === 'granted' && <span className="text-xs text-green-500 shrink-0">✓</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── GroupInfoView ──────────────────────────────────────────────────────────
|
||||
|
||||
function GroupInfoView({ room, allMembers, currentUserId, saving, editName, onEditName, onAvatarClick, onSave }: {
|
||||
|
||||
Reference in New Issue
Block a user