bb650ac2a0
Backend: - DB: reactions table, invites table, reply_to column on messages - gateway/events.go: added REACTION_ADD, REACTION_REMOVE events - internal/reaction/handlers.go: reaction CRUD with WebSocket broadcast - internal/invite/handlers.go: invite creation, info, join with code - gateway/hub.go: presence tracking with idle detection - gateway/client.go: idle timeout support Frontend - Social: - TypingIndicator: real-time 'user is typing...' display - ReactionBar: emoji reactions on messages with counts - EmojiPicker: searchable emoji grid for reactions - ReplyBar: quoted reply display above messages - MentionPopup: @mention autocomplete with user list Frontend - PWA: - manifest.json: PWA manifest with theme color and icons - sw.js: service worker with cache-first strategy and push support - stores/push.ts: push notification subscription management - InstallPrompt: 'Add to Home Screen' banner Frontend - Mobile: - MobileNav: bottom nav bar for mobile (servers/channels/chat/members) - MobileDrawer: slide-out drawer with server bar + channel list - index.html: PWA meta tags, safe area viewport Frontend - Polish: - ThemeToggle: dark/light mode switch with localStorage persistence - InviteModal: generate invite links with expiry and max uses - JoinServer: /invite/:code join flow - stores/typing.ts: typing indicator state management - stores/presence.ts: real-time presence tracking - tailwind.config.js: darkMode: 'class', light mode color tokens - styles/index.css: light mode CSS variable overrides
54 lines
1.8 KiB
TypeScript
54 lines
1.8 KiB
TypeScript
import { useVoiceStore } from '../stores/voice.ts';
|
|
import { ThemeToggle } from './ThemeToggle.tsx';
|
|
|
|
interface MobileNavProps {
|
|
activeTab: 'servers' | 'channels' | 'chat' | 'members';
|
|
onTabChange: (tab: 'servers' | 'channels' | 'chat' | 'members') => void;
|
|
onToggleDrawer: () => void;
|
|
}
|
|
|
|
export function MobileNav({ activeTab, onTabChange, onToggleDrawer }: MobileNavProps) {
|
|
const isConnected = useVoiceStore((state: any) => state.isConnected);
|
|
|
|
const tabs = [
|
|
{ id: 'servers' as const, label: '[=]', title: 'Servers' },
|
|
{ id: 'channels' as const, label: '[#]', title: 'Channels' },
|
|
{ id: 'chat' as const, label: '[_]', title: 'Chat' },
|
|
{ id: 'members' as const, label: '[@]', title: 'Members' },
|
|
];
|
|
|
|
return (
|
|
<div className="md:hidden fixed bottom-0 left-0 right-0 bg-gb-bg-h border-t border-gb-bg-t z-40">
|
|
<div className="flex items-center justify-around py-2">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => {
|
|
if (tab.id === 'servers' || tab.id === 'channels') {
|
|
onToggleDrawer();
|
|
}
|
|
onTabChange(tab.id);
|
|
}}
|
|
className={`
|
|
px-3 py-1 text-xs font-mono transition-colors
|
|
${activeTab === tab.id
|
|
? 'text-gb-orange'
|
|
: 'text-gb-fg-f hover:text-gb-fg'
|
|
}
|
|
`}
|
|
title={tab.title}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
{isConnected && (
|
|
<div className="w-1.5 h-1.5 rounded-full bg-gb-green animate-pulse" title="In voice" />
|
|
)}
|
|
<ThemeToggle />
|
|
</div>
|
|
{/* Safe area for phones with bottom notch */}
|
|
<div className="h-[env(safe-area-inset-bottom)]" />
|
|
</div>
|
|
);
|
|
}
|