Reactor Chat Tutorial, Part 3

Chat Example 3 adds the following new features to Chat Example 2:

  • Private chat
  • A "total messages sent" counter
  • An external configuration file

Here's what the new chat looks like:

Once again, in order to focus on Reactor code rather than user interface code, the code for this chat does not use any component libraries such as the Flex framework or Flash's components. Hence, the private chat feature is brute force: to set a private chat recipient, you must type the recipient's name in the "private chat recipient" text field.

The private chat feature is implemented with a simple client-to-client message, sent via the Client class's sendMessage() method.

1
2
privateChatRecipient.sendMessage("PRIVATE_MESSAGE",
                                 outgoingMessages.text);

All clients listen for PRIVATE_CHAT messages by registering with the MessageManager:

1
2
connection.getMessageManager().addMessageListener("PRIVATE_MESSAGE",
                                                  privateMessageListener);

The message counter is implemented with a room attribute, MESSAGE_COUNTER, that is incremented by one every time a message is sent. To increment the attribute's value mathematically on the server, the client sends an expression and asks for it to be evaluated server-side:

1
2
3
4
// The %v means "the attribute's current value"
// The last "true" means "evaluate the specified value
// on the server before assignment"
chatRoom.setAttribute("MESSAGE_COUNTER", "%v+1", true, false, true);

By incrementing MESSAGE_COUNTER on the server rather than on the client, the application avoids an important synchronization problem: Suppose two clients simultaneously send a message, and both want to increment the counter entirely with client code. If the counter's existing value were, say, 12, both clients would check the value of MESSAGE_COUNTER, find it to be 12, add one to that, and tell the server to set the new value of MESSAGE_COUNTER to 13. But two messages were sent, so the actual value should be 14. The "server-side increment" approach avoids the miscount by performing the increment on the server's current value of MESSAGE_COUNTER, which is guaranteed to be accurate. Instead of saying "set MESSAGE_COUNTER to 13," the clients say "add one to MESSAGE_COUNTER's current server-side value."

Clients listen for changes to MESSAGE_COUNTER by registering for the AttributeEvent.UPDATE event.

1
2
3
4
5
6
7
8
9
10
11
12
// Event registration
chatRoom.addEventListener(AttributeEvent.UPDATE,
                                updateRoomAttributeListener);

// Event listener
protected function updateRoomAttributeListener (e:RoomEvent):void {
  var changedAttr:Attribute = e.getChangedAttr();
  if (changedAttr.name == "MESSAGE_COUNTER") {
    messageCounter.text = "Total Messages: "
                        + parseInt(e.getChangedAttr().value);
  }
}

The Code

Here's the complete code for the chat.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
package {
  import flash.display.Sprite;
  import flash.events.KeyboardEvent;
  import flash.text.TextField;
  import flash.text.TextFieldType;
  import flash.ui.Keyboard;
 
  import net.user1.reactor.*;

  public class UnionChatPart3 extends Sprite {
// =============================================================================
// APPLICATION VARIABLES
// =============================================================================

    // Union objects
    protected var reactor:Reactor;
    protected var chatRoom:Room;
   
    // User-interface objects
    protected var incomingMessages:TextField;
    protected var outgoingMessages:TextField;
    protected var userlist:TextField;
    protected var nameInput:TextField;
    protected var privateRecipientInput:TextField;
    protected var messageCounter:TextField;

// =============================================================================
// APPLICATION STARTUP
// =============================================================================

    // Constructor    
    public function UnionChatPart3 () {
      // Create the user interface
      buildUI();
      // Make the Reactor object, and load configuration settings from
      // an external file. The connection will open automatically after the
      // settings have loaded.
      reactor = new Reactor("config.xml");
      // Register for the READY event
      reactor.addEventListener(ReactorEvent.READY,
                               readyListener);
    }

// =============================================================================
// CONNECTION READY TASKS
// =============================================================================
   
    // Method invoked when the connection is ready
    protected function readyListener (e:ReactorEvent):void {
      // Assign the current user an auto-generated username
      setUserName("Guest" + reactor.self().getClientID());
     
      // Register for private-chat messages from other users
      reactor.getMessageManager().addMessageListener("PRIVATE_MESSAGE",
                                                     privateMessageListener);
                                                       
      // Display a welcome message
      displayMessage("Connected to Union");
     
      // Create the settings for the chat room. We want the
      // room to exist until the server shuts down, so set
      // dieOnEmpty to false.
      var settings:RoomSettings = new RoomSettings();
      settings.removeOnEmpty = false;  
     
      // Create the chat room (if the room already exists, the
      // request will be ignored, but the Room reference will
      // still be valid)
      chatRoom = reactor.getRoomManager().createRoom(
                                             "chatRoomThree", settings);
      // Register for regular chat messages
      chatRoom.addMessageListener("CHAT_MESSAGE",
                                  chatMessageListener);
      // Register for room events
      chatRoom.addEventListener(RoomEvent.JOIN,
                                joinRoomListener);
      chatRoom.addEventListener(RoomEvent.ADD_OCCUPANT,
                                addClientListener);
      chatRoom.addEventListener(RoomEvent.REMOVE_OCCUPANT,
                                removeClientListener);
      chatRoom.addEventListener(RoomEvent.UPDATE_CLIENT_ATTRIBUTE,
                                updateClientAttributeListener);
      chatRoom.addEventListener(AttributeEvent.UPDATE,
                                updateRoomAttributeListener);
      // Join the chat room
      chatRoom.join();
    }

// =============================================================================
// UI CREATION
// =============================================================================
   
    // Create the user interface
    protected function buildUI ():void {
      // Incoming chat messages
      incomingMessages = makeTextField(0, 0, 300, 200);
      incomingMessages.wordWrap = true;

      // Outgoing chat messages
      outgoingMessages = makeTextField(0, 210, 399, 20);
      outgoingMessages.type = TextFieldType.INPUT;
      outgoingMessages.addEventListener(KeyboardEvent.KEY_UP,
                                        outgoingKeyUpListener);
     
      // The list of users                                        
      userlist = makeTextField(310, 0, 89, 200);
     
      // Input field for setting user name
      nameInput = makeTextField(0, 240, 399, 20);
      nameInput.type = TextFieldType.INPUT;
      nameInput.addEventListener(KeyboardEvent.KEY_UP,
                                 nameKeyUpListener);

      // Displays the total messages sent in the chat
      messageCounter = makeTextField(0, 274, 120, 20, false, false);
      messageCounter.text = "Total Messages: ??";
      messageCounter.textColor = 0xFFFFFF;

      // Instructions for using private chat
      var privateRecipientLabel:TextField = makeTextField(130, 274, 180, 20,
                                                          false, false);
      privateRecipientLabel.text = "To private chat, enter user name here:";
      privateRecipientLabel.textColor = 0xFFFFFF;

      // Input field for setting private-chat recipient
      privateRecipientInput = makeTextField(310, 270, 89, 20);
      privateRecipientInput.type = TextFieldType.INPUT;
     
      addChild(incomingMessages);
      addChild(outgoingMessages);
      addChild(userlist);
      addChild(nameInput);
      addChild(messageCounter);
      addChild(privateRecipientLabel);
      addChild(privateRecipientInput);
    }

     // Helper function to build text fields
    protected function makeTextField (tx:Number = 0, ty:Number = 0,
                                      twidth:Number = 0, theight:Number = 0,
                                      border:Boolean = true,
                                      background:Boolean = true):TextField {
      var textField:TextField = new TextField();
      textField.x = tx;
      textField.y = ty;
      textField.width = twidth;
      textField.height = theight;
      textField.border = border;
      textField.background = background;
      return textField;
    }

// =============================================================================
// UI EVENT LISTENERS
// =============================================================================

    // Keyboard listener for outgoingMessages text field
    protected function outgoingKeyUpListener (e:KeyboardEvent):void {
      var privateChatRecipient:IClient;
     
      // When the user presses the ENTER key...
      if (e.keyCode == Keyboard.ENTER) {
        // If there's a private-chat recipient specified,
        // attempt to find the matching client
        if (privateRecipientInput.text != "") {
          privateChatRecipient =
            reactor.getClientManager().getClientByAttribute("USERNAME",
                                                    privateRecipientInput.text);
          if (privateChatRecipient == null) {
            displayMessage("Cound not send message to: '"
                           + privateRecipientInput.text
                           + "'. No such user.");
            return;
          }
        }
       
        // If there's no private-chat recipient,
        // send the message to everyone in the room
        if (privateChatRecipient == null) {
          chatRoom.sendMessage("CHAT_MESSAGE",
                               true,
                               null,
                               outgoingMessages.text);
          // Add one to the room's MESSAGE_COUNTER attribute
          chatRoom.setAttribute("MESSAGE_COUNTER", "%v+1", true, false, true);
        } else {
          // There's a valid private-chat recipient, so send
          // the message to that client only
          privateChatRecipient.sendMessage("PRIVATE_MESSAGE",
                                          outgoingMessages.text);
          displayMessage("You told " + getUserName(privateChatRecipient)
                         + ": " + outgoingMessages.text);
        }
        outgoingMessages.text = "";
      }
    }
   
    // Keyboard listener for nameInput
    protected function nameKeyUpListener (e:KeyboardEvent):void {
      // When the user presses the ENTER key...
      if (e.keyCode == Keyboard.ENTER) {
        // Assign the new user name
        setUserName(nameInput.text);
        nameInput.text = "";
      }
    }
   
// =============================================================================
// UNION MESSAGE LISTENERS
// =============================================================================

    // Method invoked when a regular chat message is received
    protected function chatMessageListener (fromClient:IClient,
                                            messageText:String
                                            ):void {
      displayMessage(getUserName(fromClient)
                    + " says: " + messageText);
    }
   
    // Method invoked when a private message is received
    protected function privateMessageListener (fromClient:IClient,
                                               messageText:String
                                               ):void {
      displayMessage("Private message from: "
                     + getUserName(fromClient)
                     + ": " + messageText);
    }
   
// =============================================================================
// ROOM EVENT LISTENERS
// =============================================================================
   
    // Method invoked when the room's client list and
    // attributes are synchronized and ready for use
    protected function joinRoomListener (e:RoomEvent):void {
      updateUserList();
    }
   
    // Method invoked when a client joins the room
    protected function addClientListener (e:RoomEvent):void {
      if (e.getClient().isSelf()) {
        displayMessage("You joined the chat.");
      } else {
        if (chatRoom.getSyncState() != SynchronizationState.SYNCHRONIZING) {
          // Show a "guest joined" message only when the room isn't performing
          // its initial occupant-list synchronization.
          displayMessage(getUserName(e.getClient())
                         + " joined the chat.");
        }
      }
     
      updateUserList();
    }
   
    // Method invoked when a client leave the room
    protected function removeClientListener (e:RoomEvent):void {
      displayMessage(getUserName(e.getClient())
                     + " left the chat.");
      updateUserList();
    }
   
    // Method invoked when a client
    // changes one of its shared attributes
    protected function updateClientAttributeListener (e:RoomEvent):void {
      var changedAttr:Attribute = e.getChangedAttr();
      // If the attribute that changed was USERNAME...
      if (changedAttr.name == "USERNAME") {
        // Display a message and update the user list
        if (changedAttr.oldValue != null) {
          displayMessage(changedAttr.oldValue
                         + "'s name changed to "
                         + getUserName(e.getClient()));
          updateUserList();
        }
      }
    }
   
    // Method invoked when any of the room's
    // shared room attributes change
    protected function updateRoomAttributeListener (e:AttributeEvent):void {
      var changedAttr:Attribute = e.getChangedAttr();
      // If the attribute that changed was MESSAGE_COUNTER...
      if (changedAttr.name == "MESSAGE_COUNTER") {
        // Display the new message count
        messageCounter.text = "Total Messages: "
                            + parseInt(e.getChangedAttr().value);
      }
    }
   
// =============================================================================
// USERNAME MANAGEMENT
// =============================================================================
   
    // Returns the specified client's username
    protected function getUserName (client:IClient):String {
      return client.getAttribute("USERNAME");
    }
   
    // Assigns a new username to the current client
    protected function setUserName (userName:String):void {
      var self:IClient;
      // Check if the desired new name is valid
      if (userName == null || userName.length == 0) {
        return;
      }
      self = reactor.self();
      // Set the shared attribute so other clients will
      // be notified of the changed user name.
      self.setAttribute("USERNAME", userName);
    }
   
// =============================================================================
// UI CONTROL
// =============================================================================
   
    // Displays the client list on screen
    protected function updateUserList ():void {
      userlist.text = "";
      for each (var client:IClient in chatRoom.getOccupants()) {
        userlist.appendText(getUserName(client) + "\n");
      }
    }
   
    // Displays a message in the incoming text field
    protected function displayMessage (message:String):void {
      incomingMessages.appendText(message + "\n");
      incomingMessages.scrollV = incomingMessages.maxScrollV;
    }
  }
}

Here's the chat's configuration file:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<config>
  <connections>
    <connection host="tryunion.com" port="80"/>
  </connections>
  <logLevel>DEBUG</logLevel>
</config>