fix(mobile): resolve all TypeScript errors from integration
- Fix voiceStore method names (toggleMic, leaveVoice, setCurrentVoiceChannel) - Fix MemberWithUser nested user property access - Pass required channelId props to MessageList, TypingIndicator, MessageInput - Fix ConfirmDialog props (isOpen, description, variant, onClose) - Add mobileStyle prop to Modal component (fullscreen + sheet modes) - Fix fromId/isAdmin camelCase property names - Guard possibly undefined touch events and array accesses - Fix notification channelId resolution
This commit is contained in:
@@ -62,8 +62,7 @@ export function NotificationController() {
|
||||
? message.content.replace(/[*_~`>#\-\[\]]/g, '').slice(0, 100)
|
||||
: 'Sent an attachment';
|
||||
sendNotification(displayName, body, {
|
||||
channelId: message.channelId ?? message.dmChannelId,
|
||||
spaceId: message.dmChannelId ? '@me' : undefined,
|
||||
channelId: message.channelId,
|
||||
});
|
||||
break; // one notification per batch
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function MobileBottomNav() {
|
||||
const hasUnreadSpaces = Array.from(unreadChannels).some(chId => !voiceChannelIds.has(chId));
|
||||
|
||||
// Pending friend requests — filter for incoming only
|
||||
const pendingIncoming = requests.filter(r => r.status === 'pending' && r.from_id !== authUser?.id);
|
||||
const pendingIncoming = requests.filter(r => r.status === 'pending' && r.fromId !== authUser?.id);
|
||||
|
||||
const handleTab = (tab: 'spaces' | 'dms' | 'you') => {
|
||||
setMobileTab(tab);
|
||||
|
||||
@@ -75,12 +75,12 @@ export function MobileChatScreen({ params }: MobileChatScreenProps) {
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MessageList />
|
||||
{channelId && <MessageList channelId={channelId} />}
|
||||
</div>
|
||||
|
||||
{/* Typing indicator + Input */}
|
||||
<TypingIndicator />
|
||||
<MessageInput />
|
||||
{channelId && <TypingIndicator channelId={channelId} />}
|
||||
{channelId && <MessageInput channelId={channelId} channelName={channelName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const sectionIcons: Record<string, React.ReactNode> = {
|
||||
export function MobileSettingsScreen({ initialPanel }: MobileSettingsScreenProps) {
|
||||
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
||||
const pushMobileScreen = useUIStore((s) => s.pushMobileScreen);
|
||||
const isAdmin = useAuthStore((s) => s.user?.is_admin);
|
||||
const isAdmin = useAuthStore((s) => s.user?.isAdmin);
|
||||
|
||||
// If initialPanel is set, render that panel directly
|
||||
if (initialPanel) {
|
||||
|
||||
@@ -63,7 +63,8 @@ export function MobileShell() {
|
||||
const path = location.pathname;
|
||||
const match = path.match(/^\/channels\/([^/]+)\/([^/]+)$/);
|
||||
if (match && mobileStack.length === 0) {
|
||||
const [, spaceId, channelId] = match;
|
||||
const spaceId = match[1] ?? '';
|
||||
const channelId = match[2] ?? '';
|
||||
if (spaceId === '@me') {
|
||||
pushMobileScreen('channel-chat', { channelId, spaceId: '@me' });
|
||||
} else {
|
||||
|
||||
@@ -27,7 +27,7 @@ export function MobileSpacesScreen() {
|
||||
|
||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const joinChannel = useVoiceStore((s) => s.joinChannel);
|
||||
const setCurrentVoiceChannel = useVoiceStore((s) => s.setCurrentVoiceChannel);
|
||||
|
||||
const pushMobileScreen = useUIStore((s) => s.pushMobileScreen);
|
||||
const setMobileTab = useUIStore((s) => s.setMobileTab);
|
||||
@@ -52,7 +52,7 @@ export function MobileSpacesScreen() {
|
||||
|
||||
// Auto-select first space if none selected
|
||||
useEffect(() => {
|
||||
if (!selectedSpaceId && spaces.length > 0) {
|
||||
if (!selectedSpaceId && spaces.length > 0 && spaces[0]) {
|
||||
setSelectedSpaceId(spaces[0].id);
|
||||
}
|
||||
}, [selectedSpaceId, spaces]);
|
||||
@@ -107,7 +107,7 @@ export function MobileSpacesScreen() {
|
||||
|
||||
const handleChannelTap = (channel: Channel) => {
|
||||
if (channel.type === 'voice') {
|
||||
joinChannel(channel.id);
|
||||
setCurrentVoiceChannel(channel.id);
|
||||
return;
|
||||
}
|
||||
if (selectedSpaceId) {
|
||||
|
||||
@@ -13,11 +13,11 @@ export function MobileVoiceFullScreen() {
|
||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
||||
const toggleMute = useVoiceStore((s) => s.toggleMute);
|
||||
const toggleMute = useVoiceStore((s) => s.toggleMic);
|
||||
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
||||
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
|
||||
const leaveChannel = useVoiceStore((s) => s.leaveChannel);
|
||||
const leaveVoice = useVoiceStore((s) => s.leaveVoice);
|
||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||
|
||||
@@ -62,9 +62,9 @@ export function MobileVoiceFullScreen() {
|
||||
const member = members.find(m => m.userId === userId);
|
||||
if (member) {
|
||||
return {
|
||||
name: member.nickname ?? member.displayName ?? member.username,
|
||||
avatar: member.avatar ? `/api/uploads/${member.avatar}` : null,
|
||||
avatarColor: member.avatarColor,
|
||||
name: member.nickname ?? member.user?.displayName ?? member.user?.username ?? 'User',
|
||||
avatar: member.user?.avatar ? `/api/uploads/${member.user.avatar}` : null,
|
||||
avatarColor: member.user?.avatarColor ?? null,
|
||||
};
|
||||
}
|
||||
// Check DM members
|
||||
@@ -82,7 +82,7 @@ export function MobileVoiceFullScreen() {
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
leaveChannel();
|
||||
leaveVoice();
|
||||
popMobileScreen();
|
||||
};
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ export function MobileVoiceMiniBar() {
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const isMuted = useVoiceStore((s) => s.isMuted);
|
||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||
const toggleMute = useVoiceStore((s) => s.toggleMute);
|
||||
const toggleMute = useVoiceStore((s) => s.toggleMic);
|
||||
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||
const leaveChannel = useVoiceStore((s) => s.leaveChannel);
|
||||
const leaveVoice = useVoiceStore((s) => s.leaveVoice);
|
||||
|
||||
const channels = useSpaceStore((s) => s.channels);
|
||||
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
||||
@@ -21,7 +21,8 @@ export function MobileVoiceMiniBar() {
|
||||
if (!currentVoiceChannelId) return null;
|
||||
|
||||
// Don't show mini-bar if voice full-screen is on top of the stack
|
||||
const topScreen = mobileStack.length > 0 ? mobileStack[mobileStack.length - 1].screen : null;
|
||||
const topEntry = mobileStack.length > 0 ? mobileStack[mobileStack.length - 1] : undefined;
|
||||
const topScreen = topEntry?.screen ?? null;
|
||||
if (topScreen === 'voice-full') return null;
|
||||
|
||||
// Resolve channel name
|
||||
@@ -91,7 +92,7 @@ export function MobileVoiceMiniBar() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); leaveChannel(); }}
|
||||
onClick={(e) => { e.stopPropagation(); leaveVoice(); }}
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center bg-accent-rose/20 text-accent-rose hover:bg-accent-rose/30 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
|
||||
@@ -142,12 +142,13 @@ export function MobileYouScreen() {
|
||||
|
||||
{showLogoutConfirm && (
|
||||
<ConfirmDialog
|
||||
isOpen={true}
|
||||
title="Log Out"
|
||||
message="Are you sure you want to log out?"
|
||||
description="Are you sure you want to log out?"
|
||||
confirmLabel="Log Out"
|
||||
onConfirm={() => { setShowLogoutConfirm(false); logout(); }}
|
||||
onCancel={() => setShowLogoutConfirm(false)}
|
||||
destructive
|
||||
onClose={() => setShowLogoutConfirm(false)}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -6,9 +7,13 @@ interface ModalProps {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: string;
|
||||
/** Mobile display style: 'fullscreen' fills the screen, 'sheet' anchors to bottom, 'default' stays centered */
|
||||
mobileStyle?: 'fullscreen' | 'sheet' | 'default';
|
||||
}
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md' }: ModalProps) {
|
||||
export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md', mobileStyle = 'default' }: ModalProps) {
|
||||
const isMobile = useUIStore((s) => s.isMobile);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
@@ -24,6 +29,61 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md'
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Mobile fullscreen style
|
||||
if (isMobile && mobileStyle === 'fullscreen') {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex flex-col bg-surface-base animate-fade-in">
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-4 pt-4 flex-shrink-0" style={{ paddingTop: 'calc(16px + env(safe-area-inset-top))' }}>
|
||||
<h2 className="text-xl font-bold text-txt-primary">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-txt-tertiary hover:text-txt-primary transition-colors p-1"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 overflow-y-auto scrollbar-thin flex-1 min-h-0" style={{ paddingBottom: 'calc(16px + env(safe-area-inset-bottom))' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mobile bottom sheet style
|
||||
if (isMobile && mobileStyle === 'sheet') {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-end justify-center animate-fade-in">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="relative w-full max-h-[85vh] flex flex-col glass-modal rounded-t-2xl animate-slide-up" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
||||
{title && (
|
||||
<div className="flex items-center justify-between px-4 pt-4 flex-shrink-0">
|
||||
<h2 className="text-xl font-bold text-txt-primary">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-txt-tertiary hover:text-txt-primary transition-colors p-1"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 overflow-y-auto scrollbar-thin flex-1 min-h-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default centered dialog (desktop and mobile default)
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center animate-fade-in">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user