2025-12-24 11:51:40 +00:00
|
|
|
import React, { useEffect, useRef, useState } from 'react';
|
2025-12-13 02:34:34 +00:00
|
|
|
import { useChatStore } from '../store/chatStore';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { ChatMessageComponent } from './ChatMessage';
|
|
|
|
|
import { useChat } from '../hooks/useChat';
|
2026-02-22 02:46:10 +00:00
|
|
|
import { useWebRTC } from '../hooks/useWebRTC';
|
2025-12-24 11:51:40 +00:00
|
|
|
import { MessageSearch } from './MessageSearch';
|
|
|
|
|
import { TypingIndicator } from './TypingIndicator';
|
2026-02-22 02:46:10 +00:00
|
|
|
import { CallButton } from './CallButton';
|
|
|
|
|
import { IncomingCallModal } from './IncomingCallModal';
|
|
|
|
|
import { ActiveCallBar } from './ActiveCallBar';
|
2026-01-26 13:12:17 +00:00
|
|
|
import {
|
2026-03-06 17:52:08 +00:00
|
|
|
Search,
|
|
|
|
|
X,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
UserPlus,
|
|
|
|
|
Users,
|
2026-01-26 13:12:17 +00:00
|
|
|
} from 'lucide-react';
|
2025-12-24 11:51:40 +00:00
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-11 02:20:52 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-03-06 17:52:08 +00:00
|
|
|
import { InviteRoomModal } from './InviteRoomModal';
|
|
|
|
|
import { RoomMembersModal } from './RoomMembersModal';
|
2026-01-16 11:31:40 +00:00
|
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
2026-03-02 18:25:37 +00:00
|
|
|
import { adminService } from '@/services/adminService';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
interface ChatRoomProps {
|
|
|
|
|
conversationId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ChatRoom: React.FC<ChatRoomProps> = ({ conversationId }) => {
|
2026-02-22 02:46:10 +00:00
|
|
|
const { messages, conversations, userId, incomingCall, activeCall } =
|
|
|
|
|
useChatStore();
|
|
|
|
|
const { fetchHistory, sendRawMessage, wsStatus } = useChat();
|
fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
- Service layer double-unwrapping (subscriptionService, distributionService, gearService)
- Self-referencing variables in SearchPageResults
- FeedView/ExploreView .posts→.items alignment
- useQueueSync Zustand subscribe API
- AdminAuditLogsView missing interface fields
- Toast proxy type, interceptor type narrowing
- 22 unused imports/variables removed
- 5 storybook mock data fixes
- Align frontend API calls with backend endpoints:
- Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
- Chat: chatService uses /conversations (was mock data), WS URL from backend token
- Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
- Settings: suppress 2FA toast error when endpoint unavailable
- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)
- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.
- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:18:49 +00:00
|
|
|
const { data: _user } = useUser();
|
2026-02-22 02:46:10 +00:00
|
|
|
const webrtc = useWebRTC({ sendMessage: sendRawMessage });
|
2025-12-03 21:56:50 +00:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
2025-12-24 11:51:40 +00:00
|
|
|
const [showSearch, setShowSearch] = useState(false);
|
2026-03-06 17:52:08 +00:00
|
|
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
|
|
|
|
const [showMembersModal, setShowMembersModal] = useState(false);
|
2026-03-02 18:25:37 +00:00
|
|
|
const [webrtcEnabled, setWebrtcEnabled] = useState(true);
|
2026-01-13 18:47:57 +00:00
|
|
|
const [highlightedMessageId, setHighlightedMessageId] = useState<
|
|
|
|
|
string | null
|
|
|
|
|
>(null);
|
2026-02-12 21:59:09 +00:00
|
|
|
const highlightTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
2026-03-02 18:25:37 +00:00
|
|
|
useEffect(() => {
|
feat: frontend pages and feature modules polish
Update dashboard (stats, recent tracks/activity), discover, distribution,
education, feed, subscription, support, search, settings, live, cloud,
analytics, auth, chat, social, tracks, playlists, presence, upload,
and library manager. Consistent UI patterns and error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:21 +00:00
|
|
|
let cancelled = false;
|
2026-03-02 18:25:37 +00:00
|
|
|
adminService.getClientFeatureFlags().then((flags) => {
|
feat: frontend pages and feature modules polish
Update dashboard (stats, recent tracks/activity), discover, distribution,
education, feed, subscription, support, search, settings, live, cloud,
analytics, auth, chat, social, tracks, playlists, presence, upload,
and library manager. Consistent UI patterns and error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:21 +00:00
|
|
|
if (cancelled) return;
|
2026-03-02 18:25:37 +00:00
|
|
|
const webrtc = flags.find((f) => f.name === 'WEBRTC_CALLS');
|
|
|
|
|
setWebrtcEnabled(webrtc?.enabled ?? true);
|
feat: frontend pages and feature modules polish
Update dashboard (stats, recent tracks/activity), discover, distribution,
education, feed, subscription, support, search, settings, live, cloud,
analytics, auth, chat, social, tracks, playlists, presence, upload,
and library manager. Consistent UI patterns and error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:46:21 +00:00
|
|
|
}).catch(() => { if (!cancelled) setWebrtcEnabled(true); });
|
|
|
|
|
return () => { cancelled = true; };
|
2026-03-02 18:25:37 +00:00
|
|
|
}, []);
|
|
|
|
|
|
2026-02-12 21:59:09 +00:00
|
|
|
// Cleanup highlight timeout on unmount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (highlightTimeoutRef.current) {
|
|
|
|
|
clearTimeout(highlightTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
const currentMessages = messages[conversationId] || [];
|
2026-01-03 17:48:45 +00:00
|
|
|
const fetchingRef = useRef<{ [key: string]: boolean }>({});
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
useEffect(() => {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (
|
|
|
|
|
conversationId &&
|
|
|
|
|
!messages[conversationId] &&
|
|
|
|
|
!fetchingRef.current[conversationId]
|
|
|
|
|
) {
|
2026-01-03 17:48:45 +00:00
|
|
|
fetchingRef.current[conversationId] = true;
|
|
|
|
|
fetchHistory(conversationId).finally(() => {
|
2026-01-11 02:20:52 +00:00
|
|
|
// Fetch complete
|
2026-01-03 17:48:45 +00:00
|
|
|
});
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
2026-01-03 17:48:45 +00:00
|
|
|
}, [conversationId, messages[conversationId], fetchHistory]);
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-01-11 02:20:52 +00:00
|
|
|
if (messagesEndRef.current) {
|
|
|
|
|
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
}, [currentMessages.length, conversationId]); // Scroll on new messages or channel switch
|
2025-12-03 21:56:50 +00:00
|
|
|
|
2025-12-24 11:51:40 +00:00
|
|
|
const handleMessageSelect = (messageId: string) => {
|
|
|
|
|
setHighlightedMessageId(messageId);
|
|
|
|
|
const messageElement = document.getElementById(`message-${messageId}`);
|
|
|
|
|
if (messageElement) {
|
|
|
|
|
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
2026-02-12 21:59:09 +00:00
|
|
|
if (highlightTimeoutRef.current) {
|
|
|
|
|
clearTimeout(highlightTimeoutRef.current);
|
|
|
|
|
}
|
|
|
|
|
highlightTimeoutRef.current = setTimeout(() => setHighlightedMessageId(null), 3000);
|
2025-12-24 11:51:40 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-22 02:46:10 +00:00
|
|
|
const conversation = conversations.find((c) => c.id === conversationId);
|
|
|
|
|
const isDM =
|
|
|
|
|
conversation?.type === 'direct' && conversation.participants.length === 2;
|
2026-03-06 17:52:08 +00:00
|
|
|
const isGroupRoom = conversation && conversation.type !== 'direct';
|
2026-02-22 02:46:10 +00:00
|
|
|
const targetUserId =
|
|
|
|
|
isDM && userId
|
|
|
|
|
? conversation.participants.find((p) => p !== userId) ?? null
|
|
|
|
|
: null;
|
|
|
|
|
const remoteUserName =
|
|
|
|
|
conversation?.name && conversation.name !== 'direct'
|
|
|
|
|
? conversation.name
|
|
|
|
|
: 'Utilisateur';
|
|
|
|
|
|
2025-12-03 21:56:50 +00:00
|
|
|
if (!conversationId) {
|
|
|
|
|
return (
|
2026-02-09 22:23:09 +00:00
|
|
|
<div className="flex-1 flex flex-col items-center justify-center text-muted-foreground space-y-4 animate-empty-state-in">
|
|
|
|
|
<div className="w-24 h-24 rounded-full bg-muted flex items-center justify-center">
|
|
|
|
|
<MessageSquare className="w-10 h-10 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<p className="text-sm font-medium text-foreground mb-1">
|
|
|
|
|
No conversation selected
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Pick a channel from the sidebar to start chatting.
|
|
|
|
|
</p>
|
2026-01-11 02:20:52 +00:00
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-22 02:46:10 +00:00
|
|
|
<div className="flex-1 flex flex-col h-full overflow-hidden relative">
|
|
|
|
|
<IncomingCallModal
|
|
|
|
|
open={!!incomingCall}
|
|
|
|
|
callerName={remoteUserName}
|
|
|
|
|
onAccept={() =>
|
|
|
|
|
incomingCall &&
|
|
|
|
|
webrtc.acceptCall(
|
|
|
|
|
incomingCall.conversationId,
|
|
|
|
|
incomingCall.callerUserId,
|
|
|
|
|
incomingCall.sdp,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
onReject={() =>
|
|
|
|
|
incomingCall &&
|
|
|
|
|
webrtc.rejectCall(
|
|
|
|
|
incomingCall.conversationId,
|
|
|
|
|
incomingCall.callerUserId,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
{activeCall && (
|
|
|
|
|
<div className="absolute bottom-0 left-0 right-0 z-30">
|
|
|
|
|
<ActiveCallBar
|
|
|
|
|
remoteUserName={remoteUserName}
|
|
|
|
|
isMuted={webrtc.isMuted}
|
|
|
|
|
onToggleMute={webrtc.toggleMute}
|
|
|
|
|
onHangup={() =>
|
|
|
|
|
webrtc.hangup(activeCall.conversationId, activeCall.remoteUserId)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-11 02:20:52 +00:00
|
|
|
{/* Search Header Overlay */}
|
2026-01-13 18:47:57 +00:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-02-12 01:09:29 +00:00
|
|
|
'absolute top-0 left-0 right-0 z-20 px-4 py-2 transition-all duration-[var(--sumi-duration-normal)]',
|
2026-01-13 18:47:57 +00:00
|
|
|
showSearch
|
feat(ui): semantic tokens on library, chat, dashboard, search
PlaylistDetailView: hero border, overlay, sort buttons, table header, row hover → border-border, bg-background/50, hover:bg-muted/50
ChatMessage: action buttons hover, own/other bubbles, attachment preview, context menu, modal → muted/border/foreground
ChatRoom: header bar, channel item hover, input pill → bg-card/90 border-border, hover:bg-muted/50, bg-muted/30
TrackList: play icon and title when not current → text-foreground
SearchPageHeader: title, search container, input, clear button → text-foreground, bg-card/80 border-border, hover:bg-muted/50
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 08:53:17 +00:00
|
|
|
? 'bg-card/90 backdrop-blur-md border-b border-border'
|
2026-01-13 18:47:57 +00:00
|
|
|
: 'bg-transparent pointer-events-none',
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-12-24 11:51:40 +00:00
|
|
|
{showSearch ? (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex items-center gap-2 max-w-2xl mx-auto">
|
2025-12-24 11:51:40 +00:00
|
|
|
<div className="flex-1">
|
|
|
|
|
<MessageSearch
|
|
|
|
|
conversationId={conversationId}
|
|
|
|
|
onMessageSelect={handleMessageSelect}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setShowSearch(false)}
|
feat(ui): semantic tokens on library, chat, dashboard, search
PlaylistDetailView: hero border, overlay, sort buttons, table header, row hover → border-border, bg-background/50, hover:bg-muted/50
ChatMessage: action buttons hover, own/other bubbles, attachment preview, context menu, modal → muted/border/foreground
ChatRoom: header bar, channel item hover, input pill → bg-card/90 border-border, hover:bg-muted/50, bg-muted/30
TrackList: play icon and title when not current → text-foreground
SearchPageHeader: title, search container, input, clear button → text-foreground, bg-card/80 border-border, hover:bg-muted/50
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 08:53:17 +00:00
|
|
|
className="hover:bg-muted/50"
|
2025-12-24 11:51:40 +00:00
|
|
|
>
|
|
|
|
|
<X className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-02-22 02:46:10 +00:00
|
|
|
<div className="flex justify-end items-center gap-2 pointer-events-auto">
|
2026-03-06 17:52:08 +00:00
|
|
|
{isGroupRoom && (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setShowInviteModal(true)}
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
className="text-muted-foreground/50 hover:text-foreground hover:bg-muted/50 bg-muted/30 backdrop-blur-sm rounded-full h-8 px-4 shadow-[0_0_8px_rgba(26,26,30,0.05)]"
|
2026-03-06 17:52:08 +00:00
|
|
|
>
|
|
|
|
|
<UserPlus className="h-3 w-3 mr-2" />
|
|
|
|
|
<span className="text-xs font-mono uppercase">Invite</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setShowMembersModal(true)}
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
className="text-muted-foreground/50 hover:text-foreground hover:bg-muted/50 bg-muted/30 backdrop-blur-sm rounded-full h-8 px-4 shadow-[0_0_8px_rgba(26,26,30,0.05)]"
|
2026-03-06 17:52:08 +00:00
|
|
|
>
|
|
|
|
|
<Users className="h-3 w-3 mr-2" />
|
|
|
|
|
<span className="text-xs font-mono uppercase">Members</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-03-02 18:25:37 +00:00
|
|
|
{isDM && targetUserId && webrtcEnabled && (
|
2026-02-22 02:46:10 +00:00
|
|
|
<CallButton
|
|
|
|
|
conversationId={conversationId}
|
|
|
|
|
targetUserId={targetUserId}
|
|
|
|
|
onCall={() =>
|
|
|
|
|
webrtc.startCall(conversationId, targetUserId, 'audio')
|
|
|
|
|
}
|
|
|
|
|
disabled={wsStatus !== 'connected'}
|
feat(webrtc): coturn ICE config endpoint + frontend wiring + ops template (v1.0.9 item 1.2)
Closes FUNCTIONAL_AUDIT.md §4 #1: WebRTC 1:1 calls had working
signaling but no NAT traversal, so calls between two peers behind
symmetric NAT (corporate firewalls, mobile carrier CGNAT, Incus
container default networking) failed silently after the SDP exchange.
Backend:
- GET /api/v1/config/webrtc (public) returns {iceServers: [...]}
built from WEBRTC_STUN_URLS / WEBRTC_TURN_URLS / *_USERNAME /
*_CREDENTIAL env vars. Half-config (URLs without creds, or vice
versa) deliberately omits the TURN block — a half-configured TURN
surfaces auth errors at call time instead of falling back cleanly
to STUN-only.
- 4 handler tests cover the matrix.
Frontend:
- services/api/webrtcConfig.ts caches the config for the page
lifetime and falls back to the historical hardcoded Google STUN
if the fetch fails.
- useWebRTC fetches at mount, hands iceServers synchronously to
every RTCPeerConnection, exposes a {hasTurn, loaded} hint.
- CallButton tooltip warns up-front when TURN isn't configured
instead of letting calls time out silently.
Ops:
- infra/coturn/turnserver.conf — annotated template with the SSRF-
safe denied-peer-ip ranges, prometheus exporter, TLS for TURNS,
static lt-cred-mech (REST-secret rotation deferred to v1.1).
- infra/coturn/README.md — Incus deploy walkthrough, smoke test
via turnutils_uclient, capacity rules of thumb.
- docs/ENV_VARIABLES.md gains a 13bis. WebRTC ICE servers section.
Coturn deployment itself is a separate ops action — this commit lands
the plumbing so the deploy can light up the path with zero code
changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 21:38:42 +00:00
|
|
|
hasTurn={webrtc.nat.loaded ? webrtc.nat.hasTurn : undefined}
|
2026-02-22 02:46:10 +00:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-24 11:51:40 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setShowSearch(true)}
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
className="text-muted-foreground/50 hover:text-foreground hover:bg-muted/50 bg-muted/30 backdrop-blur-sm rounded-full h-8 px-4 shadow-[0_0_8px_rgba(26,26,30,0.05)]"
|
2025-12-24 11:51:40 +00:00
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<Search className="h-3 w-3 mr-2" />
|
|
|
|
|
<span className="text-xs font-mono uppercase">Search Log</span>
|
2025-12-24 11:51:40 +00:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 space-y-4 scroll-smooth">
|
|
|
|
|
{/* Welcome Message for Empty Room */}
|
|
|
|
|
{currentMessages.length === 0 && (
|
2026-02-10 13:06:30 +00:00
|
|
|
<div className="flex flex-col items-center justify-center h-layout-lyrics-sm text-center space-y-4 animate-empty-state-in">
|
2026-02-09 22:23:09 +00:00
|
|
|
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
|
|
|
|
|
<MessageSquare className="w-7 h-7 text-muted-foreground" />
|
2026-01-11 02:20:52 +00:00
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-02-09 22:23:09 +00:00
|
|
|
<p className="text-foreground font-medium">No messages yet</p>
|
2026-02-08 23:13:27 +00:00
|
|
|
<p className="text-sm text-muted-foreground mt-1">
|
2026-02-09 22:23:09 +00:00
|
|
|
Send the first message to start the conversation.
|
2026-01-13 18:47:57 +00:00
|
|
|
</p>
|
2026-01-11 02:20:52 +00:00
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-11 02:20:52 +00:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Message Stream */}
|
2026-02-12 23:32:08 +00:00
|
|
|
{currentMessages.map((msg) => {
|
2026-01-11 02:20:52 +00:00
|
|
|
return (
|
2025-12-24 11:51:40 +00:00
|
|
|
<div
|
|
|
|
|
key={msg.id}
|
|
|
|
|
id={`message-${msg.id}`}
|
2026-01-11 02:20:52 +00:00
|
|
|
className={cn(
|
2026-02-12 01:09:29 +00:00
|
|
|
'transition-all duration-[var(--sumi-duration-slow)] animate-slideUp',
|
2026-01-13 18:47:57 +00:00
|
|
|
highlightedMessageId === msg.id &&
|
refactor: Phase 3a — Global color class migration to SUMI semantics
- Replace all kodo-* color classes across ~100 TSX files:
kodo-void → background, kodo-ink → card, kodo-graphite → muted,
kodo-steel → muted-foreground, kodo-cyan → primary, kodo-magenta → destructive,
kodo-lime → success, kodo-red → destructive, kodo-gold → warning
- Replace cyan-500, magenta-500, lime-500 default Tailwind colors with
semantic equivalents (primary, destructive, success)
- Fix WaveformVisualizer hardcoded hex colors to SUMI values
- Delete global-effects.css (conflicting, redundant with index.css)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 00:51:49 +00:00
|
|
|
'bg-muted/10 rounded-xl -mx-4 px-4 py-2 ring-1 ring-border/30',
|
2026-01-11 02:20:52 +00:00
|
|
|
)}
|
2025-12-24 11:51:40 +00:00
|
|
|
>
|
|
|
|
|
<ChatMessageComponent message={msg} />
|
|
|
|
|
</div>
|
2026-01-11 02:20:52 +00:00
|
|
|
);
|
|
|
|
|
})}
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-24 11:51:40 +00:00
|
|
|
<TypingIndicator conversationId={conversationId} />
|
2026-01-11 02:20:52 +00:00
|
|
|
<div ref={messagesEndRef} className="h-4" />
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-03-06 17:52:08 +00:00
|
|
|
|
|
|
|
|
{isGroupRoom && (
|
|
|
|
|
<>
|
|
|
|
|
<InviteRoomModal
|
|
|
|
|
open={showInviteModal}
|
|
|
|
|
onOpenChange={setShowInviteModal}
|
|
|
|
|
conversationId={conversationId}
|
|
|
|
|
/>
|
|
|
|
|
<RoomMembersModal
|
|
|
|
|
open={showMembersModal}
|
|
|
|
|
onOpenChange={setShowMembersModal}
|
|
|
|
|
conversationId={conversationId}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|