npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@curia_/curia-chat-modal

v4.0.2

Published

Chat modal package for embedding The Lounge IRC client in Curia forum

Downloads

61

Readme

Curia Chat Modal

A React package for embedding The Lounge IRC client in a responsive modal interface with session-aware architecture.

Overview

This package provides a session-aware chat modal system designed for integration with IRC-based chat systems. It requires the parent application to handle IRC provisioning, channel management, and session state, while the modal focuses purely on UI presentation.

Architecture

Session-Aware Pattern

The chat modal uses a two-phase initialization pattern:

  1. Session Initialization (Parent App): IRC provisioning + channel fetching happens once per user session
  2. Modal Invocation (This Package): Pre-provisioned data is passed as props for instant display

This eliminates the 2-3 second delay on each modal open while providing better error handling and retry capabilities.

Installation

yarn add @curia_/curia-chat-modal

Integration Guide

1. Parent App Setup

A. Install Dependencies

yarn add sonner  # For toast notifications

B. IRC Provisioning API

Create an IRC user provisioning endpoint in your app:

// /api/irc-user-provision
export async function POST(req: NextRequest) {
  const user = req.user; // From your auth middleware
  
  try {
    // Generate IRC credentials
    const ircUsername = generateIrcUsername(user.name, user.id);
    const ircPassword = generateSecurePassword();
    
    // Provision user in your IRC bouncer (e.g., Soju)
    await sojuAdminService.provisionUser({
      ircUsername,
      ircPassword,
      nickname: generateIrcNickname(user.name),
      realname: user.name || ircUsername
    });

    return NextResponse.json({
      success: true,
      ircUsername,
      ircPassword,
      networkName: 'your-network-name'
    });
  } catch (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

C. Chat Channels API

Create a chat channels endpoint:

// /api/communities/[communityId]/chat-channels
export async function GET(req: AuthenticatedRequest, context: RouteContext) {
  const { communityId } = await context.params;
  
  try {
    // Get channels from database
    let channels = await ChatChannelQueries.getChannelsByCommunity(communityId);
    
    // AUTO-CREATE DEFAULT CHANNEL if none exist
    if (channels.length === 0) {
      const communityResult = await query(
        'SELECT name FROM communities WHERE id = $1',
        [communityId]
      );
      
      const communityName = communityResult.rows[0]?.name;
      
      const defaultChannel = await ChatChannelQueries.createChannel({
        community_id: communityId,
        name: communityName,
        description: `Main chat for ${communityName}`,
        irc_channel_name: generateIrcChannelName(communityName),
        is_single_mode: true,
        is_default: true,
        settings: {
          irc: { autoconnect: true, lockchannel: true, nofocus: true },
          ui: { defaultTheme: 'auto', allowThemeSwitch: true }
        }
      });
      
      channels = [defaultChannel];
    }
    
    return NextResponse.json(channels);
  } catch (error) {
    return NextResponse.json({ error: 'Failed to fetch channels' }, { status: 500 });
  }
}

D. Chat Session Hook

Create a session management hook in your app:

// hooks/useChatSession.ts
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'sonner';

export function useChatSession() {
  const { user, token } = useAuth();
  const [sessionData, setSessionData] = useState(null);
  const [isInitialized, setIsInitialized] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [initError, setInitError] = useState(null);
  const [retryCount, setRetryCount] = useState(0);
  const [isRetrying, setIsRetrying] = useState(false);

  // Retry logic with exponential backoff (3 attempts total)
  const initializeSessionWithRetry = useCallback(async (attemptNumber = 1) => {
    try {
      setIsLoading(attemptNumber === 1);
      setIsRetrying(attemptNumber > 1);
      setRetryCount(attemptNumber);
      setInitError(null);
      
      // 1. Provision IRC credentials
      const ircCredentials = await provisionIrcUser(
        token,
        process.env.NEXT_PUBLIC_CHAT_BASE_URL || '',
        process.env.NEXT_PUBLIC_CURIA_BASE_URL || ''
      );
      
      // 2. Fetch available channels
      const channels = await authFetchJson(
        `/api/communities/${user.cid}/chat-channels`,
        { token }
      );
      
      // 3. Identify default channel
      const defaultChannel = channels.find(ch => ch.is_default) || channels[0];
      
      if (!defaultChannel) {
        throw new Error('No chat channels available for community');
      }

      setSessionData({ ircCredentials, channels, defaultChannel });
      setIsInitialized(true);
      
      // Success toast
      toast.success("Chat connected! 💬");
      
    } catch (error) {
      if (attemptNumber < 3) {
        // Retry with exponential backoff: 1s, 2s delays
        const delay = Math.pow(2, attemptNumber - 1) * 1000;
        setTimeout(() => initializeSessionWithRetry(attemptNumber + 1), delay);
      } else {
        // Final failure
        setInitError(error.message);
        setIsInitialized(false);
        toast.error("Chat connection failed. Please refresh the page to retry.", {
          duration: 10000,
        });
      }
    } finally {
      setIsLoading(false);
      setIsRetrying(false);
    }
  }, [user?.cid, token]);

  useEffect(() => {
    if (user && token && user.cid) {
      initializeSessionWithRetry(1);
    }
  }, [user?.userId, user?.cid, user, token, initializeSessionWithRetry]);

  const retryInitialization = useCallback(() => {
    setRetryCount(0);
    setInitError(null);
    setIsRetrying(false);
    setIsInitialized(false);
    setSessionData(null);
    
    if (user && token && user.cid) {
      initializeSessionWithRetry(1);
    }
  }, [user, token, initializeSessionWithRetry]);

  return {
    sessionData,
    isInitialized,
    isLoading,
    initError,
    retryCount,
    isRetrying,
    retryInitialization,
    getChannelById: (channelId) => sessionData?.channels.find(ch => ch.id === channelId)
  };
}

E. Chat Modal Wrapper

Create a wrapper component that handles all states:

// components/ChatModalWrapper.tsx
import React from 'react';
import { ChatModal, useChatModal } from '@curia_/curia-chat-modal';
import { useChatSession } from '@/hooks/useChatSession';
import { ChatLoadingModal } from '@/components/chat/ChatLoadingModal';
import { ChatErrorModal } from '@/components/chat/ChatErrorModal';

export function ChatModalWrapper() {
  const { isChatOpen, selectedChannelId, closeChat } = useChatModal();
  const { 
    sessionData, 
    isInitialized, 
    isLoading, 
    initError, 
    retryCount, 
    isRetrying, 
    retryInitialization 
  } = useChatSession();
  const theme = useEffectiveTheme();
  
  if (!isChatOpen) return null;

  // Show loading during initialization/retries
  if (isLoading || isRetrying) {
    const message = isRetrying 
      ? `Retrying connection (${retryCount}/3)...`
      : "Connecting to chat...";
    
    return <ChatLoadingModal message={message} onClose={closeChat} />;
  }

  // Show error after all retries failed
  if (initError && !isRetrying && retryCount >= 3) {
    return (
      <ChatErrorModal 
        error={initError}
        retryCount={retryCount}
        onRetry={retryInitialization}
        onClose={closeChat}
      />
    );
  }

  if (!isInitialized || !sessionData) return null;

  // Determine target channel
  const targetChannel = selectedChannelId 
    ? sessionData.channels.find(ch => ch.id === selectedChannelId)
    : sessionData.defaultChannel;

  if (!targetChannel) {
    return (
      <ChatErrorModal 
        error="Selected chat channel not found."
        onRetry={() => closeChat()}
        onClose={closeChat}
      />
    );
  }

  return (
    <ChatModal
      ircCredentials={sessionData.ircCredentials}
      channel={targetChannel}
      chatBaseUrl={process.env.NEXT_PUBLIC_CHAT_BASE_URL}
      theme={theme}
      mode={targetChannel.is_single_mode ? 'single' : 'normal'}
      onClose={closeChat}
    />
  );
}

2. Sidebar Action Listener (Optional)

For parent app integration (e.g., iframe communication):

// components/SidebarActionListener.tsx
'use client';

import { useEffect } from 'react';
import { useChatModal } from '@curia_/curia-chat-modal';

export function SidebarActionListener() {
  const { openChat, closeChat } = useChatModal();

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.data?.type === 'sidebar_action') {
        const { action, data } = event.data;
        
        if (action === 'messages') {
          console.log('[SidebarActionListener] Opening chat modal');
          openChat(data?.channelId); // Optional channel selection
        }
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [openChat, closeChat]);

  return null; // This component only listens for events
}

3. App Integration

// app/layout.tsx or app/providers.tsx
import { ChatProvider } from '@curia_/curia-chat-modal';
import { ChatModalWrapper } from '@/components/ChatModalWrapper';
import { SidebarActionListener } from '@/components/SidebarActionListener';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ChatProvider>
          {/* Your app content */}
          {children}
          
          {/* Chat integration components */}
          <SidebarActionListener />
          <ChatModalWrapper />
        </ChatProvider>
      </body>
    </html>
  );
}

API Reference

ChatModal Props

interface ChatModalProps {
  ircCredentials: IrcCredentials;     // Pre-provisioned IRC credentials
  channel: ApiChatChannel;            // Pre-selected channel data
  chatBaseUrl?: string;               // Base URL for The Lounge IRC client
  theme?: 'light' | 'dark';          // Current app theme
  mode?: 'single' | 'normal';        // Channel display mode
  onClose: () => void;                // Close handler
}

interface IrcCredentials {
  success: boolean;
  ircUsername: string;
  ircPassword: string;
  networkName: string;
}

interface ApiChatChannel {
  id: number;
  community_id: string;
  name: string;
  description: string | null;
  irc_channel_name: string;
  is_single_mode: boolean;
  is_default: boolean;
  settings: ChatChannelSettings;
  created_at: string;
  updated_at: string;
}

Context API

// Chat context for modal state management
interface ChatContextType {
  isChatOpen: boolean;
  selectedChannelId: number | null;
  openChat: (channelId?: number) => void;
  closeChat: () => void;
}

// Usage
const { isChatOpen, openChat, closeChat } = useChatModal();

Environment Variables

# Required for local development
NEXT_PUBLIC_CHAT_BASE_URL=http://localhost:9000
NEXT_PUBLIC_CURIA_BASE_URL=http://localhost:3000

Features

  • 📱 Responsive Design: Desktop sidebar (25rem width), mobile full-screen
  • 🔄 Retry Logic: 3 attempts with exponential backoff (1s, 2s delays)
  • 🎨 Theme Support: Light/dark mode with CSS custom properties
  • ⌨️ Keyboard Navigation: ESC to close, proper focus management
  • 🔒 Secure Iframe: Sandboxed with necessary permissions
  • 🌐 Configurable URLs: Support for local development and production
  • 👥 Multi-Channel: Channel selection and single-channel mode
  • 🎯 Session-Aware: Pre-provisioned credentials for instant loading
  • 📢 Toast Notifications: Success/failure feedback via Sonner
  • 🔧 Error Recovery: Manual retry with comprehensive error handling

IRC Stack Requirements

This package is designed to work with:

  • IRC Server: Ergo IRCd or compatible
  • IRC Bouncer: Soju with admin interface enabled
  • Web Client: The Lounge with auto-login support
  • Backend API: IRC user provisioning endpoint
  • Database: Chat channels management system

Migration from Legacy Patterns

If migrating from a direct API-calling chat modal:

  1. Move IRC provisioning from modal to parent app session hook
  2. Implement channel management API endpoints
  3. Add retry logic with exponential backoff
  4. Update modal props to accept pre-provisioned data
  5. Add error handling modals and toast notifications
  6. Configure environment variables for local development

This architecture provides better performance, reliability, and user experience compared to per-modal-open provisioning patterns.