AJAX Support Chat with visitor tracker and invitation

Posted on Posted in AJAX, ASP.NET, C#

I really wanted to develop my own support chat with fully AJAX technology, would it be great that if the chat room would not do a single postback to server and get that annoying refresh sound. This Support Chat is a primary feature for me in my http://www.zendsms.com/ project; it’s a Microsoft Outlook 2007 Mobile Service. Therefore here I am with my Support Chat, below are some features that I had prioritized.

  1. Plain and simply
  2. Fully interactive AJAX site
  3. No database required
  4. Tracker visitor to the support chat page
  5. Invite him/here to chat with me if I want him/her too
  6. Send the chat transcript to the client, and CC also to me
  7. Convert URLs and Emails to a hyperlink
  8. Close a chat if I want too
  9. Lock the conversion if I logoff

First the client enter the support chat default page, he/her will see the normal pre-chat form to enter they name, email, and a question.

But I don’t want it to be that simple, its need to know that an operator is also online to ready to chat with the client. Here my solution, check the Chat class to see if an operator is online, if not disable the chat button and tell the client that the support representative is offline.

Now let’s enter into the chat room, if the client supply a name in the name textbox, or else use “client” as the client default name. now if the client has supply the question in the form, then show it as the first chat message.

Now if I enter an URL or an email in the message textbox, I want it to be render as a hyperlink also. This is how the enter url in normal life;

And that went shown in the chat log; it will be reader as a hyperlink, just as I wanted.

Let’s us see the admin side of things, I construct the admin site to 2 column (admit that 3 column with each for chat room, visitors, and chators would be better). You get the simple login on the top left which only need to enter the operator name (you do the security stuff yourself my friend).

This is the simple login for the operator

Now let’s us invite a visitor to chat with us. Just click on the IP address hyperlink for any visitor to the site that you want to invite him/her chat with

And you will get a popup message asking that you really want to invite this visitor ip address chat with.

Then suddenly in the visitor browser they will the popup alert chat invitation, if he/she admit the chat will start.

When the chat is over the client browser will be alert that the chat transcript has been sent to him/her email (and also CC you).

Now you have seen the features, let’s get into some coding. The structure of the web application is divided into 3 sections.

  1. The Sathai.Net.SupportChat namespace
  2. The client default page and it’s code-behide
  3. The admin page and it’s code-behide

The Sathai.Net.SupportChat namespace contain many components class, but mainly are Config, Chat, Mailer, and Visitor region.

The Config region manage the web application configuration of the entre web application, mostly you can let this class query the AppSetting in the Web.config file, but I just hard coded it in there (I known it not good practices).

Config.cs

using System;

///
/// Summary description for SupportChat
/// Author:     Sarin Na Wangkanai
/// Website:    http://www.sarin.mobi/
/// License:    GPL
///
namespace Sathai.Net.SupportChat
{
    public static class Config
    {
        public static string AdminDefaultName = "Support";
        public static string ClientDefaultName = "Client";
        public static string SmtpHost = "smtp.yourdomain.com";
        public static string SmtpUsername = "support@yourdomain.com";
        public static string SmtpPassword = "password";

        static Config() { }
    }
}

The Chat region of code contain 4 class, 1) Chat class, 2) ChatSession class, 3) ChatMessage class, and 4) ForceChat class

Chat.cs

using System;
using System.Collections;
using System.Threading;
using System.Text.RegularExpressions;

/// <summary>
/// Summary description for SupportChat
/// Author:     Sarin Na Wangkanai
/// Website:    http://www.sarin.mobi/
/// License:    GPL
/// </summary>
namespace Sathai.Net.SupportChat
{
    public static class Chat
    {
        private static bool allowNewChat = false; // change to true for testing only
        private static ArrayList chatSession = new ArrayList();
        private static ArrayList forceChat = new ArrayList();
        private static string adminName = Config.AdminDefaultName;

        public static bool AllowNewChat { get { return allowNewChat; } set { allowNewChat = value; } }
        public static ArrayList Chats { get { return chatSession; } set { chatSession = value; } }
        public static ArrayList Forcechat { get { return forceChat; } set { forceChat = value; } }
        public static string AdminName { get { return adminName; } set { adminName = value; } }

        static Chat()
        {
            Thread thread = new Thread(new ThreadStart(AutoRemove));
            thread.Start();
        }
        public static void ChatAdd(string ClientSessionID,string Client, string ClientEmail, string Admin)
        {
            int index = ChatIndex(ClientSessionID);
            if (index < 0)
                chatSession.Add(new ChatSession(ClientSessionID, Client, ClientEmail, Admin));
            else
                ChatAddMessage(ClientSessionID, "", "<span>Reconnecting session</span>",false);
        }
        public static void ChatAddMessage(string ClientSessionID,string Name, string Message, bool WaitForAdmin)
        {
            int index = ChatIndex(ClientSessionID);
            if (index >= 0)
                ((ChatSession)chatSession[index]).AddMessage(new ChatMessage(Name, Message), WaitForAdmin);
        }
        public static void ChatRemove(ChatSession ClientSession)
        {
            int index = ChatIndex(ClientSession.ClientSessionID);
            if (index >= 0)
            {
                // Client copy
                Mailer.Send(((ChatSession)Chats[index]).ClientEmail, Config.SmtpUsername, (ChatSession)Chats[index]);
                // Admin copy
                Mailer.Send(Config.SmtpUsername, Config.SmtpUsername, (ChatSession)Chats[index]);
                try { chatSession.RemoveAt(index); }
                catch { }
            }
        }
        public static int ChatIndex(string ClientSessionID)
        {
            int index = 0;
            foreach (ChatSession session in chatSession)
                if (session.ClientSessionID == ClientSessionID)
                    return index;
                else
                    index++;
            return -1;
        }
        public static void ForceAdd(string SessionID)
        {
            forceChat.Add(new ForceSession(SessionID));
        }
        public static void ForceRemove(string SessionID)
        {
            int index = ForceIndex(SessionID);
            if (index >= 0)
                forceChat.RemoveAt(index);
        }
        public static int ForceIndex(string SessionID)
        {
            int index = 0;
            foreach (ForceSession force in forceChat)
                if (SessionID == force.SessionID)
                    return index;

            return -1;
        }
        public static void AutoRemove()
        {
            while (true)
            {
                try
                {
                    int index = 0;
                    foreach (ChatSession session in chatSession)
                        if (session.Ticks > 20)
                            ChatRemove(session);
                        else
                            ((ChatSession)chatSession[index]).Ticks++;

                    index = 0;
                    foreach (ForceSession force in forceChat)
                        if (force.Ticks > 20)
                            ForceRemove(force.SessionID);
                        else
                            ((ForceSession)forceChat[index]).Ticks++;
                }
                catch { }
                Thread.Sleep(60 * 1000); // loop this call every 1 mintune;
            }
        }
    }
    public class ForceSession
    {
        private string sessionID;
        private int ticks = 0;

        public string SessionID { get { return sessionID; } set { sessionID = value; } }
        public int Ticks { get { return ticks; } set { ticks = value; } }

        public ForceSession() { }
        public ForceSession(string SessionID)
        {
            sessionID = SessionID;
        }

    }
    public class ChatSession
    {
        private string clientSessionID;
        private string clientName;
        private string clientEmail;
        private string adminName;
        private int ticks = 0;
        private bool waitForAdmin = false;
        private ArrayList message = new ArrayList();

        public string ClientSessionID { get { return clientSessionID; } set { clientSessionID = value; } }
        public string ClientName { get { return clientName; } set { clientName = value; } }
        public string ClientEmail { get { return clientEmail; } set { clientEmail = value; } }
        public string AdminName { get { return adminName; } set { adminName = value; } }
        public bool WaitForAdmin { get { return waitForAdmin; } set { waitForAdmin = value; } }
        public int Ticks { get { return ticks; } set { ticks = value; } }
        public ArrayList Message { get { return message; } set { message = value; } }

        public ChatSession() { }
        public ChatSession(string ClientSessionID, string Client, string ClientEmail, string Admin)
        {
            clientSessionID = ClientSessionID;
            clientName = Client;
            clientEmail = ClientEmail;
            adminName = Admin;
        }
        public void AddMessage(ChatMessage chatMessage, bool WaitForAdmin)
        { 
            message.Add(chatMessage);
            waitForAdmin = WaitForAdmin;
        }
    }
    public class ChatMessage
    {
        private string speakerName;
        private DateTime speakTime = DateTime.Now;
        private string message;

        public string SpeakerName { get { return speakerName; } set { speakerName = value; } }
        public DateTime SpeakTime { get { return speakTime; } set { speakTime = value; } }
        public string Message { get { return message; } set { message = value; } }

        public ChatMessage(string Name, string TheMessage)
        {
            speakerName = Name;
            message = ConvertUrlToLink(TheMessage);
        }

        private string ConvertUrlToLink(string str)
        {
            Regex regUrl = new Regex(@"(http:\/\/([\w.]+\/?)\S*)",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);
            Regex regEmail = new Regex(@"([a-zA-Z_0-9.-]+\@[a-zA-Z_0-9.-]+\.\w+)",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

            str = regUrl.Replace(str, "<a href=\"$1\" target=\"_blank\">$1</a>");
            str = regEmail.Replace(str, "<a href=mailto:$1>$1</a>");
            return str;
        }
    }
}

That Mailer class utilize the System.Net.Mail as it core, and format the message body, then when sending the message it get the smtp settings from Config class.

Mailer.cs

using System;
using System.Net.Mail;
using System.Net.Mime;
using System.Text;

/// <summary>
/// Summary description for SupportChat
/// Author:     Sarin Na Wangkanai
/// Website:    http://www.sarin.mobi/
/// License:    GPL
/// </summary>
namespace Sathai.Net.SupportChat
{
    public static class Mailer
    {
        static string css = "h4{color:#003366;}.Chating strong{font-weight: normal;color: #336699;}.Chating b{color: #800080;font-weight: bold;border-right: dotted 1px #800080;border-left: dotted 1px #800080;padding: 0px 5px 0px 5px;}.Chating span{font-style: italic;color: #808080;}";
        
        static Mailer() { }
        public static void Send(string To, string From, ChatMessage log)
        {
            StringBuilder html = new StringBuilder();
            StringBuilder text = new StringBuilder();
            
            html.AppendLine(string.Format("<strong>{0:HH:mm}</strong> | <b>{1}</b> | {2}<br />", log.SpeakTime, log.SpeakerName, log.Message));
            text.AppendLine(string.Format("{0:HH:mm} | {1} | {2}\n\r", log.SpeakTime, log.SpeakerName, log.Message));
         
            Send(To, From, html.ToString(), text.ToString());
        }
        public static void Send(string To, string From, ChatSession chat)
        {
            StringBuilder html = new StringBuilder();
            StringBuilder text = new StringBuilder();

            foreach (ChatMessage log in chat.Message)
            {
                html.AppendLine(string.Format("<strong>{0:HH:mm}</strong> | <b>{1}</b> | {2}<br />", log.SpeakTime, log.SpeakerName, log.Message));
                text.AppendLine(string.Format("{0:HH:mm} | {1} | {2}\n\r", log.SpeakTime, log.SpeakerName, log.Message));
            }
            Send(To, From, html.ToString(), text.ToString());
        }
        public static void Send(string To, string From, string BodyHtml, string BodyText)
        {
            try
            {
                StringBuilder html = new StringBuilder();
                html.AppendLine("<style type=\"text/css\">" + css + "</style>");
                html.AppendLine("<h4>Support Chat Log</h4>");
                html.AppendLine("<div class=\"Chating\">" + BodyHtml + "</div");

                StringBuilder text = new StringBuilder();
                text.AppendLine("Support Chat Log\n\r");
                text.AppendLine(BodyText);

                MailMessage mail = new MailMessage(From, To);

                mail.Subject = "Support Chat Log";
                mail.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(html.ToString(), new ContentType("text/html")));
                mail.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text.ToString(), new ContentType("text/plain")));

                SmtpClient smtp = new SmtpClient(Config.SmtpHost);
                if (Config.SmtpUsername.Length + Config.SmtpPassword.Length > 0)
                    smtp.Credentials = new System.Net.NetworkCredential(Config.SmtpUsername, Config.SmtpPassword);

                smtp.Send(mail);
            }
            catch (Exception ex) { throw ex; }
        }
    }
}

The Visitor class manages the online visitors onto our support chat site.

Visitor.cs

using System;
using System.Collections;
using System.Threading;

/// <summary>
/// Summary description for SupportChat
/// Author:     Sarin Na Wangkanai
/// Website:    http://www.sarin.mobi/
/// License:    GPL
/// </summary>
namespace Sathai.Net.SupportChat
{
    public static class Visitor
    {
        public static ArrayList list = new ArrayList();

        static Visitor()
        {
            Thread thread = new Thread(new ThreadStart(AutoRemove));
            thread.Start();
        }

        public static void Add(string SessionID, string IP)
        {
            int index = Index(SessionID);
            if (index < 0)
                list.Add(new VisitorInfo(SessionID, IP, true));
        }
        public static void Remove(string SessionID)
        {
            int index = Index(SessionID);
            if (index >= 0)
                list.RemoveAt(index);
        }
        public static void AutoRemove()
        {
            while (true)
            {
                try
                {
                    int index = 0;
                    foreach (VisitorInfo info in list)
                        if (info.Ticks > 0)
                            Remove(info.SessionID);
                        else
                            ((VisitorInfo)list[index]).Ticks++;
                }
                catch { }
                Thread.Sleep(60 * 1000); // loop the every 1 minute
            }
        }
        public static int Index(string SessionID)
        {
            int index = 0;
            foreach (VisitorInfo info in list)
                if (info.SessionID == SessionID)
                    return index;
            return -1;
        }
    }
    public class VisitorInfo
    {
        private string sessionID;
        private string ip;
        private int ticks = 0;
        private bool chating = false;

        public string SessionID { get { return sessionID; } set { sessionID = value; } }
        public string IP { get { return ip; } set { ip = value; } }
        public int Ticks { get { return ticks; } set { ticks = value; } }
        public bool Chating { get { return chating; } set { chating = value; } }

        public VisitorInfo() { }
        public VisitorInfo(string SessionID, string IP, bool Chating)
        {
            sessionID = SessionID;
            ip = IP;
            chating = Chating;
        }
    }
}

Let talk about the Default.aspx, as from the presentation from the beginning you can what does it do. Main that our website contain multiple UpdatePanel sections, in which will response to difference status of that page, ranging from pre chat client information form, chat room, chat invitation, and thank you.

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Support Chat</title>
    <style type="text/css">
        .Main
        {
        	font-family: Verdana, Tahoma, Sans-Serif;
        	font-size: small;
        }
        .Main h2{
            color: #003366;
        }
        .Loading{
            width: 400px;
            padding: 20px;
            background-color: #FFFFCC;
            border: 1px dashed #FFCC66;
            color: #CC6600;
            font-weight: bold;
            text-align: center;
            text-decoration: blink;
        }
        .ForceChat
        {
        	width: 400px;
            border: 1px dashed #336699;
            padding: 20px;
            background-color: #CCCCFF;
        }
        .FormTable {
            width: 100%;
        }
        .ChatingBar{
            text-align: right;
            background-color: #336699;
            width: 422px;
        }
        .Chating {
            display: block;
            clear: both;
            width: 400px;
            height: 350px;
            border: 1px solid #808080;
            padding: 10px;
        }
        .Chating strong{
            font-weight: normal;
            color: #336699;
        }
        .Chating b{
            color: #800080;
            font-weight: bold;
            border-right: dotted 1px #800080;
            border-left: dotted 1px #800080;
            padding: 0px 5px 0px 5px;
        }
        .Chating span{
            font-style: italic;
            color: #808080;
        }
        .ChatingSubmit{
            width: 422px;
            white-space: nowrap;
            word-spacing: 0;
        }
        .input{ margin:0px;}
    </style>
    <!-- script type="text/javascript">
        function scrollPanel(panel)
        {
            panel.scrollTop = panel.scrollHeight;
        }
    </script -->
</head>
<body>
    <form id="form1" runat="server">
    <div class="Main">
        <asp:ScriptManager ID="ScriptManager1" runat="server"/>
        <h2>
            Support Chat</h2>
        <p>
            Developed by Sarin Na Wangkanai, author of&nbsp;
            <a href="http://www.sarin.mobi/">http://www.sarin.mobi/</a>
        </p>
        <asp:UpdateProgress ID="UpdateProgress1" runat="server">
            <ProgressTemplate>
                <div class="Loading">Loading...</div>
            </ProgressTemplate>
        </asp:UpdateProgress>
        <asp:UpdatePanel ID="UpdatePanelForceChat" runat="server" UpdateMode="Conditional">
            <ContentTemplate>
                <asp:Panel ID="PanelForceChat" runat="server" CssClass="ForceChat" 
                    Visible="False">
                    <p>
                        Our support team would like to assist you. Do you want to chat with him?</p>
                    <p>
                        <asp:Button ID="ButtonForceYes" runat="server" Text="Yes" Width="60px" 
                            onclick="ButtonForceYes_Click" />
                        &nbsp;&nbsp;&nbsp;
                        <asp:Button ID="ButtonForceNo" runat="server" Text="No" Width="60px" 
                            onclick="ButtonForceNo_Click" />
                    </p>
                </asp:Panel>
                <asp:Panel ID="PanelThankyou" runat="server" CssClass="ForceChat" Visible="false">
                    <asp:Label ID="LabelThankyou" runat="server"></asp:Label>
                </asp:Panel>
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
            </Triggers>
        </asp:UpdatePanel>
        <asp:UpdatePanel ID="UpdatePanelOpen" runat="server" UpdateMode="Conditional">
            <ContentTemplate>                    
                <asp:Panel ID="PanelOpen" runat="server" 
                    DefaultButton="ButtonChat">
                    <table cellpadding="5" class="FormTable">
                        <tr>
                            <td width="100">
                                Name:</tdName:</td>
                            <td>
                                <asp:TextBox ID="TextBoxName" runat="server" Width="300px"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                Email:</td>
                            <td>
                                <asp:TextBox ID="TextBoxEmail" runat="server" Width="300px"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td valign="top">
                                Question:</td>
                            <td>
                                <asp:TextBox ID="TextBoxQuestion" runat="server" Height="200px" 
                                    TextMode="MultiLine" Width="300px"></asp:TextBox>
                            </td>
                        </tr>
                        <tr>
                            <td>
                                &nbsp;</td>
                            <td>
                                <asp:UpdatePanel ID="UpdatePanelOpenButton" runat="server" 
                                    UpdateMode="Conditional">
                                    <ContentTemplate>
                                        <asp:Button ID="ButtonChat" runat="server" Text="Chat" Width="100px" 
                                            onclick="ButtonChat_Click" />
                                        &nbsp;&nbsp;&nbsp;
                                        <asp:Button ID="ButtonEmail" runat="server" Text="Email" Width="100px" 
                                            onclick="ButtonEmail_Click" />
                                        <br />
                                        <asp:Label ID="LabelMessage" runat="server"></asp:Label>                                    
                                    </ContentTemplate>
                                    <Triggers>
                                        <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
                                    </Triggers>
                                </asp:UpdatePanel>

                            </td>
                        </tr>
                    </table>
                </asp:Panel>
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="ButtonChat" EventName="Click" />
                <asp:AsyncPostBackTrigger ControlID="ButtonEmail" EventName="Click" />
                <asp:AsyncPostBackTrigger ControlID="ButtonForceYes" EventName="Click" />
            </Triggers>
        </asp:UpdatePanel>
        <asp:UpdatePanel ID="UpdatePanelChat" runat="server" UpdateMode="Conditional">
            <ContentTemplate>               
                <asp:Panel ID="PanelChat" runat="server" DefaultButton="ButtonSubmit" 
                    Visible="False">
                    <div class="ChatingBar">
                        <asp:Button ID="ButtonChatClose" runat="server" Text="Close" 
                            BackColor="#336699" Font-Bold="True" ForeColor="White" 
                            onclick="ButtonChatClose_Click" /></div>

                            <asp:Panel ID="PanelChating" runat="server" CssClass="Chating" 
                                ScrollBars="Vertical"> 
                                <asp:UpdatePanel ID="UpdatePanelChatBoard" runat="server" 
                                    UpdateMode="Conditional">
                                    <ContentTemplate>                                
                                <asp:Repeater ID="RepeaterChat" runat="server">
                                    <ItemTemplate>
                                        <strong><%# DataBinder.Eval(Container.DataItem,"SpeakTime","{0:HH:mm}") %></strong>&nbsp;
                                        <b><%# DataBinder.Eval(Container.DataItem, "SpeakerName") %></b>&nbsp;
                                        <%# DataBinder.Eval(Container.DataItem, "Message")%><br />
                                    </ItemTemplate>
                                </asp:Repeater>
                                </ContentTemplate>
                                <Triggers>
                                    <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
                                </Triggers>
                            </asp:UpdatePanel>                                 
                            </asp:Panel>                         

                            <div class="ChatingSubmit">                  
                                <asp:TextBox ID="TextBoxChatMessage" runat="server" Width="288px" 
                                    CssClass="input"></asp:TextBox>
                                <asp:UpdatePanel ID="UpdatePanel1" runat="server">
                                    <ContentTemplate>
                                <asp:Button ID="ButtonSubmit" runat="server" Text="Submit" Width="60px" 
                                    CssClass="input" onclick="ButtonSubmit_Click" />
                                <asp:Button ID="ButtonClear" runat="server" Text="Clear" Width="60px" 
                                    CssClass="input" onclick="ButtonClear_Click" />
                                    </ContentTemplate>
                                </asp:UpdatePanel>                                     
                            </div>    
                </asp:Panel>            
            </ContentTemplate>
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="ButtonSubmit" EventName="Click" />
                <asp:AsyncPostBackTrigger ControlID="ButtonClear" EventName="Click" />
                <asp:AsyncPostBackTrigger ControlID="ButtonForceYes" EventName="Click" />
                <asp:AsyncPostBackTrigger ControlID="ButtonChat" EventName="Click" />
            </Triggers>
        </asp:UpdatePanel>
        <asp:Timer ID="Timer1" runat="server" Interval="1000" OnTick="Timer1_Tick">
        </asp:Timer>
    </div>
    </form>
</body>
</html>

Default.aspx.cs

using System;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sathai.Net.SupportChat;

public partial class _Default : System.Web.UI.Page 
{
    public bool ChatActive
    {
        get
        {
            if (Session["ChatActive"] != null)
                return (bool)Session["ChatActive"];
            else
                return false;
        }
        set
        {
            Session["ChatActive"] = value;
        }
    }
    public string ThankyouWord
    {
        get
        {
            if (Session["ThankyouWord"] != null)
                return (string)Session["ThankyouWord"];
            else
                return "Thank you, a copy the chat will be email to you shortly.";
        }
        set
        {
            Session["ThankyouWord"] = value;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
            Visitor.Add(Session.SessionID, Request.UserHostAddress);
          
        
        if (Chat.AllowNewChat)
        {
            ButtonEmail.Enabled = true;
            ButtonSubmit.Enabled = true;
            ButtonChat.Enabled = true;
            LabelMessage.Text = "";
        }
        else
        {
            LabelMessage.Text = "Sorry! out support team is offline now, please leave us a message.";
            ButtonChat.Enabled = false;     
            ButtonSubmit.Enabled = false;
        }

        if (Chat.ForceIndex(Session.SessionID) >= 0)
            PanelForceChat.Visible = true;
        else
            PanelForceChat.Visible = false;

        LabelThankyou.Text = ThankyouWord;
         
    }
    protected void Timer1_Tick(object sender, EventArgs e)
    {
        if (ChatActive)
        {
            try
            {
                RepeaterChat.DataSource = ((ChatSession)Chat.Chats[Chat.ChatIndex(Session.SessionID)]).Message;
                RepeaterChat.DataBind();
            }
            catch { ChatActive = false; }

            /*
            ScriptManager.RegisterStartupScript(
                PanelChating, PanelChating.GetType(), "scrollPanel",
                "scrollPanel(" + PanelChating.ClientID.ToString() + ");",
                true);
             */ 
        }
    }
    protected void ButtonChat_Click(object sender, EventArgs e)
    {
        PanelOpen.Visible = false;
        Chat.ChatAdd(Session.SessionID, (TextBoxName.Text.Length > 0) ? TextBoxName.Text : Config.ClientDefaultName, (TextBoxEmail.Text.Length > 0) ? TextBoxEmail.Text : "unknown", Chat.AdminName);
        Chat.ChatAddMessage(Session.SessionID, "", "<span>Connection starting...</span>", true);
        if (TextBoxQuestion.Text.Length > 0)
            Chat.ChatAddMessage(Session.SessionID, ((ChatSession)Chat.Chats[Chat.ChatIndex(Session.SessionID)]).ClientName, TextBoxQuestion.Text, true);
        ChatActive = true;
        PanelChat.Visible = true;
    }
    protected void ButtonEmail_Click(object sender, EventArgs e)
    {
        ChatMessage message = new ChatMessage(TextBoxName.Text, TextBoxQuestion.Text);
        try
        {
            // Client copy
            Mailer.Send(TextBoxEmail.Text, Config.SmtpUsername, message);
            // Admin copy
            Mailer.Send(Config.SmtpUsername, Config.SmtpUsername, message);
        }
        catch
        {
            LabelThankyou.Text = "Thank you, for your interest with us, but due to you have not supply us with your email we could not send you copy of this chat";
        }
        PanelOpen.Visible = false;
        PanelThankyou.Visible = true;
    }
    protected void ButtonForceYes_Click(object sender, EventArgs e)
    {
        PanelForceChat.Visible = false;
        Chat.ChatAdd(Session.SessionID, (TextBoxName.Text.Length > 0) ? TextBoxName.Text : Config.ClientDefaultName, (TextBoxEmail.Text.Length > 0) ? TextBoxEmail.Text : "unknown", Chat.AdminName);
        Chat.ChatAddMessage(Session.SessionID, "", "<span>Connection starting...</span>",true );
        ChatActive = true;
        PanelOpen.Visible = false;
        PanelChat.Visible = true;
        Chat.ForceRemove(Session.SessionID);
    }
    protected void ButtonForceNo_Click(object sender, EventArgs e)
    {
        PanelForceChat.Visible = false;
        Chat.ForceRemove(Session.SessionID);
    }
    protected void ButtonSubmit_Click(object sender, EventArgs e)
    {
        try
        {
            if (Chat.AllowNewChat)
                Chat.ChatAddMessage(Session.SessionID, ((ChatSession)Chat.Chats[Chat.ChatIndex(Session.SessionID)]).ClientName, TextBoxChatMessage.Text, true);
            else
            {
                Chat.ChatAddMessage(Session.SessionID, "", "<span>The support representation is offline</span>", true);
                ButtonSubmit.Enabled = false;
            }
        }
        catch
        {
            ThankyouWord = "Sorry! your session has expire, please reflash this page to start again.";
            PanelChat.Visible = false;
            PanelThankyou.Visible = true;
        }
        TextBoxChatMessage.Text = "";
    }
    protected void ButtonClear_Click(object sender, EventArgs e)
    {
        TextBoxChatMessage.Text = "";
    }
    protected void ButtonChatClose_Click(object sender, EventArgs e)
    {
        ChatActive = false;
        PanelThankyou.Visible = true;
        PanelChat.Visible = false;
        try
        {
            Chat.ChatAddMessage(Session.SessionID, "", "<span>discounted by client</span>", false);
            Chat.ChatRemove(new ChatSession() { ClientSessionID = Session.SessionID });
        }
        catch
        {
            LabelThankyou.Text = "Thank you, for your interest with us, but due to you have not supply us with your email we could not send you copy of this chat";
        }
    }
}

Great now the Admin.aspx, as from presentation above, you can see that our admin part is more complex then the client part, you can change chat room on the fly, and you are not lock to the specific SessionID like the client user.

Admin.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Admin.aspx.cs" Inherits="Admin" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Support Chat Admin</title>
    <style type="text/css">
        .Main
        {
        	font-family: Verdana, Tahoma, Sans-Serif;
        	font-size: small;
        }
        .Main h2{
            color: #003366;
        }
        .Main h4{
            color: #660066;
        }
        .AdminTable{ width:100%;}
        .Chat{}
        .Chating {
            display: block;
            clear: both;
            border: 1px solid #808080;
            padding: 10px;
            height: 400px;
        }
        .Chating strong{font-weight: normal;color: #336699;}
        .Chating b{color: #800080;font-weight: bold;border-right: dotted 1px #800080;border-left: dotted 1px #800080;padding: 0px 5px 0px 5px;}
        .Chating span{font-style: italic;color: #808080;}
        .PanelVisitor{}
        .PanelVisitor ul{
            list-style-type: none;
            margin-left: 0px;
        }
        .PanelVisitor ul li{
            padding: 3px 5px 3px 5px;
            border: 1px solid #9999FF;
        }
        .PanelVisitor ul li a:link,
        .PanelVisitor ul li a:visited
        {
            color: #336699;
        }
        .PanelVisitor ul li a:hover
        {
        	color: #FF9900;
        }
        
        .PanelChator{}
        .PanelChator ul{
            list-style-type: none;
            margin-left: 0px;
        }
        .PanelChator ul li{
            padding: 3px 5px 3px 5px;
            border: 1px solid #9999FF;
        }
        .WaitForAdmin{
            padding: 3px 5px 3px 5px;
            border: 1px solid #9999FF;
            background-color: #FFCC66;
        }
        .PanelChator ul li a:link,
        .PanelChator ul li a:visited
        {
            color: #336699;
        }
        .PanelChator ul li a:hover
        {
        	color: #FF9900;
        }
        
        
    </style>
    <script type="text/javascript">
    function AskToChat(ip)
    {
        if(confirm("Do you want to chat with "+ip+"?")==true)
            return true;
        else
            return false;
    }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div class="Main">
        <asp:ScriptManager ID="ScriptManager1" runat="server" />
        <h2>Support Chat Admin</h2>
        <table cellpadding="0" cellspacing="0" class="AdminTable">
            <tr>
                <td width="200px" valign="top">
                    <asp:UpdatePanel ID="UpdatePanelStatus" runat="server" UpdateMode="Conditional">
                        <ContentTemplate>
                            <h4>Status</h4>
                            <%if (!Sathai.Net.SupportChat.Chat.AllowNewChat)
                              { %>
                            <p>Enter admin name
                                <asp:TextBox ID="TextBoxAdminName" runat="server" Text="sarin"></asp:TextBox>
                                <asp:Button ID="ButtonLogin" runat="server" Text="Login" 
                                    onclick="ButtonLogin_Click" />
                            </p>
                            <%}
                              else
                              { %>
                              Welcome 
                            <asp:Label ID="LabelAdminName" runat="server" Font-Bold="true" Text=""></asp:Label><br />
                            <asp:Button ID="ButtonLogout" runat="server" Text="Logout" 
                                onclick="ButtonLogout_Click" />
                            <%} %>
                        </ContentTemplate>
                    </asp:UpdatePanel>
                    <asp:UpdatePanel ID="UpdatePanelItem" runat="server">
                        <ContentTemplate>
                            <asp:Panel ID="PanelChator" runat="server" CssClass="PanelChator" Height="200px" ScrollBars="Vertical">
                                <h4>Chators</h4>
                                <ul>
                                <asp:Repeater ID="RepeaterChator" runat="server" OnItemDataBound="RepeaterChator_ItemDataBound">
                                    <ItemTemplate>                                        
                                        <li id="Listing" runat="server">
                                            <asp:LinkButton ID="LinkButtonChator" OnClick="LinkButtonChator_Click" runat="server"><%# DataBinder.Eval(Container.DataItem, "ClientName")%></asp:LinkButton></li>
                                    </ItemTemplate>
                                </asp:Repeater>
                                </ul>
                            </asp:Panel>
                            <asp:Panel ID="PanelVisitor" runat="server" CssClass="PanelVisitor" 
                                Height="200px" ScrollBars="Vertical">
                                <h4>Visitors</h4>
                                <ul>                            
                                <asp:Repeater ID="RepeaterVisitor" runat="server"  OnItemDataBound="RepeaterVisitor_ItemDataBound" >
                                    <ItemTemplate>
                                        <li>
                                            <asp:LinkButton ID="LinkButtonVisitor" OnClick="LinkButtonVisitor_Click" runat="server"><%# DataBinder.Eval(Container.DataItem,"IP") %></asp:LinkButton></li>
                                    </ItemTemplate>
                                </asp:Repeater>
                                </ul>
                            </asp:Panel>
                        </ContentTemplate>
                        <Triggers>
                            <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
                        </Triggers>
                    </asp:UpdatePanel>
                    <asp:Timer ID="Timer1" runat="server" Interval="1000">
                    </asp:Timer>                 
                </td>
                <td valign="top">
                    <asp:UpdatePanel ID="UpdatePanelChatBoard" runat="server" 
                        UpdateMode="Conditional">
                        <ContentTemplate>
                            <asp:Panel ID="PanelChat" runat="server" CssClass="Chat" DefaultButton="ButtonSubmit">
                                <asp:Panel ID="PanelChating" runat="server" CssClass="Chating" 
                                    ScrollBars="Vertical">                       
                                    <asp:UpdatePanel ID="UpdatePanelChating" runat="server" 
                                        UpdateMode="Conditional">
                                        <ContentTemplate>
                                        <asp:Repeater ID="RepeaterChat" runat="server">
                                                <ItemTemplate>
                                                    <strong><%# DataBinder.Eval(Container.DataItem,"SpeakTime","{0:HH:mm}") %></strong>&nbsp;
                                                    <b><%# DataBinder.Eval(Container.DataItem, "SpeakerName") %></b>&nbsp;
                                                    <%# DataBinder.Eval(Container.DataItem, "Message")%><br />
                                                </ItemTemplate>
                                            </asp:Repeater>
                                        </ContentTemplate>
                                        <Triggers>
                                            <asp:AsyncPostBackTrigger ControlID="Timer1" EventName="Tick" />
                                        </Triggers>
                                    </asp:UpdatePanel> 
                                </asp:Panel>                                                       
                                <div>
                                    <asp:TextBox ID="TextBoxMessage" runat="server" Width="300px"></asp:TextBox>
                                    <asp:Button ID="ButtonSubmit" runat="server" Text="Submit" 
                                        onclick="ButtonSubmit_Click" />
                                    <asp:Button ID="ButtonClear" runat="server" Text="Clear" 
                                        onclick="ButtonClear_Click" />
                                    <asp:Button ID="ButtonEnd" runat="server" Text="End" 
                                        onclick="ButtonEnd_Click" />
                                </div>
                            </asp:Panel>
                        </ContentTemplate>
                        <Triggers>
                            <asp:AsyncPostBackTrigger ControlID="ButtonSubmit" EventName="Click" />
                            <asp:AsyncPostBackTrigger ControlID="ButtonClear" EventName="Click" />
                            <asp:AsyncPostBackTrigger ControlID="ButtonEnd" EventName="Click" />
                        </Triggers>
                    </asp:UpdatePanel>                    
                </td>
            </tr>
        </table>
    </div>
    </form>
</body>
</html>

Admin.aspx.cs

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using Sathai.Net.SupportChat;

public partial class Admin : System.Web.UI.Page
{
    public int CurrentChatIndex
    {
        get
        {
            if (Session["CurrentChatIndex"] != null)
                return (int)Session["CurrentChatIndex"];
            else
                return -1;
        }
        set
        {
            Session["CurrentChatIndex"] = value;
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
            CurrentChatIndex = -1;

        LabelAdminName.Text = Chat.AdminName;

        RepeaterVisitor.DataSource = Visitor.list;
        RepeaterVisitor.DataBind();

        RepeaterChator.DataSource = Chat.Chats;
        RepeaterChator.DataBind();

        if (CurrentChatIndex >= 0 && Chat.Chats.Count > 0)
        {
            try
            {
                RepeaterChat.DataSource = ((ChatSession)Chat.Chats[CurrentChatIndex]).Message;
                RepeaterChat.DataBind();
            }
            catch { CurrentChatIndex--; }
        }
    }
    protected void ButtonSubmit_Click(object sender, EventArgs e)
    {
        if (CurrentChatIndex >= 0 && Chat.Chats.Count > 0)
            Chat.ChatAddMessage(((ChatSession)Chat.Chats[CurrentChatIndex]).ClientSessionID, Chat.AdminName, TextBoxMessage.Text, false);
        TextBoxMessage.Text = "";
    }
    protected void ButtonClear_Click(object sender, EventArgs e)
    {
        TextBoxMessage.Text = "";
    }
    protected void ButtonEnd_Click(object sender, EventArgs e)
    {
        Chat.ChatAddMessage(((ChatSession)Chat.Chats[CurrentChatIndex]).ClientSessionID, "", "<span>discounted by admin</span>", false);
        try
        {
            Chat.ChatRemove((ChatSession)Chat.Chats[CurrentChatIndex]);
        }
        catch { }
    }
    protected void ButtonLogin_Click(object sender, EventArgs e)
    {
        Chat.AdminName = TextBoxAdminName.Text;
        Chat.AllowNewChat = true;
        for (int i = 0; i < Chat.Chats.Count; i++)
            ((ChatSession)Chat.Chats&#91;i&#93;).AddMessage(new ChatMessage("", "<span>The support representative is now online</span>"), false);

    }
    protected void ButtonLogout_Click(object sender, EventArgs e)
    {
        Chat.AllowNewChat = false;
        for (int i = 0; i < Chat.Chats.Count; i++)
            ((ChatSession)Chat.Chats&#91;i&#93;).AddMessage(new ChatMessage("", "<span>The support representative has logoff</span>"), false);
    }
    protected void RepeaterVisitor_ItemDataBound(object source, RepeaterItemEventArgs e)
    {
        LinkButton item = (LinkButton)e.Item.FindControl("LinkButtonVisitor");
        item.OnClientClick = "AskToChat(\"" + ((VisitorInfo)e.Item.DataItem).IP + "\");";
        item.ToolTip = ((VisitorInfo)e.Item.DataItem).SessionID;
    }
    protected void RepeaterChator_ItemDataBound(object source, RepeaterItemEventArgs e)
    {
        LinkButton item = (LinkButton)e.Item.FindControl("LinkButtonChator");
        item.ToolTip = ((ChatSession)e.Item.DataItem).ClientSessionID;
        item.Text = ((ChatSession)e.Item.DataItem).ClientName;
        if (((ChatSession)e.Item.DataItem).WaitForAdmin)
        {
            HtmlControl listing = (HtmlControl)e.Item.FindControl("Listing");
            listing.Attributes.Add("class", "WaitForAdmin");
        }
    }
    protected void LinkButtonVisitor_Click(object sender, EventArgs e)
    {
        string ip = ((LinkButton)sender).Text;
        string SessionID = ((LinkButton)sender).ToolTip;

        Chat.ForceAdd(SessionID);
    }
    protected void LinkButtonChator_Click(object sender, EventArgs e)
    {
        string SessionID = ((LinkButton)sender).ToolTip;

        CurrentChatIndex = Chat.ChatIndex(SessionID);
    }
}

I hope this support chat that I have created will be a base foundation for your use in your own project, mine I have the invitation build-in my website master page, and the client with a windows popup to let the client maintain browsing my website while chatting with me. Also when I login to my admin section of my website, new chat alert will notify me with client name on the left of my browser with sound alert.

But most of all this is great post and most informative to you as possible.