Section 14.2. Under the Hood of the Chat Component

14.2. Under the Hood of the Chat Component

In this section, we look at the code of the Chat component that ships with FlashCom to understand how it works, and we enhance it with some more functionality.

14.2.1. The Server-Side Code

The server-side code for the Chat component is pretty simple, but it's worth examining. Note that some code has been removed for brevity. Look at chat.asc in your scriptlib/ components folder for the full code.

The code begins with a try/catch block, which ensures that the component is defined only once:

 try {var dummy = FCChat;} catch (e) { // #ifndef FCChat 

If the FCChat class is already defined, we do nothing (assign a reference to the class to a dummy variable); otherwise , we define it. This emulates the #ifndef (if not defined) directive of the C language, which Server-Side ActionScript (SSAS) does not support.

Next, we load components/component.asc , the base class for components:

 load("components/component.asc"); 

This class contains a few methods that make it easier for component developers to focus on the functionality of the component itself, rather than on how to interface it with the rest of the framework.

Next comes the FCChat class constructor:

 FCChat = function (name) {   this.init(name);   // Get a non-persistent shared object for sending broadcasts.   this.message_so = SharedObject.get(this.prefix + "message", false);   // If   persist   is true, then get the history back.   this.history_so = SharedObject.get(this.prefix + "history", this.persist);   this.history = this.history_so.getProperty("history");   if (this.history == null)     this.history = new Array; }; 

The constructor is a great place to get resources that will be needed by the component during its lifetime. In this case, we get two shared objects whose names begin with the prefix property, which is a unique identifier defined for the component in the superclass when we call this.init(name) .

This ensures that multiple copies of the Chat component used in the same application won't conflict (we'll also use the prefix property to route calls from the server to the clients and vice versa).

The message shared object is non-persistent and is used to send new messages to all the clients as they arrive (as we will see in the sendMessage( ) definition later):

 this.message_so = SharedObject.get(this.prefix + "message", false); 

The history shared object is persistent ( assuming the persist property is true, its default) and is used to save the chat's history in an array:

 this.history_so = SharedObject.get(this.prefix + "history", this.persist); 

The following line defines this component as a subclass of FCComponent . (If you're not familiar with prototype-based inheritance, see ActionScript for Flash MX: The Definitive Guide (O'Reilly).):

 FCChat.prototype = new FCComponent("FCChat", FCChat); 

These lines define some default values for the component:

 FCChat.prototype.histlen    = 250;        // Maximum history length FCChat.prototype.persist    = true;       // Whether to save history FCChat.prototype.allowClear = true;       // Allow clients to clear history FCChat.prototype.history    = new Array;  // History FCChat.prototype.message_so = null;       // Message broadcasts FCChat.prototype.history_so = null;       // History persistence 

The onAppStop( ) method is called on our component automatically when the application is about to get unloaded by garbage collection. At that time, a component should flush all its data that's worth saving to disk. The Chat component flushes the chat's history stored in the history shared object:

 // This is called when the application is about to stop. FCChat.prototype.onAppStop = function (  ) {   if (this.persist && this.history_so != null) {     this.history_so.setProperty("history", this.history);     this.history_so.flush( );   } }; 

The connect( ) method is the first one called by each client when it connects to the application. In it, we return the history (stored in the component's history property) through a server-to-client call (routed through this.callPrefix ), and also call setUsername( ) on the client:

 // Methods that a client-side component calls explicitly. // The first method called by a client component. FCChat.prototype.connect = function (client) {   var cglobal = this.getClientGlobalStorage(client);   if (!cglobal.usercolor) {     cglobal.usercolor = "0x000000";   }   client.call(this.callPrefix + "receiveHistory", null, this.history);   client.call(this.callPrefix + "setUsername", null, cglobal.username); }; 

The close( ) method can be called by a client when the component unloads ( onUnload( ) ):

 // The last method called by a client component. FCChat.prototype.close = function (client) { }; 

Note that this is not the same as onDisconnect( ) , which gets called when the server realizes that the user has disconnected from the application. The onUnload( ) method gets called on a movie clip when the movie goes to a different frame that doesn't contain it or the movie clip is unloaded with removeMovieClip( ) or unloadMovie( ) . In this particular component, we don't need to do anything in the close( ) handler, but some other components might clean up client-specific variables or flush them to disk.

The sendMessage( ) method is invoked by a client when it wants to send a new chat message:

 // Send a message to all others participating in the chat session. FCChat.prototype.sendMessage = function (client, mesg) {   var cglobal = this.getClientGlobalStorage(client);   mesg = this.hiliteURLs(mesg);   var hexColor = "#" + cglobal.usercolor.substring(2, cglobal.usercolor.length)   mesg = "<font color=\"" + hexColor + "\"><b>" + cglobal.username + ": </b>" +           mesg + "</font><br>\n";   this.history.push(mesg);   while (this.history.length > this.histlen)     this.history.shift( );   this.message_so.send("message", mesg); }; 

The message to be sent, mesg , is first passed through the hiliteURLs( ) utility function (not shown), which turns any URLs in the message into hyperlinks (wrapping them in <a> tags).

The code stylizes the text with the color associated with the user who sent the message; this information is stored in cglobal.usercolor (we'll talk more about the client's global storage later). The code prefixes the message with the sender's username and appends the message to the history array. For performance reasons the data is kept in memory in the history array, and is only saved to the history_so shared object, and therefore to disk, only when the application unloads (see onAppStop( ) , discussed earlier).

The while loop limits the lines of chat saved based on histlen .

The sendMessage( ) method finally uses SharedObject.send( ) to broadcast the new message to every client connected to the chat.

By now, the chat's server-side architecture should be clear:

  • It saves the history in a server-side array ( this.history ). The data isn't written to disk until the application stops.

  • The history is updated by appending each new message as it arrives.

  • When a new client connects, the component sends it the current history through a server-to-client receiveHistory( ) call.

This architecture is bandwidth efficient: one big lump of data (the chat's history) is sent to each client when he first connects, but each client receives only new messages thereafter.

The clearHistory( ) method allows clients to clear the chat's history:

 FCChat.prototype.clearHistory = function (client) {   // If this is client request, check if it is allowed.   if (client != null && !this.allowClear)     return false;   this.history_so.setProperty("history", null);   this.history_so.flush( );   delete this.history;   this.history = new Array;   // Broadcast a   clearHistory   command to all clients.   this.message_so.send("clearHistory");   return true; }; 

The clearHistory( ) method saves null to the history shared object and clears the local history array in memory ( this.history ). It then sends a clearHistory message to all clients, so that they can clear their UI.

The saveHistory( ) utility method can be called by clients or other server-side scripts to force a flush to disk of the chat's history:

 FCChat.prototype.saveHistory = function (  ) {   this.history_so.setProperty("history", this.history);   this.history_so.flush(  ); }; 

The last thing the code does is trace out that it loaded the Chat component successfully:

 trace("Chat loaded successfully."); 

In the following sections, we modify this basic Chat component to allow for some special inline commands, such as "/clear" and "/kick".

14.2.2. The Client-Side Code

The Chat component's client-side code was written in ActionScript 1.0 (remember that the communication components were built for Flash MX). Again, some comments and code have been removed for brevity. You can find the full code by looking in the Actions layer of the Chat component (drag the component to your .fla file's Stage and double-click the Chat symbol in the Library to see it).

The code defines the FCChatClass class, which implements the Chat component, as a subclass of MovieClip , and registers its class with the symbol named FCChatSymbol :

 #initclip function FCChatClass (  ) {   this.init(  ); } FCChatClass.prototype = new MovieClip( ); Object.registerClass("FCChatSymbol", FCChatClass); FCChatClass.prototype.init = function ( ) {   this.name = (this._name == null ? "_DEFAULT_" : this._name);   this.prefix = "FCChat." + this.name + ".";   this.enterListener = new Object( );   this.enterListener.owner = this;   this.enterListener.enterPressed = false;   this.enterListener.onChanged = function ( ) {     enterChar = this.owner.message_txt.text.charAt(Selection.getEndIndex( )-1);     if (enterChar == String.fromCharCode(Key.ENTER)) {       this.owner.message_txt.text = this.owner.message_txt.text.substring(0,                                     this.owner.message_txt.text.length-1);       this.owner.sendMessage( );     }   };   this.message_txt.addListener(this.enterListener);   this.username = null; }; 

The FCChatClass constructor defines two properties, name and prefix , that will be needed for receiving calls from the server side. As we will see, these property's values are unique, because Flash requires components' instance names to be unique.

The constructor also defines a keyboard listener to which to send messages when a user presses the Enter key.

The component onUnload( ) event handler calls the close( ) method when the component is unloaded:

 FCChatClass.prototype.onUnload = function (  ) {   this.close(  ); }; 

The setUsername( ) method is called by the server side in the connect( ) call (see earlier code). It tells us what our username is, and we show or hide assets depending on its value (this is to prevent lurkers from participating in the discussion):

 FCChatClass.prototype.setUsername = function (newName) {   this.username = newName;   this.sendButton._visible = (newName != null);   this.message_txt._visible = (newName != null);   this.inputBg_mc._visible = (newName != null); }; 

The connect( ) method is the first one that must be called on any communication component. If you use the SimpleConnect component, it will call connect( ) on each component instance you specify; otherwise, you'll have to call connect( ) manually:

 FCChatClass.prototype.connect = function (nc) {   this.history_txt.htmlText = "";   this.nc = nc;   if (this.nc.FCChat == null)     this.nc.FCChat = {};   this.nc.FCChat[this.name] = this;   this.so = SharedObject.getRemote(this.prefix + "message", this.nc.uri, false);   this.so.owner = this;   this.so.message = function (mesg) { this.owner.receiveMessage(mesg); };   this.so.clearHistory = function (mesg) { this.owner.receiveHistory([]); };   this.so.connect(this.nc);   // Need to call   connect( )   on our server-side counterpart first.   this.nc.call(this.prefix + "connect", null); }; 

The nc parameter passed to connect( ) is a reference to the application's main NetConnection . The connect( ) method first creates a way for server-to-client calls to get to the client by setting:

 this.nc.FCChat[this.name] = this; 

As we saw in Chapter 9, attaching an object reference to a NetConnection object causes server-to-client calls to that object's path (" FCChat/instanceName/methodName ") to be automatically routed to that object.

The connect( ) method also subscribes to the message shared object and defines the message( ) and clearHistory( ) methods on it.

Finally, the client-side connect( ) method calls connect( ) on its server-side counterpart. If this is the first client to call this method, it causes the server-side part of the Chat component to be instantiated . (This is known as lazy instantiation because component instances aren't created on the server until the client side tries to call the server side. The fancy code that makes lazy instantiation possible is discussed later under "How Components Are Registered and Instantiated.") If, instead, some other client has already connected to the application, this call will simply result in a client-to-server call to the connect( ) method, which we discussed earlier.

The close( ) method is called by either the client movie's ActionScript or the onUnload( ) method. This method cleans up memory and calls the server side to notify it that this client is going away:

 FCChatClass.prototype.close = function (  ) {   var fullName = "FCChat." + this.name;   // Let our server-side counterpart know that we are going away.   this.nc.call(this.prefix + "close", null);   this.so.owner = null;    delete this.so.owner;   delete this.so.message;   this.so.close( );   this.so = null;   this.nc.FCChat[this.name] = null;   this.nc = null; }; 

The clearHistory( ) method can be called by your ActionScript code when you want to clear the history:

 FCChatClass.prototype.clearHistory = function (  ) {   this.nc.call(this.prefix + "clearHistory", null); }; 

Invoking the client-side clearHistory( ) method calls the server-side clearHistory( ) method, which triggers a clearHistory message on the message shared object, which in turn results in a receiveHistory( ) call on each client, which finally clears the history from the text field:

 FCChatClass.prototype.receiveHistory = function (h) {   var history;   for (var i = 0; i < h.length; i++)     history += h[i];   this.history_txt.htmlText = history;   this.history_txt.scroll = this.history_txt.maxscroll; }; 

The receiveHistory( ) method is called by the server side's connect( ) method (and by the clearHistory( ) method of message_so ). In it, we simply turn the array into a long string and copy it into the text field.

The receiveMessage( ) method gets called by the message shared object whenever a new message arrives. In it, we simply append the new message to the text field:

 FCChatClass.prototype.receiveMessage = function (mesg) {   this.history_txt.htmlText += mesg;   this.history_txt.scroll = this.history_txt.maxscroll; }; 

The sendMessage( ) method sends a new message to the server for broadcasting:

 FCChatClass.prototype.sendMessage = function (mesg) {   this.nc.call("FCChat." + this.name + ".sendMessage", null,                this.message_txt.text);   this.message_txt.text = ""; }; 

Like all components, the FCChatClass class defines a setSize( ) method (not shown), which simply resizes the component to the specified width and height. The last four lines of the client-side class definition, beyond the # endinitclip directive, set the component to its default state:

 #endinitclip // Disable the Send button. this.sendButton._visible = (this.username != null); this.message_txt._visible = (this.username != null); this.inputBg_mc._visible = (this.username != null); this.setSize(this._width,this._height); 

14.2.3. Enhancing the Chat: Creating MyChat

You'll commonly tweak the behavior of a component to suit an application's needs. Because of how the Macromedia components were built, this is a pretty straightforward but also error-prone process. To make a copy of the Chat component on which to base MyChat, you will need to do the following:

On the client side:

  1. Drag an instance of the Chat component from the Components panel to the Stage.

  2. Rename its symbol name in the Library from Chat to MyChat .

  3. Set its linkage ID in the Symbol Properties dialog box to MyChatSymbol .

  4. Open its timeline, and open the Actions panel. In the code, replace every occurrence of "FCChat" with "MyChat".

  5. If you want your component to appear alongside the other communication components in the Components panel, open Communication Component.fla and drag your MyChat component to the .fla file's Library. (You may need to restart Flash to see the new components.)

On the server side:

  1. Make a copy of chat.asc , and call it mychat.asc .

  2. Open mychat.asc and replace every occurrence of "FCChat" with "MyChat".

  3. If you want your new MyChat component to be loaded with all the other components when components.asc is loaded, open the components.asc file from your scriptlib folder and add a line to it that says load("components/mychat.asc"); .

Now you can safely edit the client-side and server-side code to suit your needs (you may want to create a simple test movie that contains your new MyChat component to make sure everything still works).

14.2.4. Changing MyChat: Turning Off the Chat History

Now that we have a custom MyChat component as embodied in mychat.asc , we can simplify it so that it, for example, doesn't save the chat's history. With this change, users will not receive the transcript of the prior conversation when they first log on; instead, they'll receive only messages sent after they have logged on. The easiest way to remove such functionality is to delete every reference to the history array from the server-side code, so it looks like this:

 try {var dummy = MyChat;} catch (e) { // #ifndef MyChat   load("components/component.asc");   MyChat = function (name) {     this.init(name);     this.message_so = SharedObject.get(this.prefix + "message", false);   };   MyChat.prototype = new FCComponent("MyChat",MyChat);   MyChat.prototype.message_so = null;   // This is called when the application is about to stop.   MyChat.prototype.onAppStop = function ( ) {   };   // The first method called by a client component.   MyChat.prototype.connect = function (client) {     var cglobal = this.getClientGlobalStorage(client);     if (!cglobal.usercolor) {       cglobal.usercolor = "0x000000";     }     client.call(this.callPrefix + "setUsername", null, cglobal.username);   };   // The last method called by a client component.   MyChat.prototype.close = function (client) {   };   // Send a message to all others participating in the chat session.   MyChat.prototype.sendMessage = function (client, mesg) {     var cglobal = this.getClientGlobalStorage(client);     mesg = this.hiliteURLs(mesg);     var hexColor = "#" + cglobal.usercolor.substring(2, cglobal.usercolor.length)     mesg = "<font color=\"" + hexColor + "\"><b>" + cglobal.username + ": </b>" +            mesg + "</font><br>\n";     this.message_so.send("message", mesg);   };   // Highlight the urls in a message.   MyChat.prototype.hiliteURLs = function (msg) {     // Code removed, look at the original   chat.asc   .     return hilited;   };   trace("MyChat loaded successfully."); } // #endif 

Note how the code is much smaller and cleaner (but also less powerful!). We will use this code as a starting point to make more changes to MyChat in subsequent sections. Somewhat surprisingly, there wasn't any client-side code worth removing for this change.

14.2.5. Enhancing MyChat: Adding Special Commands

Suppose we want to modify MyChat to support special commands such as "/clear" or "/kick", to clear the chat or kick somebody out of the room.

All the changes can be made within the sendMessage( ) method in the mychat.asc file, by adding the code to the following listing where it says "ADD MODIFICATIONS HERE!!!!":

 // Send a message to all others participating in the chat session. MyChat.prototype.sendMessage = function (client, mesg) {   var cglobal = this.getClientGlobalStorage(client);   // ADD MODIFICATIONS HERE!!!!   mesg = this.hiliteURLs(mesg);   var hexColor = "#" + cglobal.usercolor.substring(2, cglobal.usercolor.length)   mesg = "<font color=\"" + hexColor + "\"><b>" + cglobal.username + ": </b>" +          mesg + "</font><br>\n";   this.message_so.send("message", mesg); }; 

A very simple enhancement is to prevent users from sending empty messages:

 if (mesg == "") { return; } 

This code allows the special command "/clear" to clear the chat's transcript:

 if (mesg == "/clear") {   trace("clear called!");   this.message_so.send("clearHistory");   return; } 

So whenever any client sends the message "/clear", the chat history is cleared for everyone connected.

Another special message could be "/ip", which could list all the IPs of users connected to the application:

 // List every user's IP address. if (mesg == "/ips") {   var allips = "<font color=\"#AAAAAA\"><b>People's IPs:</b>";   for (var i = 0; i < application.clients.length; i++) {     var name = this.getClientGlobalStorage(application.clients[i]).username;     allips += "<br><b>" + name + "</b>: " + application.clients[i].ip;   }   allips += "</font><br>\n";   client.call(this.callPrefix + "receiveMessage", null, allips);   return; } 

The script goes through the application.clients array and gets the username and ip of each client, to create a big string ( allips ). It sends that string back to the client that sent the "/ip" message, as a regular chat message.

Now that a user has a list of people's IPs, you could let her kick someone out by supporting a "/kick ip " command, like this:

 // Kick a user (by IP address). if (mesg.substr(0,5) == "/kick") {   var ipToKick = mesg.substr(mesg.indexOf(" ") + 1);   for (var i = 0; i < application.clients.length; i++) {     if (application.clients[i].ip == ipToKick) {       trace("Disconnecting " + application.clients[i]);       var name = this.getClientGlobalStorage(application.clients[i]).username;       this.message_so.send("message", "<font color=\"#AAAAAA\"><i>" + name +                           " (" + ipToKick + ") was kicked</i></font>");       application.disconnect(application.clients[i]);       break;     }   }   return; } 

The code loops through the application.clients array. If the ip property matches the address specified in the message, the code disconnects that client and sends a message to all clients notifying them that the user was kicked out.

Another simple command that we could add support for is "/help", which lists all the commands supported by MyChat:

 if ((mesg == "/help")(mesg == "/?")) {   var helpStr = "<font color=\"#AAAAAA\"><b>Available Commands:</b>";   helpStr += "<br>  /clear";   helpStr += "<br>  /help";   helpStr += "<br>  /ips";   helpStr += "<br>  /kick ip";   helpStr += "</font><br>\n";   client.call(this.callPrefix + "receiveMessage", null, helpStr);   return; } 

Note that the "/help" command doesn't run any server-side code, so it could be moved entirely to the client side (just before sending the message). You can add other commands, such as "/private name" (to initiate a private conversation with the named user) or "/ban ip" (to bar users from the specified IP address from connecting), in an analogous way.



Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net