Notifications (web push now, Android FCM ready, iOS prepared): - device_tokens / notification_settings / conversation_mutes (migration 006) - REST: register/list/unregister devices, get/update settings (quiet hours, timezone, enable), mute/unmute conversation, VAPID public key - BullMQ queue: backend enqueues a job per new message; a dedicated stateless `notifications` worker service drains it - Delivery gating: skip sender, muted chats, disabled users, quiet hours, and ONLINE users (they get the message live + an in-app toast) — offline → push - Providers behind one interface: Web Push (VAPID/web-push) + FCM (firebase-admin, HTTP v1; Android + iOS via the same path). Dead tokens (404/410/unregistered) auto-retired; transient failures recorded - Web: service worker (push display + tap-to-open the originating chat), push registration/unregistration, in-app toast stack, deep-link via ?conversation= In-app voice & video messages: MediaRecorder capture in the composer (mic/video), live preview + timer, upload via the existing presigned-media path. No emojis anywhere: replaced all glyphs with inline SVG icons (shared/ui) so the UI renders identically across web/desktop/mobile; notification text is plain. Chores: dedupe ioredis (BullMQ) + pin uuid>=11.1.1 (audit clean) via pnpm overrides; reusable Centrifugo client factory for the worker. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
53 lines
1.6 KiB
JavaScript
53 lines
1.6 KiB
JavaScript
import { base } from '../../eslint.config.mjs';
|
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
import globals from 'globals';
|
|
|
|
// Strict Feature-Sliced Design: a layer may import only from itself and the
|
|
// layers below it. Upward imports are build errors. Enforced with path-based
|
|
// no-restricted-imports (reliable across relative imports), one override per
|
|
// layer forbidding every higher layer.
|
|
const FSD_LAYERS = ['app', 'pages', 'widgets', 'features', 'entities', 'shared'];
|
|
|
|
const fsdLayerOverrides = FSD_LAYERS.flatMap((layer, index) => {
|
|
const higherLayers = FSD_LAYERS.slice(0, index);
|
|
if (higherLayers.length === 0) {
|
|
return [];
|
|
}
|
|
return [
|
|
{
|
|
files: [`src/${layer}/**/*.{ts,tsx}`],
|
|
rules: {
|
|
'no-restricted-imports': [
|
|
'error',
|
|
{
|
|
patterns: higherLayers.map((higher) => ({
|
|
group: [`**/${higher}`, `**/${higher}/**`],
|
|
message: `FSD violation: layer '${layer}' may not import from the higher layer '${higher}'.`,
|
|
})),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
];
|
|
});
|
|
|
|
export default [
|
|
// `public/` holds static assets (incl. the service worker) served as-is; it is
|
|
// not part of the typed TS project, so keep it out of the typed-lint graph.
|
|
{ ignores: ['dist/**', 'public/**'] },
|
|
...base,
|
|
{
|
|
files: ['src/**/*.{ts,tsx}'],
|
|
languageOptions: {
|
|
globals: { ...globals.browser },
|
|
},
|
|
plugins: {
|
|
'react-hooks': reactHooks,
|
|
},
|
|
rules: {
|
|
'react-hooks/rules-of-hooks': 'error',
|
|
'react-hooks/exhaustive-deps': 'warn',
|
|
},
|
|
},
|
|
...fsdLayerOverrides,
|
|
];
|