Group Chat

Chat Text

The total chat app, including the ask/answer component for soliciting a name comments, etc. is 68 lines of code. There is no special code to support AJAX/Comet (all the wrapping is done automatically by lift.

When the Chat comet widget is added to the page, it needs to solict the user for a "chat name". It asks the "AskName" comet widget for the name. Until the AskName comet widget provides a name, all rendering messages are forwarded to AskName. Here's the code for the "AskName":

class AskName extends CometActor {
  def render = ajaxForm(<div>What is your username?</div> ++ 
                             text("",name => answer(name.trim)) ++ 
                             submit("Enter", ignore => true))
}

When the user submits the form, the question asked by the Chat comet widget is answered with the value the user submitted. This is similar to the ask/answer paradigm in Seaside, except that there's no need for continuations.

Now, onto the heart of the chat app:

class Chat extends CometActor {
  private var userName = ""
  private var currentData: List[ChatLine] = Nil
  
  private val server = {
    val ret = ChatServer.server
    ret !? ChatServerAdd(this) match {
      case ChatServerUpdate(value) => currentData = value
      case _ => {}
    }
    ret
  }
  

  override def lowPriority : PartialFunction[Any, Unit] = {
      case ChatServerUpdate(value) => {
        currentData = value
        reRender
        loop
      }
    } 
  
  def render = {
    val inputName = this.uniqueId+"_msg"
    val ret = <span>Hello {userName}<ul>{
      currentData.reverse.map{
        cl =>
        <li>{hourFormat(cl.when)} {cl.user}: {cl.msg}</li>
      }.toList
    }</ul><lift:form method="POST">
    <input name={inputName} type="text" value=""/><input value="Send" type="submit"/>
    </lift:form></span>

    XmlAndMap(ret, TreeMap(inputName -> sendMessage))
  }
  
  override def localSetup {
    if (userName.length == 0) {
    ask(new AskName, "what's your username") {
      answer =>
      answer match {
        case s : String if (s.length > 2) => userName = s; true
        case _ => localSetup; false
      }
    }
    }
  }
  
  def waitForUpdate : Option[List[ChatLine]] = {
    receiveWithin(100) {
      case ChatServerUpdate(l) => Some(l)
      case TIMEOUT => None
    }
  }
  
  def sendMessage(in: List[String]) = {
      server ! ChatServerMsg(userName, in.head)
      waitForUpdate match {
        case Some(l : List[ChatLine]) => currentData = l ; true
        case _ => false
      }
    }
}

server is a value (calculated at instantiation and unchanged) that contains the Chat server shared by all the Chat comet clients.

lowPriority defines what happens when we receive a message from another Actor. In this case, when the Chat server receives a new posting, it sends a message to all the Chat comet wigets. When the Chat comet widget receives the message, it notifies the containing page (via reRender) of the new content.

render yields the XHTML and parameter to function map.

localSetup is called when the comet widget is instantiated or when it joins a new page (comet widgets can be shared across pages). Chat comet widget creates an AskName and askes it a question. When AskName "answers" the question, the attached function will be called with the Answer to the question.

sendMessage is called when the user submits the form (enters a line in chat). It sends a message to the Chat server, waits up to 100ms for the Chat server to send an update. If the Chat server sends an update, the Chat client updates itself.

This example demonstrates the power of Scala's Actors and lift. With very few lines of code, we've got a complete AJAX/Comet app that has Seaside style Ask/Answer for building modal dialogs.