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)
|
? message.content.replace(/[*_~`>#\-\[\]]/g, '').slice(0, 100)
|
||||||
: 'Sent an attachment';
|
: 'Sent an attachment';
|
||||||
sendNotification(displayName, body, {
|
sendNotification(displayName, body, {
|
||||||
channelId: message.channelId ?? message.dmChannelId,
|
channelId: message.channelId,
|
||||||
spaceId: message.dmChannelId ? '@me' : undefined,
|
|
||||||
});
|
});
|
||||||
break; // one notification per batch
|
break; // one notification per batch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function MobileBottomNav() {
|
|||||||
const hasUnreadSpaces = Array.from(unreadChannels).some(chId => !voiceChannelIds.has(chId));
|
const hasUnreadSpaces = Array.from(unreadChannels).some(chId => !voiceChannelIds.has(chId));
|
||||||
|
|
||||||
// Pending friend requests — filter for incoming only
|
// 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') => {
|
const handleTab = (tab: 'spaces' | 'dms' | 'you') => {
|
||||||
setMobileTab(tab);
|
setMobileTab(tab);
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ export function MobileChatScreen({ params }: MobileChatScreenProps) {
|
|||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<MessageList />
|
{channelId && <MessageList channelId={channelId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Typing indicator + Input */}
|
{/* Typing indicator + Input */}
|
||||||
<TypingIndicator />
|
{channelId && <TypingIndicator channelId={channelId} />}
|
||||||
<MessageInput />
|
{channelId && <MessageInput channelId={channelId} channelName={channelName} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const sectionIcons: Record<string, React.ReactNode> = {
|
|||||||
export function MobileSettingsScreen({ initialPanel }: MobileSettingsScreenProps) {
|
export function MobileSettingsScreen({ initialPanel }: MobileSettingsScreenProps) {
|
||||||
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
const popMobileScreen = useUIStore((s) => s.popMobileScreen);
|
||||||
const pushMobileScreen = useUIStore((s) => s.pushMobileScreen);
|
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 is set, render that panel directly
|
||||||
if (initialPanel) {
|
if (initialPanel) {
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ export function MobileShell() {
|
|||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
const match = path.match(/^\/channels\/([^/]+)\/([^/]+)$/);
|
const match = path.match(/^\/channels\/([^/]+)\/([^/]+)$/);
|
||||||
if (match && mobileStack.length === 0) {
|
if (match && mobileStack.length === 0) {
|
||||||
const [, spaceId, channelId] = match;
|
const spaceId = match[1] ?? '';
|
||||||
|
const channelId = match[2] ?? '';
|
||||||
if (spaceId === '@me') {
|
if (spaceId === '@me') {
|
||||||
pushMobileScreen('channel-chat', { channelId, spaceId: '@me' });
|
pushMobileScreen('channel-chat', { channelId, spaceId: '@me' });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function MobileSpacesScreen() {
|
|||||||
|
|
||||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
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 pushMobileScreen = useUIStore((s) => s.pushMobileScreen);
|
||||||
const setMobileTab = useUIStore((s) => s.setMobileTab);
|
const setMobileTab = useUIStore((s) => s.setMobileTab);
|
||||||
@@ -52,7 +52,7 @@ export function MobileSpacesScreen() {
|
|||||||
|
|
||||||
// Auto-select first space if none selected
|
// Auto-select first space if none selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSpaceId && spaces.length > 0) {
|
if (!selectedSpaceId && spaces.length > 0 && spaces[0]) {
|
||||||
setSelectedSpaceId(spaces[0].id);
|
setSelectedSpaceId(spaces[0].id);
|
||||||
}
|
}
|
||||||
}, [selectedSpaceId, spaces]);
|
}, [selectedSpaceId, spaces]);
|
||||||
@@ -107,7 +107,7 @@ export function MobileSpacesScreen() {
|
|||||||
|
|
||||||
const handleChannelTap = (channel: Channel) => {
|
const handleChannelTap = (channel: Channel) => {
|
||||||
if (channel.type === 'voice') {
|
if (channel.type === 'voice') {
|
||||||
joinChannel(channel.id);
|
setCurrentVoiceChannel(channel.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedSpaceId) {
|
if (selectedSpaceId) {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export function MobileVoiceFullScreen() {
|
|||||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||||
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
|
||||||
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
|
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 toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||||
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
|
||||||
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
|
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 voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
||||||
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
|
||||||
|
|
||||||
@@ -62,9 +62,9 @@ export function MobileVoiceFullScreen() {
|
|||||||
const member = members.find(m => m.userId === userId);
|
const member = members.find(m => m.userId === userId);
|
||||||
if (member) {
|
if (member) {
|
||||||
return {
|
return {
|
||||||
name: member.nickname ?? member.displayName ?? member.username,
|
name: member.nickname ?? member.user?.displayName ?? member.user?.username ?? 'User',
|
||||||
avatar: member.avatar ? `/api/uploads/${member.avatar}` : null,
|
avatar: member.user?.avatar ? `/api/uploads/${member.user.avatar}` : null,
|
||||||
avatarColor: member.avatarColor,
|
avatarColor: member.user?.avatarColor ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Check DM members
|
// Check DM members
|
||||||
@@ -82,7 +82,7 @@ export function MobileVoiceFullScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => {
|
||||||
leaveChannel();
|
leaveVoice();
|
||||||
popMobileScreen();
|
popMobileScreen();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export function MobileVoiceMiniBar() {
|
|||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const isMuted = useVoiceStore((s) => s.isMuted);
|
const isMuted = useVoiceStore((s) => s.isMuted);
|
||||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
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 toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||||
const voiceUsers = useVoiceStore((s) => s.voiceUsers);
|
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 channels = useSpaceStore((s) => s.channels);
|
||||||
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
const dmChannels = useSpaceStore((s) => s.dmChannels);
|
||||||
@@ -21,7 +21,8 @@ export function MobileVoiceMiniBar() {
|
|||||||
if (!currentVoiceChannelId) return null;
|
if (!currentVoiceChannelId) return null;
|
||||||
|
|
||||||
// Don't show mini-bar if voice full-screen is on top of the stack
|
// 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;
|
if (topScreen === 'voice-full') return null;
|
||||||
|
|
||||||
// Resolve channel name
|
// Resolve channel name
|
||||||
@@ -91,7 +92,7 @@ export function MobileVoiceMiniBar() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
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}>
|
<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 && (
|
{showLogoutConfirm && (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
isOpen={true}
|
||||||
title="Log Out"
|
title="Log Out"
|
||||||
message="Are you sure you want to log out?"
|
description="Are you sure you want to log out?"
|
||||||
confirmLabel="Log Out"
|
confirmLabel="Log Out"
|
||||||
onConfirm={() => { setShowLogoutConfirm(false); logout(); }}
|
onConfirm={() => { setShowLogoutConfirm(false); logout(); }}
|
||||||
onCancel={() => setShowLogoutConfirm(false)}
|
onClose={() => setShowLogoutConfirm(false)}
|
||||||
destructive
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -6,9 +7,13 @@ interface ModalProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
maxWidth?: string;
|
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) => {
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
onClose();
|
onClose();
|
||||||
@@ -24,6 +29,61 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'max-w-md'
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-[200] flex items-center justify-center animate-fade-in">
|
<div className="fixed inset-0 z-[200] flex items-center justify-center animate-fade-in">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function useSwipeGesture({
|
|||||||
const handleTouchStart = useCallback((e: TouchEvent) => {
|
const handleTouchStart = useCallback((e: TouchEvent) => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
|
if (!touch) return;
|
||||||
if (touch.clientX <= edgeThreshold) {
|
if (touch.clientX <= edgeThreshold) {
|
||||||
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
swipingRef.current = false;
|
swipingRef.current = false;
|
||||||
@@ -28,6 +29,7 @@ export function useSwipeGesture({
|
|||||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||||
if (!touchStartRef.current) return;
|
if (!touchStartRef.current) return;
|
||||||
const touch = e.touches[0];
|
const touch = e.touches[0];
|
||||||
|
if (!touch) return;
|
||||||
const dx = touch.clientX - touchStartRef.current.x;
|
const dx = touch.clientX - touchStartRef.current.x;
|
||||||
const dy = touch.clientY - touchStartRef.current.y;
|
const dy = touch.clientY - touchStartRef.current.y;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user