diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java
index 664c16e4414b9863e47e7fde5e75df33aed906b5..d4768231bd0baaa2f0886648e9fe3873e9a16d3a 100755
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java
@@ -13,6 +13,7 @@ import org.bigbluebutton.conference.service.messaging.RegisterUserMessage;
 import org.bigbluebutton.conference.service.messaging.UserConnectedToGlobalAudio;
 import org.bigbluebutton.conference.service.messaging.UserDisconnectedFromGlobalAudio;
 import org.bigbluebutton.conference.service.messaging.ValidateAuthTokenMessage;
+import org.bigbluebutton.conference.service.messaging.GetAllMeetingsRequest;
 import org.bigbluebutton.conference.service.messaging.redis.MessageHandler;
 import org.bigbluebutton.core.api.IBigBlueButtonInGW;
 import org.red5.logging.Red5LoggerFactory;
@@ -86,6 +87,11 @@ public class MeetingMessageHandler implements MessageHandler {
 					log.info("User disconnected from global audio: data={}", logStr);
 					bbbGW.userDisconnectedFromGlobalAudio(emm.voiceConf, emm.userid, emm.name);
 				}
+				else if (msg instanceof GetAllMeetingsRequest) {
+					GetAllMeetingsRequest emm = (GetAllMeetingsRequest) msg;
+					log.info("Received GetAllMeetingsRequest");
+					bbbGW.getAllMeetings("no_need_of_a_meeting_id");
+				}
 			}
 		} else if (channel.equalsIgnoreCase(MessagingConstants.TO_SYSTEM_CHANNEL)) {
 			IMessage msg = MessageFromJsonConverter.convert(message);
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java
index 9e0cbb17743202ecb53a844663be46884b18bfe3..bd256d180f0711a1a3c5bd5d9599fd032ebc4a13 100755
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java
@@ -31,11 +31,8 @@ public class ChatMessageListener implements MessageHandler{
 			String eventName = headerObject.get("name").toString();
 			eventName = eventName.replace("\"", "");
 
-			if (eventName.equalsIgnoreCase("public_chat_message_event") ||
-				eventName.equalsIgnoreCase("send_public_chat_message") || //identical
-				eventName.equalsIgnoreCase("private_chat_message_event") ||
-				eventName.equalsIgnoreCase("send_private_chat_message") ||//identical
-				eventName.equalsIgnoreCase("get_chat_history")){
+			if (eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST) ||
+				eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)){
 
 				String meetingID = payloadObject.get("meeting_id").toString().replace("\"", "");
 				String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
@@ -67,13 +64,11 @@ public class ChatMessageListener implements MessageHandler{
 					map.put(ChatKeyUtil.TO_USERNAME, toUsername);
 					map.put(ChatKeyUtil.MESSAGE, chatText);
 
-					//public message
-					if(eventName.equalsIgnoreCase("public_chat_message_event") 
-							|| eventName.equalsIgnoreCase("send_public_chat_message")) {
+					if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST)) {
 						bbbGW.sendPublicMessage(meetingID, requesterID, map);
-					}	else if(eventName.equalsIgnoreCase("private_chat_message_event") 
-							|| eventName.equalsIgnoreCase("send_private_chat_message")) {
-						bbbGW.sendPrivateMessage(meetingID, requesterID, map); 
+					}
+					else if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)) {
+						bbbGW.sendPrivateMessage(meetingID, requesterID, map);
 					}
 				}
 			}
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..baa1cd4099f32188401382a6095455b6cac21925
--- /dev/null
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java
@@ -0,0 +1,12 @@
+package org.bigbluebutton.conference.service.messaging;
+
+public class GetAllMeetingsRequest implements IMessage {
+	public static final String GET_ALL_MEETINGS_REQUEST_EVENT  = "get_all_meetings_request";
+	public static final String VERSION = "0.0.1";
+
+	public final String meetingId;
+
+	public GetAllMeetingsRequest(String meetingId) {
+		this.meetingId = meetingId;
+	}
+}
\ No newline at end of file
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java
index ed14ac7d20d09df6867a441034f078953095b02f..70ac6181cee81c8bdc79ec4a8f9c35caab0751b8 100755
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java
@@ -33,6 +33,8 @@ public class MessageFromJsonConverter {
 					return UserConnectedToGlobalAudio.fromJson(message);
 				  case UserDisconnectedFromGlobalAudio.USER_DISCONNECTED_FROM_GLOBAL_AUDIO:
 					return UserDisconnectedFromGlobalAudio.fromJson(message);
+				  case GetAllMeetingsRequest.GET_ALL_MEETINGS_REQUEST_EVENT:
+					return new GetAllMeetingsRequest("the_string_is_not_used_anywhere");
 				}
 			}
 		}
@@ -76,4 +78,6 @@ public class MessageFromJsonConverter {
 		String id = payload.get(Constants.KEEP_ALIVE_ID).getAsString();		
 		return new KeepAliveMessage(id);
 	}
+
+	//private static IMessage processGetAllMeetings(JsonObject)
 }
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java
index 3dfb3a9e69b3f78572c1c29ce1a47ca8c81f8dcd..7e6a0258b97a2f1103c2c9c2dd79d074354b58fa 100644
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java
@@ -37,8 +37,9 @@ public class MessagingConstants {
 	public static final String TO_PRESENTATION_CHANNEL = TO_BBB_APPS_CHANNEL + ":presentation";
 	public static final String TO_POLLING_CHANNEL = TO_BBB_APPS_CHANNEL + ":polling";
 	public static final String TO_USERS_CHANNEL = TO_BBB_APPS_CHANNEL + ":users";
-	public static final String TO_CHAT_CHANNEL = TO_BBB_APPS_CHANNEL + ":chat";   
-
+	public static final String TO_CHAT_CHANNEL = TO_BBB_APPS_CHANNEL + ":chat";
+	public static final String TO_VOICE_CHANNEL = TO_BBB_APPS_CHANNEL + ":voice";
+	public static final String TO_WHITEBOARD_CHANNEL = TO_BBB_APPS_CHANNEL + ":whiteboard";
 
 	public static final String DESTROY_MEETING_REQUEST_EVENT = "DestroyMeetingRequestEvent";
 	public static final String CREATE_MEETING_REQUEST_EVENT = "CreateMeetingRequestEvent";	
@@ -48,7 +49,11 @@ public class MessagingConstants {
 	public static final String MEETING_DESTROYED_EVENT = "meeting_destroyed_event";
 	public static final String USER_JOINED_EVENT = "UserJoinedEvent";
 	public static final String USER_LEFT_EVENT = "UserLeftEvent";
+	public static final String USER_LEFT_VOICE_REQUEST = "user_left_voice_request";
 	public static final String USER_STATUS_CHANGE_EVENT = "UserStatusChangeEvent";	
 	public static final String SEND_POLLS_EVENT = "SendPollsEvent";
 	public static final String RECORD_STATUS_EVENT = "RecordStatusEvent";
+	public static final String SEND_PUBLIC_CHAT_MESSAGE_REQUEST = "send_public_chat_message_request";
+	public static final String SEND_PRIVATE_CHAT_MESSAGE_REQUEST = "send_private_chat_message_request";
+	public static final String MUTE_USER_REQUEST = "mute_user_request";
 }
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java
index b10a9f835ed00d34a31a35e26fdd81a8645ef7d1..3f8c2a45513ab6313f23bec01ca377c8115cd80b 100644
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java
@@ -4,6 +4,7 @@ package org.bigbluebutton.conference.service.participants;
 
 import org.bigbluebutton.conference.service.messaging.MessagingConstants;
 import org.bigbluebutton.conference.service.messaging.redis.MessageHandler;
+//import org.bigbluebutton.core.api.*;
 
 import org.bigbluebutton.core.api.IBigBlueButtonInGW;
 import com.google.gson.JsonParser;
@@ -20,7 +21,6 @@ public class ParticipantsListener implements MessageHandler{
 	@Override
 	public void handleMessage(String pattern, String channel, String message) {
 		if (channel.equalsIgnoreCase(MessagingConstants.TO_USERS_CHANNEL)) {
-			System.out.println("AntonChannel=(participants)" + channel);
 
 			JsonParser parser = new JsonParser();
 			JsonObject obj = (JsonObject) parser.parse(message);
@@ -29,46 +29,22 @@ public class ParticipantsListener implements MessageHandler{
 
 			String eventName =  headerObject.get("name").toString().replace("\"", "");
 
-			if(eventName.equalsIgnoreCase("register_user_request") ||
-				eventName.equalsIgnoreCase("user_left_event") ||
-				eventName.equalsIgnoreCase("user_joined_event") ||
-				eventName.equalsIgnoreCase("get_users_request") ||
-				eventName.equalsIgnoreCase("raise_user_hand_request")){
+			if(eventName.equalsIgnoreCase("user_leaving_request") ||
+				eventName.equalsIgnoreCase("user_raised_hand_message") ||
+				eventName.equalsIgnoreCase("user_lowered_hand_message")){
 
 				String roomName = payloadObject.get("meeting_id").toString().replace("\"", "");
+				String userID = payloadObject.get("userid").toString().replace("\"", "");
 
-				if(eventName.equalsIgnoreCase("register_user_request")){
-					String userID = payloadObject.get("user_id").toString().replace("\"", "");
-					String username = payloadObject.get("name").toString().replace("\"", "");
-					String role = payloadObject.get("role").toString().replace("\"", "");
-					String externUserID = payloadObject.get("external_user_id").toString().replace("\"", "");
-
-				}
-				else if(eventName.equalsIgnoreCase("user_left_event")){
-					String userID = payloadObject.get("user_id").toString().replace("\"", "");
-
+				if(eventName.equalsIgnoreCase("user_leaving_request")){
 					bbbInGW.userLeft(roomName, userID);
 				}
-				else if(eventName.equalsIgnoreCase("user_joined_event")){
-					String userID = payloadObject.get("user_id").toString().replace("\"", "");
-
-					bbbInGW.userJoin(roomName, userID);
-				}
-				else if(eventName.equalsIgnoreCase("get_users_request")){
-					String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
-					bbbInGW.getUsers(roomName, requesterID);
+				else if(eventName.equalsIgnoreCase("user_raised_hand_message")){
+					bbbInGW.userRaiseHand(roomName, userID);
 				}
-				else if(eventName.equalsIgnoreCase("raise_user_hand_request")){
-					String userID = payloadObject.get("user_id").toString().replace("\"", "");
-					boolean raise = Boolean.parseBoolean(payloadObject.get("raise").toString().replace("\"", ""));
-
-					if(raise){
-						bbbInGW.userRaiseHand(roomName, userID);
-					}
-					else {
-						String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
-						bbbInGW.lowerHand(roomName, userID, requesterID);
-					}
+				else if(eventName.equalsIgnoreCase("user_lowered_hand_message")){
+					String requesterID = payloadObject.get("lowered_by").toString().replace("\"", "");
+					bbbInGW.lowerHand(roomName, userID, requesterID);
 				}
 			}
 		}
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..9bc1fd75bc5df2452d4fefd0a8924cbe4d9dcc4b
--- /dev/null
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java
@@ -0,0 +1,6 @@
+package org.bigbluebutton.conference.service.voice;
+
+public class VoiceKeyUtil {
+	public static final String MUTE = "mute";
+	public static final String USERID = "userId";
+}
\ No newline at end of file
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab7062c8b6b955377ba1c36ff35a8bfee8874372
--- /dev/null
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java
@@ -0,0 +1,53 @@
+package org.bigbluebutton.conference.service.voice;
+
+import org.bigbluebutton.conference.service.messaging.MessagingConstants;
+import org.bigbluebutton.conference.service.messaging.redis.MessageHandler;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonObject;
+
+import java.util.Map;
+import java.util.HashMap;
+
+import org.bigbluebutton.core.api.IBigBlueButtonInGW;
+
+public class VoiceMessageListener implements MessageHandler{
+
+	private IBigBlueButtonInGW bbbGW;
+	
+	public void setBigBlueButtonInGW(IBigBlueButtonInGW bbbGW) {
+		this.bbbGW = bbbGW;
+	}
+
+	@Override
+	public void handleMessage(String pattern, String channel, String message) {
+		if (channel.equalsIgnoreCase(MessagingConstants.TO_VOICE_CHANNEL)) {
+
+			JsonParser parser = new JsonParser();
+			JsonObject obj = (JsonObject) parser.parse(message);
+			JsonObject headerObject = (JsonObject) obj.get("header");
+			JsonObject payloadObject = (JsonObject) obj.get("payload");
+
+			String eventName = headerObject.get("name").toString().replace("\"", "");
+
+			if (eventName.equalsIgnoreCase(MessagingConstants.MUTE_USER_REQUEST)){
+
+				String meetingID = payloadObject.get("meeting_id").toString().replace("\"", "");
+				String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
+				String userID = payloadObject.get("userid").toString().replace("\"", "");
+				String muteString = payloadObject.get(VoiceKeyUtil.MUTE).toString().replace("\"", "");
+				Boolean mute = Boolean.valueOf(muteString);
+
+				System.out.println("handling mute_user_request");
+				bbbGW.muteUser(meetingID, requesterID, userID, mute);
+			}
+			else if (eventName.equalsIgnoreCase(MessagingConstants.USER_LEFT_VOICE_REQUEST)){
+
+				String meetingID = payloadObject.get("meeting_id").toString().replace("\"", "");
+				String userID = payloadObject.get("userid").toString().replace("\"", "");
+
+				System.out.println("handling user_left_voice_request");
+				bbbGW.voiceUserLeft(meetingID, userID);
+			}
+		}
+	}
+}
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java
index 7e70ff62b593efe07180890d95c642622f6c1d10..b7b1c5966cda5acf2cbe20609078b745a0e0f0d0 100644
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java
@@ -17,11 +17,15 @@
 *
 */
 package org.bigbluebutton.conference.service.voice;
-
import org.slf4j.Logger;
import org.red5.server.api.Red5;
import org.bigbluebutton.conference.BigBlueButtonSession;
import org.bigbluebutton.conference.Constants;
import org.bigbluebutton.core.api.IBigBlueButtonInGW;
+import org.slf4j.Logger;
+import org.red5.server.api.Red5;
+import org.bigbluebutton.conference.BigBlueButtonSession;
+import org.bigbluebutton.conference.Constants;
+import org.bigbluebutton.core.api.IBigBlueButtonInGW;
 import org.red5.logging.Red5LoggerFactory;
 import java.util.List;
 import java.util.Map;
-
+
 public class VoiceService {
 	
 	private static Logger log = Red5LoggerFactory.getLogger( VoiceService.class, "bigbluebutton" );
@@ -70,16 +74,17 @@ public class VoiceService {
 		String requesterID = getBbbSession().getInternalUserID();		
 		bbbInGW.isMeetingMuted(meetingID, requesterID); 	
 	}
-	
+
+	// not sure if this is used
 	public void muteUnmuteUser(Map<String, Object> msg) {
-		Boolean mute = (Boolean) msg.get("mute");
-		String userid = (String) msg.get("userId");
+		Boolean mute = (Boolean) msg.get(VoiceKeyUtil.MUTE);
+		String userid = (String) msg.get(VoiceKeyUtil.USERID);
 
 		String meetingID = Red5.getConnectionLocal().getScope().getName();
-		String requesterID = getBbbSession().getInternalUserID();		
+		String requesterID = getBbbSession().getInternalUserID();
 		bbbInGW.muteUser(meetingID, requesterID, userid, mute); 
 	}
-	
+
 	public void lockMuteUser(Map<String, Object> msg) { 			
 		Boolean lock = (Boolean) msg.get("lock");
 		String userid = (String) msg.get("userId");
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..47bc7b68c60f50e5301787ac2ac46fa8150a62b3
--- /dev/null
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java
@@ -0,0 +1,52 @@
+
+package org.bigbluebutton.conference.service.whiteboard;
+
+
+import org.bigbluebutton.conference.service.messaging.MessagingConstants;
+import org.bigbluebutton.conference.service.messaging.redis.MessageHandler;
+
+import org.bigbluebutton.core.api.IBigBlueButtonInGW;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonObject;
+
+public class WhiteboardListener implements MessageHandler{
+	
+	private IBigBlueButtonInGW bbbInGW;
+	
+	public void setBigBlueButtonInGW(IBigBlueButtonInGW bbbInGW) {
+		this.bbbInGW = bbbInGW;
+	}
+
+	@Override
+	public void handleMessage(String pattern, String channel, String message) {
+		if (channel.equalsIgnoreCase(MessagingConstants.TO_WHITEBOARD_CHANNEL)) {
+			System.out.println("AntonChannel=(whiteboard)" + channel);
+
+			JsonParser parser = new JsonParser();
+			JsonObject obj = (JsonObject) parser.parse(message);
+			JsonObject headerObject = (JsonObject) obj.get("header");
+			JsonObject payloadObject = (JsonObject) obj.get("payload");
+
+			String eventName =  headerObject.get("name").toString().replace("\"", "");
+
+			if(eventName.equalsIgnoreCase("get_whiteboard_shapes_request")){
+			//more cases to follow
+
+				String roomName = payloadObject.get("meeting_id").toString().replace("\"", "");
+
+				if(eventName.equalsIgnoreCase("get_whiteboard_shapes_request")){
+					String requesterID = payloadObject.get("requester_id").toString().replace("\"", "");
+					if(payloadObject.get("whiteboard_id") != null){
+						String whiteboardID = payloadObject.get("whiteboard_id").toString().replace("\"", "");
+						System.out.println("\n FOUND A whiteboardID:" + whiteboardID + "\n");
+						bbbInGW.requestWhiteboardAnnotationHistory(roomName, requesterID, whiteboardID, requesterID);
+					}
+					else {
+						System.out.println("\n DID NOT FIND A whiteboardID \n");
+					}
+					System.out.println("\n\n\n user<" + requesterID + "> requested the shapes.\n\n");
+				}
+			}
+		}
+	}
+}
diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java
index 6e8fb6c695130bd5ab971394395e15ecdf83b862..b436e1b4444be87ca275d2571c60929c46df0379 100644
--- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java
+++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java
@@ -14,6 +14,7 @@ public interface IBigBlueButtonInGW {
 			    String voiceBridge, long duration, boolean autoStartRecording,
 			    boolean allowStartStopRecording);
 	void destroyMeeting(String meetingID);
+	void getAllMeetings(String meetingID);
 	void lockSettings(String meetingID, Boolean locked, Map<String, Boolean> lockSettigs);
 	
 	
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala
index 6810a1aec3fd587eae87f8b2f4473e580454501c..e71dafc4fad340f07842cb836bc3d23d77fadd30 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala
@@ -17,6 +17,7 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor with LogHelper
 	      case msg: CreateMeeting                 => handleCreateMeeting(msg)
 	      case msg: DestroyMeeting                => handleDestroyMeeting(msg)
 	      case msg: KeepAliveMessage              => handleKeepAliveMessage(msg)
+        case msg: GetAllMeetingsRequest         => handleGetAllMeetingsRequest(msg)
 	      case msg: InMessage                     => handleMeetingMessage(msg)
 	      case _ => // do nothing
 	    }
@@ -100,5 +101,45 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor with LogHelper
       }
     }
   }
-  
+
+  private def handleGetAllMeetingsRequest(msg: GetAllMeetingsRequest) {
+    var len = meetings.keys.size
+    println("meetings.size=" + meetings.size)
+    println("len_=" + len)
+
+    val set = meetings.keySet
+    val arr : Array[String] = new Array[String](len)
+    set.copyToArray(arr)
+    val resultArray : Array[MeetingInfo] = new Array[MeetingInfo](len)
+
+    for(i <- 0 until arr.length) {
+      val id = arr(i)
+      val duration = meetings.get(arr(i)).head.getDuration()
+      val name = meetings.get(arr(i)).head.getMeetingName()
+      val recorded = meetings.get(arr(i)).head.getRecordedStatus()
+      val voiceBridge = meetings.get(arr(i)).head.getVoiceBridgeNumber()
+
+      var info = new MeetingInfo(id, name, recorded, voiceBridge, duration)
+      resultArray(i) = info
+
+      //remove later
+      println("for a meeting:" + id)
+      println("Meeting Name = " + meetings.get(id).head.getMeetingName())
+      println("isRecorded = " + meetings.get(id).head.getRecordedStatus())
+      println("voiceBridge = " + voiceBridge)
+      println("duration = " + duration)
+
+      //send the users
+      this ! (new GetUsers(id, "nodeJSapp"))
+
+      //send the presentation
+      this ! (new GetPresentationInfo(id, "nodeJSapp", "nodeJSapp"))
+
+      //send chat history
+      this ! (new GetChatHistoryRequest(id, "nodeJSapp", "nodeJSapp"))
+    }
+
+    outGW.send(new GetAllMeetingsReply(resultArray))
+  }
+
 }
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala
index 514e795a10b7397db40e323a29465b54c0830bd7..b4473b9fc5f907ca23ad17a726341bd26e9a4107 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala
@@ -30,7 +30,12 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
 //    println("******************** DESTROY MEETING [" + meetingID + "] ***************************** ")
     bbbGW.accept(new DestroyMeeting(meetingID))
   }
-  
+
+  def getAllMeetings(meetingID: String) {
+  	println("******************** GET ALL MEETINGS ***************************** ")
+  	bbbGW.accept(new GetAllMeetingsRequest("meetingId"))
+  }
+
   def isAliveAudit(aliveId:String) {
     bbbGW.acceptKeepAlive(new KeepAliveMessage(aliveId)); 
   }
@@ -221,10 +226,12 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
 	    val current = if (i == 1) true else false
 	    val thumbnail = presBaseUrl + "/thumbnail/" + i
 	    val swfUri = presBaseUrl + "/slide/" + i
-	    val txtUri = presBaseUrl + "/textfiles/" + i
-				
+
+        val txtUri = presBaseUrl + "/textfiles/slide-" + i + ".txt"
+        val pngUri = presBaseUrl + "/png/" + i
+
 	    val p = new Page(id=id, num=num, thumbUri=thumbnail, swfUri=swfUri,
-	                     txtUri=txtUri, pngUri=thumbnail,
+	                     txtUri=txtUri, pngUri=pngUri,
 	                     current=current)
 	    pages += (p.id -> p)
 	  }
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala
index 3635a8ad9479bcea19199a0eec5ca1d89381dc81..96e4e1146389145800aaf3351d233dadb91a1e5d 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala
@@ -98,6 +98,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
         case msg: UndoWhiteboardRequest         => handleUndoWhiteboardRequest(msg)
         case msg: EnableWhiteboardRequest       => handleEnableWhiteboardRequest(msg)
         case msg: IsWhiteboardEnabledRequest    => handleIsWhiteboardEnabledRequest(msg)
+        case msg: GetAllMeetingsRequest         => handleGetAllMeetingsRequest(msg)
 
         //OUT MESSAGES
         case msg: MeetingCreated                => handleMeetingCreated(msg)
@@ -177,6 +178,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
         case msg: UndoWhiteboardEvent           => handleUndoWhiteboardEvent(msg)
         case msg: WhiteboardEnabledEvent        => handleWhiteboardEnabledEvent(msg)
         case msg: IsWhiteboardEnabledReply      => handleIsWhiteboardEnabledReply(msg)
+        case msg: GetAllMeetingsReply           => handleGetAllMeetingsReply(msg)
 
         case _ => // do nothing
       }
@@ -1376,6 +1378,12 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
     dispatcher.dispatch(buildJson(header, payload))
   }
 
+  private def handleGetAllMeetingsRequest(msg: GetAllMeetingsRequest) {
+    println("***** DISPATCHING GET ALL MEETINGS REQUEST *****************")
+  }
+
+
+
   // OUT MESSAGES
   private def handleMeetingCreated(msg: MeetingCreated) {
     val json = MeetingMessageToJsonConverter.meetingCreatedToJson(msg)
@@ -2196,4 +2204,9 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
     val json = WhiteboardMessageToJsonConverter.isWhiteboardEnabledReplyToJson(msg)
     dispatcher.dispatch(json)
   }
+  private def handleGetAllMeetingsReply(msg: GetAllMeetingsReply) {
+    val json = MeetingMessageToJsonConverter.getAllMeetingsReplyToJson(msg)
+    println("*****  DISPATCHING GET ALL MEETINGS REPLY OUTMSG *****************")
+    dispatcher.dispatch(json)
+  }
 }
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala
index 7021f5959e52d1e73525fa315f3f007b8339a8ed..ec0003c803b7a91757a5662687f37a4f42ff9491 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala
@@ -30,11 +30,27 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
   var recording = false;
   var muted = false;
   var meetingEnded = false
+
+  def getDuration():Long = {
+    duration
+  }
+
+  def getMeetingName():String = {
+    meetingName
+  }
+
+  def getRecordedStatus():Boolean = {
+    recorded
+  }
+
+  def getVoiceBridgeNumber():String = {
+    voiceBridge
+  }
   
   val TIMER_INTERVAL = 30000
   var hasLastWebUserLeft = false
   var lastWebUserLeftOn:Long = 0
-  
+
   class TimerActor(val timeout: Long, val who: Actor, val reply: String) extends Actor {
     def act {
         reactWithin(timeout) {
@@ -121,7 +137,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
 	    case _ => // do nothing
 	  }
 	}
-  }	
+  }
   
   def hasMeetingEnded():Boolean = {
     meetingEnded
@@ -143,7 +159,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
   def webUserJoined() {
     if (users.numWebUsers > 0) {
       lastWebUserLeftOn = 0
-	  }      
+    }      
   }
   
   def startRecordingIfAutoStart() {
@@ -168,7 +184,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
       lastWebUserLeftOn = timeNowInMinutes
 	    logger.debug("MonitorNumberOfWebUsers started for meeting [" + meetingID + "]")
       scheduleEndVoiceConference()
-	  }
+    }
   }
   
   def handleMonitorNumberOfWebUsers() {
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala
index a8d2c80e56ff9d095bac4460906a83377c5171ff..c4fd79ea05d2abf01d8abecff7d293b1e8268521 100644
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala
@@ -503,3 +503,7 @@ case class IsWhiteboardEnabledRequest(
     requesterID: String,
     replyTo: String
 ) extends InMessage
+
+case class GetAllMeetingsRequest(
+    meetingID: String /** Not used. Just to satisfy trait **/
+    ) extends InMessage
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala
index 6d02f3113a7a651af8ca591dd8c4a25a95f415e4..6443025d1f30d105650988144a6faf45109e197b 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala
@@ -78,6 +78,7 @@ object MessageNames {
   val UNDO_WHITEBOARD                      = "undo_whiteboard_request"
   val ENABLE_WHITEBOARD                    = "enable_whiteboard_request"
   val IS_WHITEBOARD_ENABLED                = "is_whiteboard_enabled_request"
+  val GET_ALL_MEETINGS_REQUEST             = "get_all_meetings_request"
 
   // OUT MESSAGES
   val MEETING_CREATED                      = "meeting_created_message"
@@ -158,5 +159,5 @@ object MessageNames {
   val MEETING_DESTROYED_EVENT              = "meeting_destroyed_event"
   val KEEP_ALIVE_REPLY                     = "keep_alive_reply"
   val USER_LISTEN_ONLY                     = "user_listening_only"
-  
+  val GET_ALL_MEETINGS_REPLY               = "get_all_meetings_reply"
 }
\ No newline at end of file
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala
index 553042c1249949ae55702c489ff9aaa5da504a7e..388d467738dcb047fba294ef5a3dc42ba09fc95f 100644
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala
@@ -647,7 +647,11 @@ case class IsWhiteboardEnabledReply(
     replyTo: String,
     version:String = Versions.V_0_0_1
 ) extends IOutMessage
-                       
+
+case class GetAllMeetingsReply(
+    meetings: Array[MeetingInfo],
+    version:String = Versions.V_0_0_1
+) extends IOutMessage
 
 // Value Objects
 case class MeetingVO(
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala
index fc95079998d64d0f9c8630bb156cc325d1b82590..e9eb5ad35217a0acaee01b365711d96e47749315 100644
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala
@@ -5,7 +5,7 @@ object Role extends Enumeration {
 	val MODERATOR = Value("MODERATOR")
 	val VIEWER = Value("VIEWER")
 }
-
+
 case class Presenter(
   presenterID: String, 
   presenterName: String, 
@@ -102,4 +102,6 @@ case class VoiceConfig(telVoice: String, webVoice: String, dialNumber: String)
 case class MeetingPasswords(moderatorPass: String, viewerPass: String)
     
 case class MeetingDuration(duration: Int = 0, createdTime: Long = 0, 
-   startTime: Long = 0, endTime: Long = 0)
\ No newline at end of file
+   startTime: Long = 0, endTime: Long = 0)
+
+case class MeetingInfo(meetingID: String, meetingName: String, recorded: Boolean, voiceBridge: String, duration: Long)
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
index 18572be961aeb33cc6e412b3c3305e6fc643a559..7e34b97810e895add249e9c9a4f969f87ebef230 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
@@ -53,6 +53,7 @@ trait UsersApp {
     }
   }
   
+
   def handleMuteAllExceptPresenterRequest(msg: MuteAllExceptPresenterRequest) {
     meetingMuted = msg.mute
     outGW.send(new MeetingMuted(meetingID, recorded, meetingMuted))
@@ -61,7 +62,7 @@ trait UsersApp {
       outGW.send(new MuteVoiceUser(meetingID, recorded, msg.requesterID, u.userID, msg.mute))
     }       
   }
-    
+    
   def handleMuteMeetingRequest(msg: MuteMeetingRequest) {
     meetingMuted = msg.mute
     outGW.send(new MeetingMuted(meetingID, recorded, meetingMuted))
@@ -73,15 +74,31 @@ trait UsersApp {
   def handleValidateAuthToken(msg: ValidateAuthToken) {
 //    println("*************** Got ValidateAuthToken message ********************" )
     regUsers.get (msg.userId) match {
-      case Some(u) => {
-        logger.info("ValidateToken success: mid=[" + meetingID + "] uid=[" + msg.userId + "]")
+      case Some(u) =>
+      {
+        val replyTo = meetingID + '/' + msg.userId
+
+        //send the reply
         outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, true, msg.correlationId))
+
+        //send the list of users in the meeting
+        outGW.send(new GetUsersReply(meetingID, msg.userId, users.getUsers))
+
+        //send chat history
+        this ! (new GetChatHistoryRequest(meetingID, msg.userId, replyTo))
+
+        //join the user
+        handleUserJoin(new UserJoining(meetingID, msg.userId))
+
+        //send the presentation
+        logger.info("ValidateToken success: mid=[" + meetingID + "] uid=[" + msg.userId + "]")
+        this ! (new GetPresentationInfo(meetingID, msg.userId, replyTo))
       }
       case None => {
         logger.info("ValidateToken failed: mid=[" + meetingID + "] uid=[" + msg.userId + "]")
         outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, false, msg.correlationId))
       }
-    }  
+    }
   }
   
   def handleRegisterUser(msg: RegisterUser) {
@@ -129,8 +146,23 @@ trait UsersApp {
       case None => // do nothing
     }
   }
-   
-      
+
+  def handleLockUser(msg: LockUser) {
+    
+  }
+  
+  def handleLockAllUsers(msg: LockAllUsers) {
+    
+  }
+  
+  def handleGetLockSettings(msg: GetLockSettings) {
+    
+  }
+  
+  def handleIsMeetingLocked(msg: IsMeetingLocked) {
+    
+  }
+
   def handleSetLockSettings(msg: SetLockSettings) {
 //    println("*************** Received new lock settings ********************")
     if (!permissionsEqual(msg.settings)) {
@@ -228,7 +260,7 @@ trait UsersApp {
       outGW.send(new UserUnsharedWebcam(meetingID, recorded, uvo.userID, stream))
     }     
   }
-	                         
+	                         
   def handleChangeUserStatus(msg: ChangeUserStatus):Unit = {    
 	if (users.hasUser(msg.userID)) {
 		  outGW.send(new UserStatusChange(meetingID, recorded, msg.userID, msg.status, msg.value))
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala
index bbbe0629367cf7e86057db6a3e487b8609a6bef6..d0d8a82027ef98db8ff61035915661d3f63ba895 100644
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala
@@ -43,8 +43,7 @@ trait WhiteboardApp {
 	    wbModel.modifyText(wbId, shape)
 	} else {
 //	    println("Received UNKNOWN whiteboard shape!!!!. status=[" + status + "], shapeType=[" + shapeType + "]")
-	}
-      
+	}  
     wbModel.getWhiteboard(wbId) foreach {wb =>
 //        println("WhiteboardApp::handleSendWhiteboardAnnotationRequest - num shapes [" + wb.shapes.length + "]")
         outGW.send(new SendWhiteboardAnnotationEvent(meetingID, recorded, 
@@ -60,20 +59,20 @@ trait WhiteboardApp {
   }
   
   def handleGetWhiteboardShapesRequest(msg: GetWhiteboardShapesRequest) {
-//    println("WB: Received page history [" + msg.whiteboardId + "]")
+      //println("WB: Received page history [" + msg.whiteboardId + "]")
       wbModel.history(msg.whiteboardId) foreach {wb =>
           outGW.send(new GetWhiteboardShapesReply(meetingID, recorded, 
-                       msg.requesterID, wb.id, wb.shapes.toArray, msg.replyTo))         
+                       msg.requesterID, wb.id, wb.shapes.toArray, msg.replyTo))
       }
-    }
-    
+  }
+
   def handleClearWhiteboardRequest(msg: ClearWhiteboardRequest) {
-//    println("WB: Received clear whiteboard")
+      //println("WB: Received clear whiteboard")
       wbModel.clearWhiteboard(msg.whiteboardId)
       wbModel.getWhiteboard(msg.whiteboardId) foreach {wb =>
           outGW.send(new ClearWhiteboardEvent(meetingID, recorded, 
-                       msg.requesterID, wb.id))        
-      }      
+                       msg.requesterID, wb.id))
+      }
     }
     
   def handleUndoWhiteboardRequest(msg: UndoWhiteboardRequest) {
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala
index ad82351546e0eb416be322205c788a82f30a380f..79973fb14798a06323fd7b1014153d8760980ad7 100755
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala
@@ -23,7 +23,8 @@ class MeetingEventRedisPublisher(service: MessageSender) extends OutMessageListe
       case msg: MeetingDestroyed              => handleMeetingDestroyed(msg)
 	    case msg: KeepAliveMessageReply         => handleKeepAliveMessageReply(msg)
 	    case msg: StartRecording                => handleStartRecording(msg)
-      case msg: StopRecording                 => handleStopRecording(msg)  
+      case msg: StopRecording                 => handleStopRecording(msg) 
+      case msg: GetAllMeetingsReply           => handleGetAllMeetingsReply(msg)
 	    case _ => //println("Unhandled message in MeetingEventRedisPublisher")
 	  }
   }
@@ -84,5 +85,10 @@ class MeetingEventRedisPublisher(service: MessageSender) extends OutMessageListe
   private def handleMeetingHasEnded(msg: MeetingHasEnded) {
     val json = MeetingMessageToJsonConverter.meetingHasEndedToJson(msg)
     service.send(MessagingConstants.FROM_MEETING_CHANNEL, json)
-  }  
+  }
+
+  private def handleGetAllMeetingsReply(msg: GetAllMeetingsReply) {
+    val json = MeetingMessageToJsonConverter.getAllMeetingsReplyToJson(msg)
+    service.send(MessagingConstants.FROM_MEETING_CHANNEL, json)
+  }
 }
\ No newline at end of file
diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala
index 915c9e7e9696a6faf718e9e886d78136d7581a19..6215c01c0aee4575c9872d9dd660cda6e42d6cdc 100644
--- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala
+++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala
@@ -108,7 +108,7 @@ object MeetingMessageToJsonConverter {
     Util.buildJson(header, payload)
   }
 
-	def stopRecordingToJson(msg: StopRecording):String = {
+  def stopRecordingToJson(msg: StopRecording):String = {
     val payload = new java.util.HashMap[String, Any]()
     payload.put(Constants.MEETING_ID, msg.meetingID)
     payload.put(Constants.RECORDED, msg.recorded) 
@@ -117,5 +117,12 @@ object MeetingMessageToJsonConverter {
     val header = Util.buildHeader(MessageNames.STOP_RECORDING, msg.version, None)
     Util.buildJson(header, payload)
   }
-	
+
+  def getAllMeetingsReplyToJson(msg: GetAllMeetingsReply):String = {
+    val payload = new java.util.HashMap[String, Any]()
+    payload.put("meetings", msg.meetings)
+
+    val header = Util.buildHeader(MessageNames.GET_ALL_MEETINGS_REPLY, msg.version, None)
+    Util.buildJson(header, payload)
+  }
 }
\ No newline at end of file
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml
index 8a173fba0c8dcf2c1e77ae1b317d6578f677f16e..02237809f7ff0a1005bf67122bbec543b40b0ee2 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml
@@ -34,5 +34,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	<bean id="chat.service" class="org.bigbluebutton.conference.service.chat.ChatService">
 		<property name="chatApplication"> <ref local="chatApplication"/></property>
 	</bean>
-	
+
+	<bean id="chatMessageListener" class="org.bigbluebutton.conference.service.chat.ChatMessageListener">
+		<property name="bigBlueButtonInGW" ref="bbbInGW" />
+	</bean>
 </beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml
index 2201a5ff352b40c8c089cf8f1730c1e182c0e40b..1438b14c19f1952f01c7a0a77208298f12a512f3 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
@@ -42,12 +42,4 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	<bean id="presentationMessageListener" class="org.bigbluebutton.conference.service.presentation.PresentationMessageListener">
 		<property name="conversionUpdatesProcessor" ref="conversionUpdatesProcessor" />
 	</bean>
-	
-	<bean id="chatMessageListener" class="org.bigbluebutton.conference.service.chat.ChatMessageListener">
-		<property name="bigBlueButtonInGW" ref="bbbInGW" />
-	</bean>
-
-	<bean id="participantsListener" class="org.bigbluebutton.conference.service.participants.ParticipantsListener">
-		<property name="bigBlueButtonInGW" ref="bbbInGW" />
-	</bean>
 </beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml
index 0e8e62847c4a65bf38619038eab3dc94fd32baac..a3eb65037368452f389712d141ead8e2d7f7b184 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
@@ -26,7 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			http://www.springframework.org/schema/util 
 			http://www.springframework.org/schema/util/spring-util-2.0.xsd
 			">
-	    		
+
 	<bean id="participantsHandler" class="org.bigbluebutton.conference.service.participants.ParticipantsHandler">
 		<property name="participantsApplication"> <ref local="participantsApplication"/></property>
 	</bean>
@@ -38,5 +38,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 	<bean id="participants.service" class="org.bigbluebutton.conference.service.participants.ParticipantsService">
 		<property name="participantsApplication"> <ref local="participantsApplication"/></property>
 	</bean>
-  			 
+
+	<bean id="participantsListener" class="org.bigbluebutton.conference.service.participants.ParticipantsListener">
+		<property name="bigBlueButtonInGW" ref="bbbInGW" />
+	</bean>
 </beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml
index 8c426036f8c4b7c83c1019d775dc6b2e0c43ce04..adcfb71c3ad2909bf40a9aea324b2709d185b4ca 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
@@ -26,13 +26,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			http://www.springframework.org/schema/util 
 			http://www.springframework.org/schema/util/spring-util-2.0.xsd
 			">
-	
+
 	<bean id="whiteboardApplication" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardApplication">
-        <property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
+		<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
 	</bean>
-	
+
 	<bean id="whiteboard.service" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardService">
 		<property name="whiteboardApplication"> <ref local="whiteboardApplication"/></property>
 	</bean>
-	
+
+	<bean id="whiteboardListener" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardListener">
+		<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
+	</bean>
+
 </beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml
index 47504b0d9f4ff10a03a032a9867ac3089b7a34bc..5563af25782d8361a0fea270e2be2824eb2e296f 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
@@ -26,35 +26,36 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 			http://www.springframework.org/schema/util 
 			http://www.springframework.org/schema/util/spring-util-2.0.xsd
 			">
-	
-	
+
     <bean id="redisMessageSender" class="org.bigbluebutton.conference.service.messaging.redis.MessageSender" 
                     init-method="start" destroy-method="stop">
-    	<property name="redisPool"> <ref bean="redisPool"/></property>
-  	</bean>
+      <property name="redisPool"> <ref bean="redisPool"/></property>
+    </bean>
 
     <bean id="redisMessageReceiver" class="org.bigbluebutton.conference.service.messaging.redis.MessageReceiver" 
                     init-method="start" destroy-method="stop">
-    	<property name="redisPool"> <ref bean="redisPool"/></property>
-    	<property name="messageHandler"> <ref local="redisMessageHandler"/> </property>
-  	</bean>
+      <property name="redisPool"> <ref bean="redisPool"/></property>
+      <property name="messageHandler"> <ref local="redisMessageHandler"/> </property>
+    </bean>
 
     <bean id="redisMessageHandler" class="org.bigbluebutton.conference.service.messaging.redis.ReceivedMessageHandler" 
                     init-method="start" destroy-method="stop">
       <property name="messageDistributor"><ref bean="redisMessageDistributor" /></property>
     </bean>
-  	
-  	<bean id="redisMessageDistributor" class="org.bigbluebutton.conference.service.messaging.redis.MessageDistributor">
-  	     <property name="messageHandler"> <ref local="redisMessageHandler"/> </property>
-  	     <property name="messageListeners">
-            <set>
-                <ref bean="presentationMessageListener" />
-                <ref bean="chatMessageListener" />
-                <ref bean="meetingMessageHandler" />
-                <ref bean="pollMessageHandler" />
-                <ref bean="participantsListener" />
-            </set>
-        </property> 
-  	</bean>	  	
- 
+
+    <bean id="redisMessageDistributor" class="org.bigbluebutton.conference.service.messaging.redis.MessageDistributor">
+       <property name="messageHandler"> <ref local="redisMessageHandler"/> </property>
+       <property name="messageListeners">
+          <set>
+              <ref bean="presentationMessageListener" />
+              <ref bean="chatMessageListener" />
+              <ref bean="meetingMessageHandler" />
+              <ref bean="pollMessageHandler" />
+              <ref bean="participantsListener" />
+              <ref bean="voiceMessageListener" />
+              <ref bean="whiteboardListener" />
+          </set>
+      </property>
+    </bean>
+
 </beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml
index bf2d885820935bd01ea9828280f5eb15b0115b05..308802613c3e833a32a2624035623069d66754bf 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
@@ -23,13 +23,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
     xmlns:beans="http://www.springframework.org/schema/beans"
     xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
-	
+
 	<beans:bean id="voiceEventRecorder" class="org.bigbluebutton.webconference.voice.VoiceEventRecorder">
 		<beans:property name="recorderApplication" ref="recorderApplication"/>
 	</beans:bean>
-				
+
 	<beans:bean id="voice.service" class="org.bigbluebutton.conference.service.voice.VoiceService">
 		<beans:property name="bigBlueButtonInGW" ref="bbbInGW"/>
 	</beans:bean>
-	
+
+	<beans:bean id="voiceMessageListener" class="org.bigbluebutton.conference.service.voice.VoiceMessageListener">
+		<beans:property name="bigBlueButtonInGW" ref="bbbInGW" />
+	</beans:bean>
+
 </beans:beans>
diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml
index 5e3f8ffa658400e806ddaf7d651c5f49e1e8b574..d1fb2b56b58f5f991f20e7080d66bdd10c00c329 100755
--- a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml
+++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml
@@ -3,7 +3,7 @@
 
 BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 
-Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 
 This program is free software; you can redistribute it and/or modify it under the
 terms of the GNU Lesser General Public License as published by the Free Software
diff --git a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy
index 2dad58221d670c5ba3f70a81d632408d8837ce49..c6175a35e4afe881e76666616dc65619df0789b3 100755
--- a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy
+++ b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy
@@ -24,6 +24,14 @@ class UrlMappings {
 		"/presentation/$conference/$room/$presentation_name/thumbnail/$id"(controller:"presentation") {
 			action = [GET:'showThumbnail']
 		}
+
+		"/presentation/$conference/$room/$presentation_name/pngs"(controller:"presentation") {
+			action = [GET:'numberOfPngs']
+		}
+
+		"/presentation/$conference/$room/$presentation_name/png/$id"(controller:"presentation") {
+			action = [GET:'showPngImage']
+		}
 	  
 		"/presentation/$conference/$room/$presentation_name/textfiles"(controller:"presentation") {
 			action = [GET:'numberOfTextfiles']
diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
index 5b29897b398b82448bef950a4bfcd76e6a55640d..041af441a751b5b087f3fd5cd05b05c0494d677e 100755
--- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
+++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties
@@ -68,6 +68,11 @@ maxNumPages=200
 # Maximum swf file size for load to the client (default 500000).
 MAX_SWF_FILE_SIZE=500000
 
+#----------------------------------------------------
+# Additional conversion of the presentation slides to PNG
+# to be used in the HTML5 client
+pngImagesRequired=false
+
 # Default number of digits for voice conference users joining through the PSTN.
 defaultNumDigitsForTelVoice=5
 
diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml
index 48b13ebb91e886533f21902f6fad1b4eaea30c80..14501b7d88acd994020da998dbfdb8dc2c4414d6 100755
--- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml
+++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml
@@ -96,6 +96,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
 		<property name="maxSwfFileSize" value="${MAX_SWF_FILE_SIZE}"/>
 		<property name="maxConversionTime" value="${maxConversionTime}"/>
 		<property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/>
+		<property name="pngImagesRequired" value="${pngImagesRequired}"/>
 	</bean>	
 	
 	<bean id="imageToSwfSlidesGenerationService" class="org.bigbluebutton.presentation.imp.ImageToSwfSlidesGenerationService">
diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
index 2b44056aa68c8c32574d1b09e57f91e53259cc9d..99319f9756a272441047c6aeb6c9511463e39f7c 100644
--- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
+++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy
@@ -303,6 +303,8 @@ class ApiController {
     boolean redirectImm = parseBoolean(params.redirectImmediately)
     
 	String internalUserID = RandomStringUtils.randomAlphanumeric(12).toLowerCase()
+
+	String authToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase()
 	
     String externUserID = params.userID
     if (StringUtils.isEmpty(externUserID)) {
@@ -346,6 +348,7 @@ class ApiController {
 	}
 	
 	UserSession us = new UserSession();
+	us.authToken = authToken;
 	us.internalUserId = internalUserID
   us.conferencename = meeting.getName()
   us.meetingID = meeting.getInternalId()
@@ -382,7 +385,7 @@ class ApiController {
 	meetingService.addUserSession(session['user-token'], us);
 	
 	// Register user into the meeting.
-	meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.internalUserId /* authToken for now */)
+	meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.authToken)
 	
 	log.info("Session user token for " + us.fullname + " [" + session['user-token'] + "]")	
     session.setMaxInactiveInterval(SESSION_TIMEOUT);
@@ -421,7 +424,7 @@ class ApiController {
 				message("You have joined successfully.")
 				meeting_id(us.meetingID)
 				user_id(us.internalUserId)
-				auth_token(us.internalUserId)
+				auth_token(us.authToken)
 			  }
 			}
 		  }
@@ -1353,6 +1356,7 @@ class ApiController {
               externMeetingID = us.externMeetingID
               externUserID = us.externUserID
               internalUserID = us.internalUserId
+              authToken = us.authToken
               role = us.role
               conference = us.conference
               room = us.room 
diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy
index ccd4229e5d3d1e0e5ac86f3a71839093e0404fe3..d82459beb84841c0eea149c2e34c6a80fbf54bfc 100644
--- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy
+++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy
@@ -297,7 +297,28 @@ class PresentationController {
         }
       }		
   }
-  
+
+  def numberOfPngs = {
+    def filename = params.presentation_name
+    def f = confInfo()
+    def numPngs = presentationService.numberOfPngs(f.conference, f.room, filename)
+      withFormat {
+        xml {
+          render(contentType:"text/xml") {
+            conference(id:f.conference, room:f.room) {
+              presentation(name:filename) {
+                pngs(count:numPngs) {
+                  for (def i=0;i<numPngs;i++) {
+                      png(name:"pngs/${i}")
+                    }
+                }
+              }
+            }
+          }
+        }
+      }
+  }
+
   def numberOfTextfiles = {
 	  def filename = params.presentation_name
 	  def f = confInfo()
diff --git a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy
index 422b8afa9a7eb99d11e8da8f75afba931028c125..2b1f6b0cc22f96f6db24bf59f1a0532b23897d8c 100644
--- a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy
+++ b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy
@@ -1,45 +1,45 @@
-/**
-* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
-*
-* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
-*
-* This program is free software; you can redistribute it and/or modify it under the
-* terms of the GNU Lesser General Public License as published by the Free Software
-* Foundation; either version 3.0 of the License, or (at your option) any later
-* version.
-*
-* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
-* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
-* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
-*
-* You should have received a copy of the GNU Lesser General Public License along
-* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-*
-*/
+/**
+* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
+*
+* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
+*
+* This program is free software; you can redistribute it and/or modify it under the
+* terms of the GNU Lesser General Public License as published by the Free Software
+* Foundation; either version 3.0 of the License, or (at your option) any later
+* version.
+*
+* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License along
+* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
+*
+*/
 package org.bigbluebutton.web.services
 
 import java.util.concurrent.*;
-import java.lang.InterruptedException
-import org.bigbluebutton.presentation.DocumentConversionService
-import org.bigbluebutton.presentation.UploadedPresentation
+import java.lang.InterruptedException
+import org.bigbluebutton.presentation.DocumentConversionService
+import org.bigbluebutton.presentation.UploadedPresentation
 
 class PresentationService {
 
-    static transactional = false
-	DocumentConversionService documentConversionService
-	def presentationDir
-	def testConferenceMock
-	def testRoomMock
-	def testPresentationName
-	def testUploadedPresentation
-	def defaultUploadedPresentation
-	def presentationBaseUrl
-	
+    static transactional = false
+	DocumentConversionService documentConversionService
+	def presentationDir
+	def testConferenceMock
+	def testRoomMock
+	def testPresentationName
+	def testUploadedPresentation
+	def defaultUploadedPresentation
+	def presentationBaseUrl
+
   def deletePresentation = {conf, room, filename ->
     		def directory = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename)
-    		deleteDirectory(directory) 
+    		deleteDirectory(directory)
 	}
-	
+
 	def deleteDirectory = {directory ->
 		log.debug "delete = ${directory}"
 		/**
@@ -47,7 +47,7 @@ class PresentationService {
 		 * We need to delete files inside a directory before a
 		 * directory can be deleted.
 		**/
-		File[] files = directory.listFiles();				
+		File[] files = directory.listFiles();
 		for (int i = 0; i < files.length; i++) {
 			if (files[i].isDirectory()) {
 				deleteDirectory(files[i])
@@ -56,9 +56,9 @@ class PresentationService {
 			}
 		}
 		// Now that the directory is empty. Delete it.
-		directory.delete()	
+		directory.delete()
 	}
-	
+
 	def listPresentations = {conf, room ->
 		def presentationsList = []
 		def directory = roomDirectory(conf, room)
@@ -69,95 +69,99 @@ class PresentationService {
 				if( file.isDirectory() )
 					presentationsList.add( file.name )
 			}
-		}	
+		}
 		return presentationsList
 	}
-
-  def getPresentationDir = {
-    return presentationDir
+
+  def getPresentationDir = {
+    return presentationDir
   }
-	
-	def processUploadedPresentation = {uploadedPres ->	
-		// Run conversion on another thread.
-		Timer t = new Timer(uploadedPres.getName(), false)
-		
-		t.runAfter(1000) {
-			try {
-				documentConversionService.processDocument(uploadedPres)
-			} finally {
-		    t.cancel()
-			} 
-		}
-	}
- 	
+
+    def processUploadedPresentation = {uploadedPres ->
+        // Run conversion on another thread.
+        Timer t = new Timer(uploadedPres.getName(), false)
+
+        t.runAfter(1000) {
+            try {
+                documentConversionService.processDocument(uploadedPres)
+            } finally {
+            t.cancel()
+            }
+        }
+    }
+
 	def showSlide(String conf, String room, String presentationName, String id) {
 		new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "slide-${id}.swf")
-	}
-	
-	def showPngImage(String conf, String room, String presentationName, String id) {
-		new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + id)
 	}
-	
+
+	def showPngImage(String conf, String room, String presentationName, String id) {
+		new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + "slide${id}.png")
+	}
+
 	def showPresentation = {conf, room, filename ->
 		new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename + File.separatorChar + "slides.swf")
 	}
-	
-	def showThumbnail = {conf, room, presentationName, thumb ->
+
+	def showThumbnail = {conf, room, presentationName, thumb ->
 		println "Show thumbnails request for $presentationName $thumb"
 		def thumbFile = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar +
 					"thumbnails" + File.separatorChar + "thumb-${thumb}.png"
 		log.debug "showing $thumbFile"
-		
+
 		new File(thumbFile)
-	}
-	
-	def showTextfile = {conf, room, presentationName, textfile ->
-		println "Show textfiles request for $presentationName $textfile"
-		def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar +
-					"textfiles" + File.separatorChar + "slide-${textfile}.txt"
-		log.debug "showing $txt"
-		
-		new File(txt)
 	}
-	
+
+	def showTextfile = {conf, room, presentationName, textfile ->
+		println "Show textfiles request for $presentationName $textfile"
+		def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar +
+					"textfiles" + File.separatorChar + "slide-${textfile}.txt"
+		log.debug "showing $txt"
+		
+		new File(txt)
+	}
+
 	def numberOfThumbnails = {conf, room, name ->
 		def thumbDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "thumbnails")
 		thumbDir.listFiles().length
-	}
-	
-	def numberOfTextfiles = {conf, room, name ->
-		log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles"
-		def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles")
-		textfilesDir.listFiles().length
 	}
-	
-  def roomDirectory = {conf, room ->
-      return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room)
-  }
-
-	def testConversionProcess() {
-		File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName)
-		
-		if (presDir.exists()) {
-			File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation)
-			if (pres.exists()) {
-				UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName);
-				uploadedPres.setUploadedFile(pres);
-				// Run conversion on another thread.
-				new Timer().runAfter(1000) 
-				{
-					documentConversionService.processDocument(uploadedPres)
-				}
-			} else {
-				log.error "${pres.absolutePath} does NOT exist"
-			}			
-		} else {
-			log.error "${presDir.absolutePath} does NOT exist."
-		}
-		
-	}
-	
-}	
+
+	def numberOfPngs = {conf, room, name ->
+		def PngsDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "pngs")
+		PngsDir.listFiles().length
+	}
+
+	def numberOfTextfiles = {conf, room, name ->
+		log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles"
+		def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles")
+		textfilesDir.listFiles().length
+	}
+
+  def roomDirectory = {conf, room ->
+      return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room)
+  }
+
+	def testConversionProcess() {
+		File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName)
+		
+		if (presDir.exists()) {
+			File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation)
+			if (pres.exists()) {
+				UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName);
+				uploadedPres.setUploadedFile(pres);
+				// Run conversion on another thread.
+				new Timer().runAfter(1000) 
+				{
+					documentConversionService.processDocument(uploadedPres)
+				}
+			} else {
+				log.error "${pres.absolutePath} does NOT exist"
+			}			
+		} else {
+			log.error "${presDir.absolutePath} does NOT exist."
+		}
+		
+	}
+}
 
 /*** Helper classes **/
 import java.io.FilenameFilter;
@@ -166,4 +170,4 @@ class PngFilter implements FilenameFilter {
     public boolean accept(File dir, String name) {
         return (name.endsWith(".png"));
     }
-}
+}
diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java
index 480b835bd3ab7fabd8fe97882af5faa3af0ed6d1..57abb2bc9424ce702b4c88a0e470784dc2d78c06 100644
--- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java
+++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java
@@ -20,6 +20,7 @@
 package org.bigbluebutton.api.domain;
 
 public class UserSession {
+  public String authToken = null;
 	public String internalUserId = null;
 	public String conferencename = null;
   public String meetingID = null;
diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java
index 13f34180f378242f58dfde7c75f58b0fd2d0166c..219028f5b22198284be638e53146487bc4fd8427 100644
--- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java
+++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java
@@ -1,7 +1,7 @@
 /**
 * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
 * 
-* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below).
 *
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
@@ -59,6 +59,7 @@ public class PdfToSwfSlidesGenerationService {
 	private long MAX_CONVERSION_TIME = 5*60*1000;
 	private String BLANK_SLIDE;
 	private int MAX_SWF_FILE_SIZE;
+	private boolean pngImagesRequired;
 		
 	public void generateSlides(UploadedPresentation pres) {
 		log.debug("Generating slides");		
@@ -66,9 +67,14 @@ public class PdfToSwfSlidesGenerationService {
 		log.info("Determined number of pages. MeetingId=[" + pres.getMeetingId() + "], presId=[" + pres.getId() + "], name=[" + pres.getName() + "], numPages=[" + pres.getNumberOfPages() + "]");
 		if (pres.getNumberOfPages() > 0) {
 			convertPdfToSwf(pres);
-//			createPngImages(pres);
 			createTextFiles(pres);
 			createThumbnails(pres);
+
+			// only create PNG images if the configuration requires it
+			if (pngImagesRequired) {
+				createPngImages(pres);
+			}
+
 			notifier.sendConversionCompletedMessage(pres);
 		}		
 	}
@@ -207,6 +213,10 @@ public class PdfToSwfSlidesGenerationService {
 	public void setMaxSwfFileSize(int size) {
 		this.MAX_SWF_FILE_SIZE = size;
 	}
+
+	public void setPngImagesRequired(boolean png) {
+		this.pngImagesRequired = png;
+	}
 	
 	public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) {
 		this.thumbnailCreator = thumbnailCreator;
diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java
index 4750593bc2f6b51243bb6ae73c6c33c88dff5d32..e36a22639deb412c39d7c76aa030f326f08848e7 100755
--- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java
+++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java
@@ -66,7 +66,8 @@ public class PngImageCreatorImp implements PngImageCreator {
 	 		for(int i=1; i<=pres.getNumberOfPages(); i++){
 	 			File tmp = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "tmp" + File.separatorChar + "slide" + i + ".pdf");
 	 			File destpng = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".png");
-	 			COMMAND = IMAGEMAGICK_DIR + "/convert -density 300x300 -quality 90 +dither -depth 8 -colors 256 " + File.separatorChar + tmp.getAbsolutePath() + " " + destpng.getAbsolutePath();
+				COMMAND = IMAGEMAGICK_DIR + "/convert -density 300x300 -quality 90 +dither -depth 8 -colors 256 " + File.separatorChar + tmp.getAbsolutePath() + " " + destpng.getAbsolutePath();
+
 	 			done = new ExternalProcessExecutor().exec(COMMAND, 60000);
 	 			if(!done){
 	 				break;
diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java
index 5ddf09db959250a153ed013f1a9169585666676e..23978e7e3bd1f304dbae425546eb682a66e54abd 100755
--- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java
+++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java
@@ -1,7 +1,7 @@
 /**
 * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
-* 
-* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
+*
+* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
 *
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
@@ -69,7 +69,6 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
 	 	String source = pres.getUploadedFile().getAbsolutePath();
 	 	String dest;
 	 	String COMMAND = "";
-	 	
 	 	if(SupportedFileTypes.isImageFile(pres.getFileType())){
 	 		dest = thumbsDir.getAbsolutePath() + File.separator + TEMP_THUMB_NAME + ".png";
 	 		COMMAND = IMAGEMAGICK_DIR + "/convert -thumbnail 150x150 " + source + " " + dest;
@@ -77,22 +76,22 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
 	 		dest = thumbsDir.getAbsolutePath() + File.separator + "thumb-";
 	 		COMMAND = IMAGEMAGICK_DIR + "/gs -q -sDEVICE=pngalpha -dBATCH -dNOPAUSE -dNOPROMPT -dDOINTERPOLATE -dPDFFitPage -r16 -sOutputFile=" + dest +"%d.png " + source;
 	 	}
-	 	
+
 	 	boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000);
-	 	
+
 	 	if (done) {
 	 		return true;
-	 	} else {			
-			log.warn("Failed to create thumbnails: " + COMMAND);	 		
+	 	} else {
+			log.warn("Failed to create thumbnails: " + COMMAND);
 	 	}
 
-		return false;		
+		return false;
 	}
-	
+
 	private File determineThumbnailDirectory(File presentationFile) {
 		return new File(presentationFile.getParent() + File.separatorChar + "thumbnails");
 	}
-	
+
 	private void renameThumbnails(File dir) {
 		/*
 		 * If more than 1 file, filename like 'temp-thumb-X.png' else filename is 'temp-thumb.png'
@@ -137,17 +136,17 @@ public class ThumbnailCreatorImp implements ThumbnailCreator {
 			}
 		}
 	}
-	
+
 	private void copyBlankThumbnail(File thumb) {
 		try {
 			FileUtils.copyFile(new File(BLANK_THUMBNAIL), thumb);
 		} catch (IOException e) {
 			log.error("IOException while copying blank thumbnail.");
-		}		
+		}
 	}
-	
-	private void cleanDirectory(File directory) {	
-		File[] files = directory.listFiles();				
+
+	private void cleanDirectory(File directory) {
+		File[] files = directory.listFiles();
 		for (int i = 0; i < files.length; i++) {
 			files[i].delete();
 		}
diff --git a/client/bbb-html5-client/assets/css/navbar.less b/client/bbb-html5-client/assets/css/navbar.less
old mode 100644
new mode 100755
index 0f9b2e557fc4f44cbc4126f4e23ced0183bd2572..ecf578d29c64b5231aba2aa6bedd20f5c0a3a3e8
--- a/client/bbb-html5-client/assets/css/navbar.less
+++ b/client/bbb-html5-client/assets/css/navbar.less
@@ -69,7 +69,7 @@
   }
 
   .navbar-btn-group-right {
-	  dispaly:block;
+	  display:block;
 	  float:right;
   }
 }
diff --git a/client/bbb-html5-client/config.coffee b/client/bbb-html5-client/config.coffee
index 416d181cfc4c5ae4847b3eb04adbddc850985be6..210cdfbec64eceff5dcd46f9ac1afe40f79f295f 100755
--- a/client/bbb-html5-client/config.coffee
+++ b/client/bbb-html5-client/config.coffee
@@ -33,6 +33,7 @@ config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*"
 config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat"
 config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting"
 config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users"
+config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard"
 config.redis.internalChannels = {}
 config.redis.internalChannels.receive = "html5-receive"
 config.redis.internalChannels.reply = "html5-reply"
diff --git a/client/bbb-html5-client/lib/clientproxy.coffee b/client/bbb-html5-client/lib/clientproxy.coffee
index 21045477237921a05cb6415de17609aab029a816..46d253486376d9049711d5a69f31ff9b7c82cbdb 100755
--- a/client/bbb-html5-client/lib/clientproxy.coffee
+++ b/client/bbb-html5-client/lib/clientproxy.coffee
@@ -20,7 +20,7 @@ module.exports = class ClientProxy
     @io.sockets.on 'connection', (socket) =>
       log.debug({ client: socket.id }, "Client has connected.")
       socket.on 'message', (jsonMsg) =>
-        log.debug({ message: jsonMsg }, "Received message") # TODO to check whether only 'message' works or 'djhkwa' too
+        log.debug({ message: jsonMsg }, "Received message")
         @_handleMessage(socket, jsonMsg)
       socket.on 'disconnect', =>
         @_handleClientDisconnected socket
@@ -49,8 +49,10 @@ module.exports = class ClientProxy
     callback?()
 
   _handleClientDisconnected: (socket) ->
-    if socket.userId?
-      log.info("User [#{socket.userId}] has disconnected.")
+    console.log "\ntrying to disconnect"
+
+    #if socket.userId?
+    #  log.info("User [#{socket.userId}] has disconnected.")
 
   _handleMessage: (socket, message) ->
     if message.header?.name?
@@ -64,6 +66,8 @@ module.exports = class ClientProxy
         @_handleLoginMessage socket, message
       when 'send_public_chat_message'
         @controller.sendingChat message
+      when 'user_leaving_request'
+        @controller.sendingUsersMessage message
       else
         log.error({ message: message }, 'Unknown message name.')
 
diff --git a/client/bbb-html5-client/lib/controller.coffee b/client/bbb-html5-client/lib/controller.coffee
index f8b1327ca59b0c058495a74f76470fad22efbcc0..5d1c93c616affcd51a21ffc38b9548d93e54abef 100755
--- a/client/bbb-html5-client/lib/controller.coffee
+++ b/client/bbb-html5-client/lib/controller.coffee
@@ -35,4 +35,8 @@ module.exports = class Controller
     #   @clientProxy.endMeeting()
 
   sendingChat: (data) =>
-    @messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data)
\ No newline at end of file
+    @messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data)
+
+
+  sendingUsersMessage: (data) =>
+    @messageBus.sendingToRedis(config.redis.channels.toBBBApps.users, data)
diff --git a/client/bbb-html5-client/lib/messagebus.coffee b/client/bbb-html5-client/lib/messagebus.coffee
index 20174d840465520335224660f027ec8606ab4b5a..e6723ba89799f275215e35e0f71fbdb7df8178ff 100755
--- a/client/bbb-html5-client/lib/messagebus.coffee
+++ b/client/bbb-html5-client/lib/messagebus.coffee
@@ -1,8 +1,8 @@
-postal = require('postal')
 crypto = require 'crypto'
+postal = require 'postal'
 
 config = require '../config'
-log = require './bbblogger'
+log    = require './bbblogger'
 
 moduleDeps = ["RedisPubSub"]
 
@@ -40,4 +40,4 @@ module.exports = class MessageBus
       data: data
 
   sendingToRedis: (channel, message) =>
-    @pubSub.publishing(channel, message)
\ No newline at end of file
+    @pubSub.publishing(channel, message)
diff --git a/client/bbb-html5-client/lib/redispubsub.coffee b/client/bbb-html5-client/lib/redispubsub.coffee
index aa05f96534474825277c27cd5bc86c0eb79173d7..c8e007f391772c5eb274c0e4b33654d1a53eb3d0 100644
--- a/client/bbb-html5-client/lib/redispubsub.coffee
+++ b/client/bbb-html5-client/lib/redispubsub.coffee
@@ -58,7 +58,7 @@ module.exports = class RedisPubSub
     # put the entry in the hash so we can match the response later
     @pendingRequests[correlationId] = entry
     message.header.reply_to = correlationId
-    console.log("\n\nmessage=" + JSON.stringify(message) + "\n\n")
+    console.log "\n  Waiting for a reply on:" + JSON.stringify(message)
     log.info({ message: message, channel: config.redis.channels.toBBBApps.meeting}, "Publishing a message")
     @pubClient.publish(config.redis.channels.toBBBApps.meeting, JSON.stringify(message))
 
@@ -73,16 +73,16 @@ module.exports = class RedisPubSub
     # TODO: this has to be in a try/catch block, otherwise the server will
     #   crash if the message has a bad format
     message = JSON.parse(jsonMsg)
+    correlationId = message.payload?.reply_to or message.header?.reply_to
 
-    unless message.header?.name is "keep_alive_reply" #temporarily stop logging the keep_alive_reply message
+    unless message.header?.name is "keep_alive_reply"
+      console.log "\nchannel=" + channel
+      console.log "correlationId=" + correlationId if correlationId?
+      console.log "eventType=" + message.header?.name + "\n"
       log.debug({ pattern: pattern, channel: channel, message: message}, "Received a message from redis")
-    #console.log "=="+JSON.stringify message
 
     # retrieve the request entry
 
-    #correlationId = message.header?.reply_to
-    correlationId = message.payload?.reply_to or message.header?.reply_to
-    console.log "\ncorrelation_id=" + correlationId
     if correlationId? and @pendingRequests?[correlationId]?
       entry = @pendingRequests[correlationId]
       # make sure the message in the timeout isn't triggered by clearing it
@@ -94,76 +94,63 @@ module.exports = class RedisPubSub
         topic: entry.replyTo.topic
         data: message
     else
-      #sendToController(message)
-
-    if message.header?.name is 'validate_auth_token_reply'
-      if message.payload?.valid is "true"
-
-        #TODO use the message library for these messages. Perhaps put it in Modules?!
-
-        joinMeetingMessage = {
-          "payload": {
-            "meeting_id": message.payload.meeting_id
-            "user_id": message.payload.userid
-          },
-          "header": {
-            "timestamp": new Date().getTime()
-            "reply_to": message.payload.meeting_id + "/" + message.payload.userid
-            "name": "user_joined_event"
-          }
-        }
-        # the user joins the meeting
-
-        @pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(joinMeetingMessage))
-        console.log "just published the joinMeetingMessage in RedisPubSub"
-
-        #get the list of users in the meeting
-        getUsersMessage = {
-          "payload": {
-            "meeting_id": message.payload.meeting_id
-            "requester_id": message.payload.userid
-          },
-          "header": {
-            "timestamp": new Date().getTime()
-            "reply_to": message.payload.meeting_id + "/" + message.payload.userid
-            "name": "get_users_request"
-          }
-        }
-
-        @pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(getUsersMessage))
-        console.log "just published the getUsersMessage in RedisPubSub"
-
-        #get the chat history
-        getChatHistory = {
+      if message.header?.name is 'get_presentation_info_reply'
+        #filter for the current=true page on the server-side
+        currentPage = null
+        numCurrentPage = null
+        presentations = message.payload?.presentations
+
+        for presentation in presentations
+          pages = presentation.pages
+
+          for page in pages
+            if page.current is true
+              currentPage = page
+              numCurrentPage = page.num
+
+        console.log "\n\n\n\n the message is: " + JSON.stringify message
+        console.log "\n" + message.payload?.presentations[0]?.id + "/" + numCurrentPage + "\n\n"
+        #request the whiteboard information
+        requestMessage = {
           "payload": {
-            "meeting_id": message.payload.meeting_id
-            "requester_id": message.payload.userid
+            "meeting_id": message.payload?.meeting_id
+            "requester_id": message.payload?.requester_id
+            "whiteboard_id": message.payload?.presentations[0]?.id + "/" + numCurrentPage #not sure if always [0]
           },
           "header": {
             "timestamp": new Date().getTime()
-            "reply_to": message.payload.meeting_id + "/" + message.payload.userid
-            "name": "get_chat_history"
+            "name": "get_whiteboard_shapes_request"
           }
         }
-
-        @pubClient.publish(config.redis.channels.toBBBApps.chat, JSON.stringify(getChatHistory))
-        console.log "just published the getChatHistory in RedisPubSub"
-
-
-    else if message.header?.name is 'get_users_reply'
-      console.log 'got a reply from bbb-apps for get users'
+        @publishing(config.redis.channels.toBBBApps.whiteboard, requestMessage)
+
+        #strip off excess data, leaving only the current slide information
+        message.payload.currentPage = currentPage
+        message.payload.presentations = null
+        message.header.name = "presentation_page"
+
+      else if message.header?.name is 'presentation_shared_message'
+        currentPage = null
+        presentation = message.payload?.presentation
+        for page in presentation.pages
+          if page.current is true
+            currentPage = page
+
+        #strip off excess data, leaving only the current slide information
+        message.payload.currentPage = currentPage
+        message.payload.presentation = null
+        message.header.name = "presentation_page"
+
+      else if message.header?.name is 'presentation_page_changed_message'
+        message.payload.currentPage = message.payload?.page
+        message.payload?.page = null
+        message.header.name = "presentation_page"
+
+      console.log "  Sending to Controller (In):" + message.header?.name
       sendToController(message)
 
-    else if message.header?.name is 'get_chat_history_reply'
-      console.log 'got a reply from bbb-apps for chat history'
-      sendToController(message)
-
-    else if message.header?.name is 'send_public_chat_message'
-      console.log "just got a public chat message :" + JSON.stringify message
-      sendToController (message)
-
   publishing: (channel, message) =>
-    console.log '\n Publishing\n'
+    console.log "Publishing #{message.header?.name}"
     @pubClient.publish(channel, JSON.stringify(message))
 
 sendToController = (message) ->
@@ -171,4 +158,3 @@ sendToController = (message) ->
     channel: config.redis.internalChannels.receive
     topic: "broadcast"
     data: message
-
diff --git a/client/bbb-html5-client/public/js/collections/users.coffee b/client/bbb-html5-client/public/js/collections/users.coffee
index 581d0a389f64df6684da49bd6c70c9edfc6156c9..9716ade8bd60768feb61f1083c40e26d224ef80e 100755
--- a/client/bbb-html5-client/public/js/collections/users.coffee
+++ b/client/bbb-html5-client/public/js/collections/users.coffee
@@ -5,7 +5,6 @@ define [
   'cs!models/user'
 ], (_, Backbone, globals, UserModel) ->
 
-  # TODO: this class should actually store UserModel's, for now it is only trigerring events
   UsersCollection = Backbone.Collection.extend
     model: UserModel
 
@@ -23,38 +22,41 @@ define [
 
     _registerEvents: ->
 
-      globals.events.on "connection:user_list_change", (users) =>
-        globals.events.trigger("users:user_list_change", users)
-
       globals.events.on "connection:load_users", (users) =>
+        #alert "load users"
         for userBlock in users
           @add [
-            id : userBlock.id
-            userid: userBlock.id
-            username: userBlock.name
+            new UserModel {id: userBlock.id, userid: userBlock.id, username: userBlock.name}
           ]
         globals.events.trigger("users:load_users", users)
 
-      globals.events.on "connection:user_join", (userid, username) =>
-        console.log "users.coffee: on(connection:user_join)" + username
-        @add [
-          id : userid
-          userid: userid
-          username: username
-        ]
-        globals.events.trigger("users:user_join", userid, username)
+      #globals.events.on "getUsers", =>
+        #users = @toJSON()
+        #globals.events.trigger("receiveUsers", users)      
 
-      globals.events.on "connection:user_leave", (userid) =>
-        toDel = @get(userid)
-        @remove(toDel)
-        globals.events.trigger("users:user_leave", userid)
+      globals.events.on "connection:user_join", (newUserid, newUsername) =>
+        unless @get(newUserid)? #check if the user is already present
+          #newUser = new UserModel {id: newUserid, userid: newUserid, username: newUsername}
+          newUser = new UserModel() 
+          newUser.id = newUserid
+          newUser.userid = newUserid
+          newUser.username = newUsername
+
+          @add [
+            newUser
+          ]
+          globals.events.trigger("user:add_new_user", newUser)
 
       globals.events.on "connection:user_left", (userid) =>
         toDel = @get(userid)
-        @remove(toDel)
-        globals.events.trigger("users:user_left", userid)
+        if toDel? # only remove if the user model was found
+          @remove(toDel)
+          globals.events.trigger("users:user_left", userid)
 
       globals.events.on "connection:setPresenter", (userid) =>
         globals.events.trigger("users:setPresenter", userid)
 
+      render: ->
+        alert "user collection rendering"
+
   UsersCollection
diff --git a/client/bbb-html5-client/public/js/models/connection.coffee b/client/bbb-html5-client/public/js/models/connection.coffee
index 99f2300c88016ac38e07400fd0d6c7f69c3a699d..165deecbb2de6d88d44af982db322a177d13ea7e 100755
--- a/client/bbb-html5-client/public/js/models/connection.coffee
+++ b/client/bbb-html5-client/public/js/models/connection.coffee
@@ -17,13 +17,10 @@ define [
       @userId = @getUrlVars()["user_id"]
       @meetingId = @getUrlVars()["meeting_id"]
       @username = @getUrlVars()["username"]
+      globals.meetingName = decodeURI(@getUrlVars()["meetingName"])      
 
     disconnect: ->
-      if @socket?
-        console.log "disconnecting from", @host
-        @socket.disconnect()
-      else
-        console.log "tried to disconnect but it's not connected"
+      alert( " i go through disconnect") # not used right now
 
     connect: ->
       console.log("user_id=" + @userId + " auth_token=" + @authToken + " meeting_id=" + @meetingId)
@@ -57,12 +54,10 @@ define [
         console.log "socket.io received: data"
         globals.events.trigger("message", data)
 
-
       # Immediately say we are connected
       @socket.on "connect", =>
         console.log "socket on: connect"
         globals.events.trigger("connection:connected")
-        #@socket.emit "user connect" # tell the server we have a new user
 
         message = {
           "payload": {
@@ -80,17 +75,22 @@ define [
         if @authToken? and @userId? and @meetingId?
           @socket.emit "message", message
 
+      # Received a list of users from bbb-apps
+      # param {object} message object
       @socket.on "get_users_reply", (message) =>
-        users = []
-        for user in message.payload?.users
-          users.push user
+        requesterId = message.payload?.requester_id
 
-        globals.events.trigger("connection:load_users", users)
+        if(requesterId is @userId)
+          users = []
+          for user in message.payload?.users
+            users.push user
 
+          globals.events.trigger("connection:load_users", users)
+
+      # Received a the chat history for a meeting
+      # @param {object} message object
       @socket.on "get_chat_history_reply", (message) =>
         requesterId = message.payload?.requester_id
-
-        #console.log("my_id=" + @userId + ", while requester_id=" + requesterId)
         if(requesterId is @userId)
           globals.events.trigger("connection:all_messages", message.payload?.chat_history)
 
@@ -102,12 +102,6 @@ define [
         text = message.payload.message.message
         globals.events.trigger("connection:msg", username, text)
 
-      # Received event to logout yourself
-      @socket.on "logout", ->
-        console.log "socket on: logout"
-        Utils.postToUrl "logout"
-        window.location.replace "./"
-
       # If the server disconnects from the client or vice-versa
       @socket.on "disconnect", ->
         console.log "socket on: disconnect"
@@ -115,83 +109,91 @@ define [
         globals.events.trigger("connection:disconnected")
         @socket = null
 
-      @socket.on "reconnect", ->
-        console.log "socket on: reconnect"
-        globals.events.trigger("connection:reconnect")
+      #@socket.on "reconnect", ->
+      #  console.log "socket on: reconnect"
+      #  globals.events.trigger("connection:reconnect")
 
-      @socket.on "reconnecting", ->
-        console.log "socket on: reconnecting"
-        globals.events.trigger("connection:reconnecting")
+      #@socket.on "reconnecting", ->
+      #  console.log "socket on: reconnecting"
+      #  globals.events.trigger("connection:reconnecting")
 
-      @socket.on "reconnect_failed", ->
-        console.log "socket on: reconnect_failed"
-        globals.events.trigger("connection:reconnect_failed")
+      #@socket.on "reconnect_failed", ->
+      #  console.log "socket on: reconnect_failed"
+      #  globals.events.trigger("connection:reconnect_failed")
 
       # If an error occurs while not connected
       # @param  {string} reason Reason for the error.
-      @socket.on "error", (reason) ->
-        console.error "unable to connect socket.io", reason
+      #@socket.on "error", (reason) -> #TODO
+      #  console.error "unable to connect socket.io", reason
 
       # Received event to update all the slide images
       # @param  {Array} urls list of URLs to be added to the paper (after old images are removed)
-      @socket.on "all_slides", (allSlidesEventObject) =>
-        console.log "socket on: all_slides"
-        console.log "allSlidesEventObject: " + allSlidesEventObject
-        globals.events.trigger("connection:all_slides", allSlidesEventObject);
+      #@socket.on "all_slides", (allSlidesEventObject) =>
+      #  console.log "socket on: all_slides"
+      #  console.log "allSlidesEventObject: " + allSlidesEventObject
+      #  globals.events.trigger("connection:all_slides", allSlidesEventObject);
 
       # Received event to clear the whiteboard shapes
-      @socket.on "clrPaper",=>
-        console.log "socket on: clrPaper"
-        globals.events.trigger("connection:clrPaper")
+      #@socket.on "clrPaper",=>
+      #  console.log "socket on: clrPaper"
+      #  globals.events.trigger("connection:clrPaper")
+
+      # Received event to update all the shapes in the whiteboard
+      # @param  {Array} shapes Array of shapes to be drawn
+      #@socket.on "allShapes", (allShapesEventObject) =>
+      #  # check for the requester_id
+      #  console.log "socket on: all_shapes" + allShapesEventObject
+      #  globals.events.trigger("connection:all_shapes", allShapesEventObject)
 
       # Received event to update all the shapes in the whiteboard
       # @param  {Array} shapes Array of shapes to be drawn
-      @socket.on "allShapes", (allShapesEventObject) =>
-        console.log "socket on: all_shapes" + allShapesEventObject
-        globals.events.trigger("connection:all_shapes", allShapesEventObject)
+      #@socket.on "get_whiteboard_shapes_reply", (object) =>
+      #  if @userId is object.payload?.requester_id
+      #    #alert("I am getting some shapes reply" + JSON.stringify object)
+      #    for shape in object.payload?.shapes
+      #      #alert("for a shape:")
+      #      shape_type = shape.shape_type
+      #      globals.events.trigger("connection:whiteboard_draw_event", shape_type, shape.shape) # TODO to change the name
+
+      #      globals.events.trigger("connection:updShape", shape_type, shape.shape)
 
       # Received event to update a shape being created
       # @param  {string} shape type of shape being updated
       # @param  {Array} data   all information to update the shape
-      @socket.on "whiteboard_update_event", (data) =>
-        console.log "socket on: whiteboard_update_event"
-        shape = data.payload.shape_type
-        globals.events.trigger("connection:updShape", shape, data)
-
-      # Received event to create a shape on the whiteboard
-      # @param  {string} shape type of shape being made
-      # @param  {Array} data   all information to make the shape
-      @socket.on "whiteboard_draw_event", (data) =>
-        console.log "socket on: whiteboard_draw_event"
-        shape = data.payload.shape_type
+      @socket.on "send_whiteboard_shape_message", (data) =>
+        alert "send_whiteboard_shape_message" + JSON.stringify data
+        shape = data.payload.shape.shape_type
+        for point in data.payload.shape.shape.points
+          point = point/100 #early attempt to scale down
         globals.events.trigger("connection:whiteboard_draw_event", shape, data)
+        globals.events.trigger("connection:updShape", shape, data)
 
       # Pencil drawings are received as points from the server and painted as lines.
-      @socket.on "whiteboardDrawPen", (data) =>
-        console.log "socket on: whiteboardDrawPen"+  data
-        globals.events.trigger("connection:whiteboardDrawPen", data)
+      #@socket.on "whiteboardDrawPen", (data) =>
+      #  console.log "socket on: whiteboardDrawPen"+  data
+      #  globals.events.trigger("connection:whiteboardDrawPen", data)
 
       # Received event to update the cursor coordinates
       # @param  {number} x x-coord of the cursor as a percentage of page width
       # @param  {number} y y-coord of the cursor as a percentage of page height
-      @socket.on "mvCur", (data) =>
-        x = data.cursor.x #TODO change to new json structure
-        y = data.cursor.y #TODO change to new json structure
-        console.log "socket on: mvCur"
-        globals.events.trigger("connection:mvCur", x, y)
+      #@socket.on "mvCur", (data) =>
+      #  x = data.cursor.x #TODO change to new json structure
+      #  y = data.cursor.y #TODO change to new json structure
+      #  console.log "socket on: mvCur"
+      #  globals.events.trigger("connection:mvCur", x, y)
 
       # Received event to update the zoom or move the slide
       # @param  {number} x x-coord of the cursor as a percentage of page width
       # @param  {number} y y-coord of the cursor as a percentage of page height
-      @socket.on "move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) =>
-        console.log "socket on: move_and_zoom"
-        globals.events.trigger("connection:move_and_zoom", xOffset, yOffset, widthRatio, heightRatio)
+      #@socket.on "move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) =>
+      #  console.log "socket on: move_and_zoom"
+      #  globals.events.trigger("connection:move_and_zoom", xOffset, yOffset, widthRatio, heightRatio)
 
       # Received event to update the slide image
       # @param  {string} url URL of image to show
-      @socket.on "changeslide", (url) =>
-        console.log "socket on: changeslide"
-        globals.events.trigger("connection:changeslide", url)
+      #@socket.on "changeslide", (url) =>
+      #  console.log "socket on: changeslide"
+      #  globals.events.trigger("connection:changeslide", url)
 
       # Received event to update the viewBox value
       # @param  {string} xperc Percentage of x-offset from top left corner
@@ -199,69 +201,67 @@ define [
       # @param  {string} wperc Percentage of full width of image to be displayed
       # @param  {string} hperc Percentage of full height of image to be displayed
       # TODO: not tested yet
-      @socket.on "viewBox", (xperc, yperc, wperc, hperc) =>
-        console.log "socket on: viewBox"
-        globals.events.trigger("connection:viewBox", xperc, yperc, wperc, hperc)
+      #@socket.on "viewBox", (xperc, yperc, wperc, hperc) =>
+      #  console.log "socket on: viewBox"
+      #  globals.events.trigger("connection:viewBox", xperc, yperc, wperc, hperc)
 
 
       # Received event to update the zoom level of the whiteboard.
       # @param  {number} delta amount of change in scroll wheel
-      @socket.on "zoom", (delta) ->
-        console.log "socket on: zoom"
-        globals.events.trigger("connection:zoom", delta)
+      #@socket.on "zoom", (delta) ->
+      #  console.log "socket on: zoom"
+      #  globals.events.trigger("connection:zoom", delta)
 
       # Received event to update the whiteboard size and position
       # @param  {number} cx x-offset from top left corner as percentage of original width of paper
       # @param  {number} cy y-offset from top left corner as percentage of original height of paper
       # @param  {number} sw slide width as percentage of original width of paper
       # @param  {number} sh slide height as a percentage of original height of paper
-      @socket.on "paper", (cx, cy, sw, sh) ->
-        console.log "socket on: paper"
-        globals.events.trigger("connection:paper", cx, cy, sw, sh)
+      #@socket.on "paper", (cx, cy, sw, sh) ->
+      #  console.log "socket on: paper"
+      #  globals.events.trigger("connection:paper", cx, cy, sw, sh)
 
       # Received event when the panning action finishes
-      @socket.on "panStop", ->
-        console.log "socket on: panStop"
-        globals.events.trigger("connection:panStop")
+      #@socket.on "panStop", ->
+      #  console.log "socket on: panStop"
+      #  globals.events.trigger("connection:panStop")
 
 
       # Received event to denote when the text has been created
-      @socket.on "textDone", ->
-        console.log "socket on: textDone"
-        globals.events.trigger("connection:textDone")
+      #@socket.on "textDone", ->
+      #  console.log "socket on: textDone"
+      #  globals.events.trigger("connection:textDone")
 
       # Received event to update the status of the upload progress
       # @param  {string} message  update message of status of upload progress
       # @param  {boolean} fade    true if you wish the message to automatically disappear after 3 seconds
-      @socket.on "uploadStatus", (message, fade) =>
-        console.log "socket on: uploadStatus"
-        globals.events.trigger("connection:uploadStatus", message, fade)
+      #@socket.on "uploadStatus", (message, fade) =>
+      #  console.log "socket on: uploadStatus"
+      #  globals.events.trigger("connection:uploadStatus", message, fade)
 
       # Received event for a user list change
       # @param  {Array} users Array of names and publicIDs of connected users
       # TODO: event name with spaces is bad
-      @socket.on "user list change", (users) =>
-        console.log "socket on: user list change"
-        globals.events.trigger("connection:user_list_change", users)
+      #@socket.on "user list change", (users) =>
+      #  console.log "socket on: user list change"
+      #  globals.events.trigger("connection:user_list_change", users)
 
       # Received event for a new user
-      @socket.on "user_joined_event", (message) =>
-        console.log "message: " + message
-        userid = message.payload.user.id
+      @socket.on "user_joined_message", (message) =>
+        userid = message.payload.user.userid
         username = message.payload.user.name
-        globals.events.trigger("connection:user_join", userid, username) #should it be user_joined?! #TODO
+        globals.events.trigger("connection:user_join", userid, username)
 
       # Received event when a user leaves
-      @socket.on "user_left_event", (message) =>
-        console.log "message: " + message
-        userid = message.payload.user.id
+      @socket.on "user_left_message", (message) ->
+        userid = message.payload.user.userid
         globals.events.trigger("connection:user_left", userid)
 
       # Received event to set the presenter to a user
       # @param  {string} userID publicID of the user that is being set as the current presenter
-      @socket.on "setPresenter", (userid) =>
-        console.log "socket on: setPresenter"
-        globals.events.trigger("connection:setPresenter", userid)
+      #@socket.on "setPresenter", (userid) =>
+      #  console.log "socket on: setPresenter"
+      #  globals.events.trigger("connection:setPresenter", userid)
 
       # Received event to update all the messages in the chat box
       # @param  {Array} messages Array of messages in public chat box
@@ -269,10 +269,11 @@ define [
       #  console.log "socket on: all_messages" + allMessagesEventObject
       #  globals.events.trigger("connection:all_messages", allMessagesEventObject)
 
-      @socket.on "share_presentation_event", (data) =>
-        console.log "socket on: share_presentation_event"
-        globals.events.trigger("connection:share_presentation_event", data)
-
+      # Change the current slide/page [if any] with the one
+      # contained in the message
+      @socket.on "presentation_page", (message) ->
+        console.log "socket on: presentation_page"
+        globals.events.trigger("connection:display_page", message)
 
     # Emit an update to move the cursor around the canvas
     # @param  {number} x x-coord of the cursor as a percentage of page width
@@ -281,16 +282,12 @@ define [
       @socket.emit "mvCur", x, y
 
     # Requests the shapes from the server.
-    emitAllShapes: ->
-      @socket.emit "all_shapes"
+    #emitAllShapes: ->
+    #  @socket.emit "all_shapes"
 
-
-    # Emit a message to the server
-    # @param  {string} the message
+    # Emit a chat message to the server
+    # @param  {string} the chat message
     emitMsg: (msg) ->
-
-      console.log "emitting message: " + msg
-
       object = {
         "header": {
           "name": "send_public_chat_message"
@@ -314,33 +311,31 @@ define [
           }
         }
       }
-      
       @socket.emit "message", object
 
-
     # Emit the finish of a text shape
-    emitTextDone: ->
-      @socket.emit "textDone"
+    #emitTextDone: ->
+    #  @socket.emit "textDone"
 
     # Emit the creation of a shape
     # @param  {string} shape type of shape
     # @param  {Array} data  all the data required to draw the shape on the client whiteboard
-    emitMakeShape: (shape, data) ->
-      @socket.emit "makeShape", shape, data
+    #emitMakeShape: (shape, data) ->
+    #  @socket.emit "makeShape", shape, data
 
     # Emit the update of a shape
     # @param  {string} shape type of shape
     # @param  {Array} data  all the data required to update the shape on the client whiteboard
-    emitUpdateShape: (shape, data) ->
-      @socket.emit "updShape", shape, data
+    #emitUpdateShape: (shape, data) ->
+    #  @socket.emit "updShape", shape, data
 
     # Emit an update in the whiteboard position/size values
     # @param  {number} cx x-offset from top left corner as percentage of original width of paper
     # @param  {number} cy y-offset from top left corner as percentage of original height of paper
     # @param  {number} sw slide width as percentage of original width of paper
     # @param  {number} sh slide height as a percentage of original height of paper
-    emitPaperUpdate: (cx, cy, sw, sh) ->
-      @socket.emit "paper", cx, cy, sw, sh
+    #emitPaperUpdate: (cx, cy, sw, sh) ->
+    #  @socket.emit "paper", cx, cy, sw, sh
 
     # Update the zoom level for the clients
     # @param  {number} delta amount of change in scroll wheel
@@ -348,43 +343,58 @@ define [
       @socket.emit "zoom", delta
 
     # Request the next slide
-    emitNextSlide: ->
-      @socket.emit "nextslide"
+    #emitNextSlide: ->
+    #  @socket.emit "nextslide"
 
     # Request the previous slide
-    emitPreviousSlide: ->
-      @socket.emit "prevslide"
+    #emitPreviousSlide: ->
+    #  @socket.emit "prevslide"
 
     # Logout of the meeting
     emitLogout: ->
-      @socket.emit "logout"
+      message = {
+        "payload": {
+          "meeting_id": @meetingId
+          "userid": @userId
+        },
+        "header": {
+          "timestamp": new Date().getTime()
+          "name": "user_leaving_request"
+          "version": "0.0.1"
+        }
+      }
+      @socket.emit "message", message
+      @socket.disconnect()
+
+      #Utils.postToUrl "logout"
+      #window.location.replace "./"
 
     # Emit panning has stopped
-    emitPanStop: ->
-      @socket.emit "panStop"
+    #emitPanStop: ->
+    #  @socket.emit "panStop"
 
     # Publish a shape to the server to be saved
     # @param  {string} shape type of shape to be saved
     # @param  {Array} data   information about shape so that it can be recreated later
-    emitPublishShape: (shape, data) ->
-      @socket.emit "saveShape", shape, JSON.stringify(data)
+    #emitPublishShape: (shape, data) ->
+    #  @socket.emit "saveShape", shape, JSON.stringify(data)
 
     # Emit a change in the current tool
     # @param  {string} tool [description]
-    emitChangeTool: (tool) ->
-      @socket.emit "changeTool", tool
+    #emitChangeTool: (tool) ->
+    #  @socket.emit "changeTool", tool
 
     # Tell the server to undo the last shape
-    emitUndo: ->
-      @socket.emit "undo"
+    #emitUndo: ->
+    #  @socket.emit "undo"
 
     # Emit a change in the presenter
-    emitSetPresenter: (id) ->
-      @socket.emit "setPresenter", id
+    #emitSetPresenter: (id) ->
+    #  @socket.emit "setPresenter", id
 
     # Emit signal to clear the canvas
-    emitClearCanvas: (id) ->
-      @socket.emit "clrPaper", id
+    #emitClearCanvas: (id) ->
+    #  @socket.emit "clrPaper", id
 
     # Helper method to get the meeting_id, user_id and auth_token from the url
     getUrlVars: ->
diff --git a/client/bbb-html5-client/public/js/models/user.coffee b/client/bbb-html5-client/public/js/models/user.coffee
old mode 100644
new mode 100755
index 5f9b5718430d3df4de4975abd9fc10f8a3b85eac..cd955792ce6721effddd4377b60bd5260f8e30c1
--- a/client/bbb-html5-client/public/js/models/user.coffee
+++ b/client/bbb-html5-client/public/js/models/user.coffee
@@ -2,10 +2,26 @@ define [
   'underscore',
   'backbone',
   'globals'
-], (_, Backbone, globals) ->
+  'text!templates/user.html'
+], (_, Backbone, globals, userTemplate) ->
 
   UserModel = Backbone.Model.extend
 
-    initialize: ->
+  	defaults: 
+      id : null
+	  	userid: null
+	  	username: null
+  	
+    initialize:  ->
+      #alert("iiiiiinitialize"+newUserid+" "+newUsername)
+    	console.log "creation"
+      
+    isValid: ->
+      console.log "inside is valid- id: #{@id} userid: #{@userid} username: #{@username}"
+      value = @id? and @userid? and @username?
 
-  UserModel
+    render: ->
+	  	_.template(userTemplate, {userID: @userid, username: @username})
+
+
+  UserModel
\ No newline at end of file
diff --git a/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee b/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee
index 5c75a166041e015c98d163f632ee68cb6ce5140d..ecbf2255e67bc40f7d30de2114bc96c070922960 100755
--- a/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee
+++ b/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee
@@ -23,6 +23,7 @@ define [
 
     # Container must be a DOM element
     initialize: (@container) ->
+      alert("initializing the paper model")
       # a WhiteboardCursorModel
       @cursor = null
 
@@ -152,8 +153,7 @@ define [
     # @return {Raphael.image} the image object added to the whiteboard
     addImageToPaper: (url, width, height) ->
       @_updateContainerDimensions()
-
-      alert "addImageToPaper url=#{url} \n #{width}x#{height}"      
+    
       if @fitToPage
         # solve for the ratio of what length is going to fit more than the other
         max = Math.max(width / @containerWidth, height / @containerHeight)
@@ -169,7 +169,7 @@ define [
         originalHeight = height
       else
         # fit to width
-        alert "no fit"
+        console.log "ERROR! The slide did not fit"
         # assume it will fit width ways
         sw = width / wr
         sh = height / wr
@@ -279,7 +279,7 @@ define [
           @cursor.undrag()
           @currentLine = @_createTool(tool)
           @cursor.drag(@currentLine.dragOnMove, @currentLine.dragOnStart, @currentLine.dragOnEnd)
-        when "rect"
+        when "rectangle"
           @cursor.undrag()
           @currentRect = @_createTool(tool)
           @cursor.drag(@currentRect.dragOnMove, @currentRect.dragOnStart, @currentRect.dragOnEnd)
@@ -352,6 +352,7 @@ define [
     # Draws an array of shapes to the paper.
     # @param  {array} shapes the array of shapes to draw
     drawListOfShapes: (shapes) ->
+      alert("drawListOfShapes" + shapes.length)
       @currentShapesDefinitions = shapes
       @currentShapes = @raphaelObj.set()
       for shape in shapes
@@ -365,11 +366,6 @@ define [
       # make sure the cursor is still on top
       @cursor.toFront()
 
-    #Changes the currently displayed presentation (if any) with this one
-    #@param {object} containing the "presentation" object -id,name,pages[]
-    sharePresentation: (data) ->
-      globals.events.trigger("connection:all_slides", data.payload)
-
     # Clear all shapes from this paper.
     clearShapes: ->
       if @currentShapes?
@@ -398,6 +394,7 @@ define [
 
     # Make a shape `shape` with the data in `data`.
     makeShape: (shape, data) ->
+      console.log("shape=" + shape + " data=" + JSON.stringify data)
       tool = null
       switch shape
         when "path", "line"
@@ -415,6 +412,7 @@ define [
         when "triangle"
           @currentTriangle = @_createTool(shape)
           toolModel = @currentTriangle
+          toolModel.draw(tool, data)
           tool = @currentTriangle.make(data)
         when "text"
           @currentText = @_createTool(shape)
@@ -423,7 +421,12 @@ define [
         else
           console.log "shape not recognized at makeShape", shape
       if tool?
-        @currentShapes.push(tool)
+        alert("in currentShapes")
+        if @currentShapes? #rewrite TODO
+          @currentShapes.push(tool)
+        else
+          @currentShapes = []
+          @currentShapes.push(tool)
         @currentShapesDefinitions.push(toolModel.getDefinition())
 
     # Update the cursor position on screen
@@ -516,47 +519,12 @@ define [
 
     # Registers listeners for events in the gloval event bus
     _registerEvents: ->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-      globals.events.on "connection:all_slides", (data) =>
-        @removeAllImagesFromPaper()
-        ###
-        urls = data.slides
-        for url in urls
-          @addImageToPaper(url[0], url[1], url[2])
-          #alert "registerEvents url[0]=" + url[0]          
-        ###
-
-        urls = data.presentation.pages
-        for url in urls
-          @addImageToPaper(url.png , 200, 200)
-          #alert "registerEvents url[0]=" + url[0]          
-        globals.events.trigger("whiteboard:paper:all_slides", urls)
-        
-
       globals.events.on "connection:clrPaper", =>
         @clearShapes()
 
       globals.events.on "connection:allShapes", (allShapesEventObject) =>
         # TODO: a hackish trick for making compatible the shapes from redis with the node.js
+        alert("on connection:allShapes:" + JSON.stringify allShapesEventObject)
         shapes = allShapesEventObject.shapes
         for shape in shapes
           properties = JSON.parse(shape.data)
@@ -585,8 +553,9 @@ define [
       globals.events.on "connection:whiteboard_draw_event", (shape, data) =>
         @makeShape shape, data
 
-      globals.events.on "connection:share_presentation_event", (data) =>
-        @sharePresentation data
+      globals.events.on "connection:display_page", (data) =>
+        console.log ("connection:display_page in whiteboard_paper.coffee")
+        @_displayPage data
 
       globals.events.on "connection:whiteboardDrawPen", (startingData) =>
         type = startingData.payload.shape_type
@@ -876,4 +845,11 @@ define [
       else
         globals.presentationServer + url
 
+    #Changes the currently displayed page/slide (if any) with this one
+    #@param {data} message object containing the "presentation" object
+    _displayPage: (data) ->
+      @removeAllImagesFromPaper()
+      page = data.payload.currentPage
+      @addImageToPaper(page.png_uri, 400, 400) #the dimensions should be modified
+
   WhiteboardPaperModel
diff --git a/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee b/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee
index bc27f797daa79761a3ddc1d5d9b63cdfadb35f2c..d66cc99ea68c8f4a4981600712cb595a30c03390 100644
--- a/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee
+++ b/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee
@@ -24,16 +24,16 @@ define [
     # @param  {string} colour    the colour of the object
     # @param  {number} thickness the thickness of the object's line(s)
     make: (startingData) ->
-      console.log "make startingData"+ startingData
-      x = startingData.payload.data.coordinate.first_x
-      y = startingData.payload.data.coordinate.first_y
-      color = startingData.payload.data.line.color
-      thickness = startingData.payload.data.line.weight
+      console.log "make startingData"#+ JSON.stringify startingData
+      x = startingData.payload.shape.shape.points[0]
+      y = startingData.payload.shape.shape.points[1]
+      color = startingData.payload.shape.shape.color
+      thickness = startingData.payload.shape.shape.thickness
 
       @obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1)
       @obj.attr Utils.strokeAndThickness(color, thickness)
       @definition =
-        shape: "rect"
+        shape: "rectangle"
         data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
       @obj
 
@@ -44,11 +44,11 @@ define [
     # @param  {number} y2 the y value of the bottom right corner
     # @param  {boolean} square (draw a square or not)
     update: (startingData) -> 
-      x1 = startingData.payload.data.coordinate.first_x
-      y1 = startingData.payload.data.coordinate.first_y
-      x2 = startingData.payload.data.coordinate.last_x
-      y2 = startingData.payload.data.coordinate.last_y
-      square = startingData.payload.data.square
+      x1 = startingData.payload.shape.shape.points[0]
+      y1 = startingData.payload.shape.shape.points[1]
+      x2 = startingData.payload.shape.shape.points[2]
+      y2 = startingData.payload.shape.shape.points[3]
+      square = startingData.payload.shape.shape.square
       if @obj?
         [x1, x2] = [x2, x1] if x2 < x1
         [x1, x2] = [x2, x1] if x2 < x1
diff --git a/client/bbb-html5-client/public/js/views/session.coffee b/client/bbb-html5-client/public/js/views/session.coffee
index de7c1fd13c05ce6b2a950377ea6969ad0ee02350..10236a0f36e9ed88e23e0dc7675b5bd3cd23bc25 100755
--- a/client/bbb-html5-client/public/js/views/session.coffee
+++ b/client/bbb-html5-client/public/js/views/session.coffee
@@ -8,10 +8,11 @@ define [
   'cs!views/session_navbar_hidden',
   'cs!views/session_chat',
   'cs!views/session_users',
+  'cs!views/SingleUserView',
   'cs!views/session_video'
   'cs!views/session_whiteboard'
 ], ($, _, Backbone, globals, sessionTemplate, SessionNavbarView, SessionNavbarHiddenView,
-    SessionChatView, SessionUsersView, SessionVideoView, SessionWhiteboardView) ->
+    SessionChatView, SessionUsersView, SingleUserView, SessionVideoView, SessionWhiteboardView) ->
 
   SessionView = Backbone.View.extend
     tagName: 'section'
@@ -23,6 +24,7 @@ define [
       @navbarHiddenView = new SessionNavbarHiddenView()
       @navbarHiddenView.$parentEl = @$el
       @chatView = new SessionChatView()
+      @singleUserView = new SingleUserView()
       @usersView = new SessionUsersView()
       @videoView = new SessionVideoView()
       @whiteboardView = new SessionWhiteboardView()
diff --git a/client/bbb-html5-client/public/js/views/session_chat.coffee b/client/bbb-html5-client/public/js/views/session_chat.coffee
index d8a405e49c2762d4409b34bff4e0ceaafb926c9e..3971b42967b8e346aaba214d158c67bd3fb1fa57 100755
--- a/client/bbb-html5-client/public/js/views/session_chat.coffee
+++ b/client/bbb-html5-client/public/js/views/session_chat.coffee
@@ -63,11 +63,8 @@ define [
             #TODO check if public or private message, etc...
         @_scrollToBottom()
 
-      globals.events.on "users:user_leave", (userid) =>
-        @_removeUserFromChatList(userid, username)
-
       globals.events.on "users:user_left", (userid) =>
-        @_removeUserFromChatList(userid) #do we need username or userid is sufficient?
+        @_removeUserFromChatList(userid)
 
       globals.events.on "users:user_join", (userid, username) =>
         console.log "session_chat - user_join for user:#{username}"
@@ -132,9 +129,7 @@ define [
     # @param userid [string] the name of the user
     _addUserToChatList: (userid, username) ->
       # only add the new element if it doesn't exist yet
-      console.log("_addUserToChatList ", userid, " ", username)
-      console.log "chat-user-#{userid}.length =" + $("#chat-user-#{userid}").length
-      unless $("#chat-user-#{userid}").length > 0
+      if $("#chat-user-#{userid}").length is 0
         data =
           userid: userid
           username: username
@@ -143,8 +138,7 @@ define [
 
     # Removes a user from the list of users in the chat
     # @param userid [string] the ID of the user
-    # @param userid [string] the name of the user
-    _removeUserFromChatList: (userid, username) ->
+    _removeUserFromChatList: (userid) ->
       $("#chat-user-#{userid}").remove()
 
     # When a user clicks to start a private chat with a user
@@ -208,7 +202,7 @@ define [
 
     # Adds a default welcome message to the chat
     _addWelcomeMessage: ->
-      msg = "You are now connected to the meeting '#{globals.currentAuth?.get('meetingID')}'"
+      msg = "You are now connected to the meeting '#{globals.meetingName}'"
       @_addChatMessage("System", msg)
 
   SessionChatView
diff --git a/client/bbb-html5-client/public/js/views/session_navbar.coffee b/client/bbb-html5-client/public/js/views/session_navbar.coffee
index 35685ec6f85793f24a198736b0d6b258dc3c0571..267c4eba99c7d089606fb4af2882337bcf0d84e5 100755
--- a/client/bbb-html5-client/public/js/views/session_navbar.coffee
+++ b/client/bbb-html5-client/public/js/views/session_navbar.coffee
@@ -22,6 +22,7 @@ define [
 
     initialize: ->
       @$parentEl = null
+      @usersShown = true # Whether the user's pane is displayed, it is displayed be default 
 
     render: ->
       compiledTemplate = _.template(sessionNavbarTemplate)
@@ -41,9 +42,15 @@ define [
       @$parentEl.toggleClass('chat-on')
       @_setToggleButtonsStatus()
 
-    # Toggle the visibility of the users panel
+    # Toggle the visibility of the user's pane
     _toggleUsers: ->
-      @$parentEl.toggleClass('users-on')
+      if @usersShown # If the user's pane is displayed, hide it and mark flag as hidden
+        $("#users").hide()
+        @usersShown=false
+      else # Display the pane
+        $("#users").show()
+        @usersShown=true
+      #@$parentEl.toggleClass('users-on')
       @_setToggleButtonsStatus()
 
     _toggleVideo: ->
@@ -65,6 +72,7 @@ define [
     _scheduleResize: (id) ->
       attempts = 0
       before = $(id).is(':visible')
+      console.log "isvisible: "+before
       interval = setInterval( ->
         if $(id).is(':visible') != before or attempts > 20
           attempts += 1
@@ -77,6 +85,6 @@ define [
     # Log out of the session
     _logout: ->
       globals.connection.emitLogout()
-      globals.currentAuth = null
+      #globals.currentAuth = null
 
   SessionNavbarView
diff --git a/client/bbb-html5-client/public/js/views/session_users.coffee b/client/bbb-html5-client/public/js/views/session_users.coffee
index 10063381c949bf41638c6d030b21dfb0e4cd16c3..7209b2993d1b3bcbe19cb8499e9a5a74bd741630 100755
--- a/client/bbb-html5-client/public/js/views/session_users.coffee
+++ b/client/bbb-html5-client/public/js/views/session_users.coffee
@@ -13,15 +13,16 @@ define [
   # manage the events in the users.
   SessionUsersView = Backbone.View.extend
     model: new UserCollection()
-
+    
     events:
       "click #switch-presenter": "_switchPresenter"
       "click .user": "_userClicked"
 
     initialize: ->
-      @userListID = "#user-list"
+      userListID = "#user-list"
       @model.start()
-
+      @users = null
+      
       # Bind to the event triggered when the client connects to the server
       if globals.connection.isConnected()
         @_registerEvents()
@@ -30,9 +31,10 @@ define [
           @_registerEvents()
 
     render: ->
+      # this renders to unordered list where users will be appended to
       compiledTemplate = _.template(sessionUsersTemplate)
       @$el.html compiledTemplate
-
+      
     # Registers listeners for events in the event bus.
     # TODO: bind to backbone events in UserCollection such as 'user added', 'user removed'
     _registerEvents: ->
@@ -40,18 +42,22 @@ define [
       globals.events.on "users:user_list_change", (users) =>
         @_removeAllUsers()
         for userBlock in users
+          console.log("on user_list_change; adding user:" + JSON.stringify userBlock)
           @_addUser(userBlock.id, userBlock.name)
 
+      #globals.events.on "receiveUsers", (data) =>
+        #@users = data
+
       globals.events.on "users:load_users", (users) =>
         @_removeAllUsers()
         for userBlock in users
-          @_addUser(userBlock.id, userBlock.name)
+          #@_addUser(userBlock.userid, userBlock.name)
+          globals.events.trigger "user:add_new_user", {id: userBlock.userid, userid: userBlock.userid, username: userBlock.name}
 
       globals.events.on "users:user_join", (userid, username) =>
-        @_addUser(userid, username)
-
-      globals.events.on "users:user_leave", (userid) =>
-        @_removeUserByID(userid)
+        #@_addUser(userid, username)
+        console.log "fffffffffffffff"
+        globals.events.trigger "user:add_new_user", {id: userid, userid: userid, username: username}
 
       globals.events.on "users:user_left", (userid) =>
         @_removeUserByID(userid)
@@ -67,14 +73,6 @@ define [
     _removeUserByID: (userID)->
       @$("#user-"+userID).remove()
 
-    # Add a user to the screen.
-    _addUser: (userID, username) ->
-      data =
-        username: username
-        userID: userID
-      compiledTemplate = _.template(userTemplate, data)
-      @$el.children("ul").append compiledTemplate
-
     # Marks a user as selected when clicked.
     _userClicked: (e) ->
       @$('.user.selected').removeClass('selected')
diff --git a/client/bbb-html5-client/public/templates/user.html b/client/bbb-html5-client/public/templates/user.html
index 796c1bf1c67278d9071ba11750e232337d3a4daf..816a12d461f925e4a7b421a6195a7043e44781e3 100644
--- a/client/bbb-html5-client/public/templates/user.html
+++ b/client/bbb-html5-client/public/templates/user.html
@@ -1,10 +1,10 @@
-<li class="user-wrapper">
-	<div class="row">
-  <div class="user-role col-md-1" id="user-<%= userID %>"><i class="icon fa fa-user"></i></div>
-  <div class="user-name col-md-5"><%= username %></div>
-  <div class="user-video col-md-1"><i class="icon fa fa-video-camera"></i></div>
-  <div class="user-audio col-md-1"><i class="icon fa fa-microphone"></i></div>
-  <div class="user-kick col-md-1"><i class="icon fa fa-sign-out"></i></div>
-	<div class="clearfix"></div>
-	</div>
-</li>
+<li class="user-wrapper" id="user-<%= userID %>">
+   <div class="row">
+      <div class="user-role col-md-1"><i class="icon fa fa-user"></i></div>
+      <div class="user-name col-md-5"><%= username %></div>
+      <div class="user-video col-md-1"><i class="icon fa fa-video-camera"></i></div>
+      <div class="user-audio col-md-1"><i class="icon fa fa-microphone"></i></div>
+      <div class="user-kick col-md-1"><i class="icon fa fa-sign-out"></i></div>
+      <div class="clearfix"></div>
+   </div>
+</li>
\ No newline at end of file
diff --git a/labs/demos/.gitignore b/labs/demos/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3c3629e647f5ddf82548912e337bea9826b434af
--- /dev/null
+++ b/labs/demos/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/labs/demos/Gruntfile.js b/labs/demos/Gruntfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..65642fc5f5f4336f704a4471c5278c76b6da5ee6
--- /dev/null
+++ b/labs/demos/Gruntfile.js
@@ -0,0 +1,32 @@
+/* jshint node: true */
+'use strict';
+
+module.exports = function(grunt) {
+  // configure Grunt
+  grunt.initConfig({
+    // files to lint with the JSHint task
+    jshint: {
+      files: {
+        src: [
+          'Gruntfile.js',
+          'public/**/*.js'
+        ]
+      }
+    },
+
+    coffeelint: {
+      files: {
+        src: ['**/*.coffee','!node_modules/**/*']
+      }
+    }
+  });
+
+  // load the module containing the JSHint task
+  grunt.loadNpmTasks('grunt-contrib-jshint');
+  grunt.loadNpmTasks('grunt-coffeelint');
+
+  // register a default task to run JSHint
+  // (allows `grunt` rather than `grunt jshint`)
+  
+  grunt.registerTask('default', ['jshint', 'coffeelint']);
+};
diff --git a/labs/demos/config.json b/labs/demos/config.json
index 1ffa20f1faf214dc5a0c04c79ae13ed2fcb1ca87..47b222d291bea5f100eb4304f247a2577f9ce2a8 100644
--- a/labs/demos/config.json
+++ b/labs/demos/config.json
@@ -1,7 +1,7 @@
 {
     "settings": {
-        "IP": "http://192.168.0.203",
+        "IP": "http://192.168.0.119",
         "PORT": "4000",
-        "salt": "74a91f30f165423067bf3039722e33e0"
+        "salt": "97d777d100aed03b60b06420a0dd9dd5"
     }
 }
\ No newline at end of file
diff --git a/labs/demos/index.coffee b/labs/demos/index.coffee
index bb46bb63528a50b233ef47c99b94da6403b52371..51ea607f74302869c80cfd01c016d703a7c3b5fa 100644
--- a/labs/demos/index.coffee
+++ b/labs/demos/index.coffee
@@ -21,8 +21,9 @@ app.get("/*", (req, res, next) ->
   file = req.params[0]
   console.log "\t :: Express :: file requested : " + file
 
-  if file is "public/js/app.js" or file is "config.json"
+  if file is "public/js/app.js" or file is "public/stylesheets/login.css" or file is "config.json"
     #Send the requesting client the file.
     res.sendfile __dirname + "/" + file
     next
-)
\ No newline at end of file
+)
+
diff --git a/labs/demos/lib/bbbapi.coffee b/labs/demos/lib/bbbapi.coffee
index 756004a709b9cc75e4cdb63a0ede7857f7909ce6..b579a7a1988ac4da0ef4ab78d49812943f5dac29 100644
--- a/labs/demos/lib/bbbapi.coffee
+++ b/labs/demos/lib/bbbapi.coffee
@@ -2,10 +2,13 @@ request = require 'request'
 sha1    = require 'js-sha1'
 
 urlEncode = (value) ->
-  encodeURIComponent(value).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A") 
+  encodeURIComponent(value)
+  .replace(/%20/g, '+')
+  .replace(/[!'()]/g, escape)
+  .replace(/\*/g, "%2A")
 
 sortKeys = (params) ->
-  keys = [];
+  keys = []
   for own propName of params
     keys.push(propName)
 
@@ -40,7 +43,7 @@ create = (params, bbb, options, callback) ->
   console.log(reqStr)
 
   request(reqStr, (error, response, body) ->
-    callback error, response, body 
+    callback error, response, body
   )
 
 join = (params, bbb, options, callback) ->
@@ -54,7 +57,7 @@ join = (params, bbb, options, callback) ->
   console.log(reqStr)
 
   request(reqStr, (error, response, body) ->
-    callback error, response, body 
+    callback error, response, body
   )
 
 exports.create = create
diff --git a/labs/demos/lib/handlers.coffee b/labs/demos/lib/handlers.coffee
old mode 100644
new mode 100755
index f9142ce7c795733cfa3c6bd0de0ddaea5d7758c0..483fae1ca882a0d67302aaa464fb1b5768db6d63
--- a/labs/demos/lib/handlers.coffee
+++ b/labs/demos/lib/handlers.coffee
@@ -2,6 +2,7 @@ xml2js  = require 'xml2js'
 
 bbbapi  = require './bbbapi'
 testapi = require './testapi'
+configJson = require './../config.json'
 
 index = (request, response) ->
   response.sendfile('./views/index.html')
@@ -12,16 +13,27 @@ login = (req, resp) ->
   serverAndSecret = testapi.serverAndSecret
 
   #use the name from the textbox
-  console.log "\n\nThe Username passed was=" + JSON.stringify(req.body.name) + "\n\n"
-  joinParams.fullName = JSON.stringify req.body.name
-  joinParams.fullName = joinParams.fullName.replace(/['"]/g,'')
+  console.log "\n\nThe Username passed was=" + JSON.stringify(req.body.name) +
+  "The Meetingname passed was=" + JSON.stringify(req.body.meetingName) + "\n\n"
+
+  # grab the username and meeting name passed in. Strip the surrounding quotes
+  joinParams.fullName = (JSON.stringify req.body.name)?.replace(/['"]/g,'')
+  passedMeetingName = (JSON.stringify req.body.meetingName)?.replace(/["]/g,'')
+
+  # use the meeting name from the form to [create if not existing and] join
+  # the meeting with such name
+  joinParams.meetingID = passedMeetingName
+  createParams.name = passedMeetingName
+  createParams.meetingID = passedMeetingName
 
   #calling createapi
-  bbbapi.create(createParams, serverAndSecret, {}, (errorOuter, responseOuter, bodyOuter) ->
+  bbbapi.create(createParams, serverAndSecret, {}, (eo, ro, bodyOuter) ->
     #console.log JSON.stringify(response)
+    console.log "\n\nouterXML=" + ro.body
+    console.log "\neo=" + JSON.stringify eo
     bbbapi.join(joinParams, serverAndSecret, {}, (error, response, body) ->
       if error
-          console.log error
+        console.log error
       else
         xml = '' + response.body
         console.log "\n\nxml=" + xml
@@ -38,8 +50,8 @@ login = (req, resp) ->
             "\nuser_id = " + user_id +
             "\nauth_token = " + auth_token
 
-            url = "http://192.168.0.203:3000/html5.client?meeting_id=" + meeting_id + "&user_id=" +
-                  user_id + "&auth_token=" + auth_token + "&username=" + joinParams.fullName
+            url = "#{configJson.settings.IP}:3000/login?meeting_id=" + meeting_id +
+                  "&user_id=" + user_id + "&auth_token=" + auth_token
 
             json =
             resp.json({
diff --git a/labs/demos/lib/testapi.coffee b/labs/demos/lib/testapi.coffee
old mode 100644
new mode 100755
index 664f9223dfddea36c529dc55d97a8088021f3f89..5038629c35c2d258a5d62b0dfa414ecfc1ba3720
--- a/labs/demos/lib/testapi.coffee
+++ b/labs/demos/lib/testapi.coffee
@@ -8,26 +8,31 @@ sharedSecret = config.settings.salt
 
 console.log "will be creating a meeting on server: " + bbbServer
 
-str = "name=Demo+Meeting&meetingID=Demo+Meeting&voiceBridge=70827&attendeePW=ap&moderatorPW=mp&record=false"
+str = "name=Demo+Meeting" +
+      "&meetingID=Demo+Meeting" +
+      "&voiceBridge=70827" +
+      "&attendeePW=ap" +
+      "&moderatorPW=mp" +
+      "&record=false"
 
 console.log(sha1("create" + str + sharedSecret))
-
+tempName = "Demo Meeting"
 createParams = {}
 createParams.attendeePW = "ap"
 createParams.moderatorPW = "mp"
 createParams.record = false
 createParams.voiceBridge = 70827
-createParams.name = "Demo Meeting"
-createParams.meetingID = "Demo Meeting"
+createParams.name = tempName
+createParams.meetingID = tempName
 
 joinParams = {}
-joinParams.password = "mp"
+joinParams.password = "ap"
 joinParams.fullName = "Richard"
-joinParams.meetingID = "Demo Meeting"
+joinParams.meetingID = tempName
 joinParams.redirect = false
 
 serverAndSecret = {server: bbbServer, secret: sharedSecret}
 
 exports.createParams = createParams
 exports.serverAndSecret = serverAndSecret
-exports.joinParams = joinParams
\ No newline at end of file
+exports.joinParams = joinParams
diff --git a/labs/demos/package.json b/labs/demos/package.json
index db388d7b07012f245266b11a5bf09014acb8da0d..1d4e81efcf41b300f5730643e647ffae74430a26 100644
--- a/labs/demos/package.json
+++ b/labs/demos/package.json
@@ -12,5 +12,12 @@
     "xml2js": "0.4.2",
     "request": "2.34.0",
     "connect": "2.15.0"
+  },
+  "devDependencies": {
+    "coffeelint": "^1.6.0",
+    "grunt": "^0.4.5",
+    "grunt-coffeelint": "0.0.13",
+    "grunt-contrib-coffee": "^0.11.1",
+    "grunt-contrib-jshint": "^0.10.0"
   }
 }
diff --git a/labs/demos/public/js/app.js b/labs/demos/public/js/app.js
old mode 100644
new mode 100755
index c0b221f0b4e86671723111afc92ddb6833d3e7d4..a6e6c5458e12f027f2b1eba05b987fbd83e61827
--- a/labs/demos/public/js/app.js
+++ b/labs/demos/public/js/app.js
@@ -5,7 +5,8 @@ myModule.controller('MainCtrl', function($scope, $http, $location, $window) {
 	$scope.postUsername = function() {
 		var account = {
 			"name": $scope.username,
-			"password": 'oOoOoO'
+			"password": 'oOoOoO',
+                        "meetingName": $scope.meetingName == undefined ? 'Demo Meeting' : $scope.meetingName
 		};
 		jQuery.getJSON("config.json", function (json) {
 			$http.post('/login', account).success(function(res) {
@@ -13,7 +14,7 @@ myModule.controller('MainCtrl', function($scope, $http, $location, $window) {
 				$window.location.href = res.success.url;
 			});
 		});
-	}
+	};
 });
 
 
diff --git a/labs/demos/public/stylesheets/login.css b/labs/demos/public/stylesheets/login.css
new file mode 100644
index 0000000000000000000000000000000000000000..bcf54992b10c557cb08e0030eb18922ea2fa6ae9
--- /dev/null
+++ b/labs/demos/public/stylesheets/login.css
@@ -0,0 +1,91 @@
+body { 
+    margin:0;
+    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+    font-size: 14px;
+    line-height: 1.42857143;
+}
+.login { }
+.login-banner { 
+    background:#f2f2f2;
+    height:325px;
+    border-bottom: 1px solid rgba(0,0,0,.05);
+}
+.login-form { 
+    width:25%;
+    margin:0 auto;
+    background:#fff;
+    border:1px solid #ddd;
+    -webkit-border-radius: 4px;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+    top:45%;
+    padding:40px;
+    display:block;
+    position: relative;
+    -webkit-box-shadow: inset 0 3px 6px rgba(0,0,0,.05);
+    box-shadow: inset 0 3px 6px rgba(0,0,0,.05);
+                }
+.login-form label {
+    display: inline-block;
+    max-width: 100%;
+    margin-bottom: 5px;
+    font-weight: 700;
+}
+.login-form input { 
+    display: block;
+    width: 100%;
+    padding: 12px 18px;
+    font-size: 14px;
+    line-height: 1.42857143;
+    background-image: none;
+    border: 1px solid #c7cdd1;
+    border-radius: 4px;
+    -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+    -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;
+    -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+    transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+    margin-bottom: 20px;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.success {
+    color: #FFF;
+    background: #54A7DB;
+    border:0 !important;
+}
+.success:hover {
+    background: #4695c6;
+    cursor: pointer;
+}
+
+
+/* Large desktop */
+@media (min-width: 1200px) { 
+    .login-form { 
+        width:20%;
+    }
+}
+ 
+/* Portrait tablet to landscape and desktop */
+@media (min-width: 768px) and (max-width: 979px) {
+
+
+
+}
+ 
+/* Landscape phone to portrait tablet */
+@media (max-width: 767px) { 
+    .login-form { 
+        width:45%;
+    }
+
+}
+ 
+/* Landscape phones and down */
+@media (max-width: 480px) { 
+    .login-form { 
+        width:65%;
+    }
+}
diff --git a/labs/demos/views/index.html b/labs/demos/views/index.html
old mode 100644
new mode 100755
index 014eb94071ea6fa01aeaf121e202676a511c1dca..a4345b5b121ad3aca455b9704be37366a68797c1
--- a/labs/demos/views/index.html
+++ b/labs/demos/views/index.html
@@ -1,20 +1,23 @@
 <!doctype html>
 <html ng-app="landingPage">
-	<head>
-		<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.7/angular.min.js"></script>
-		<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
-		<script src="../public/js/app.js"></script>
-	</head>
-	<body>
-		<div>
-			<label>Username:</label>
-			<input type="text" ng-model="username" placeholder="Enter a name here"> 
-		</div>
-		<div ng-controller="MainCtrl">
-			<form ng-submit="postUsername()">
-				<input type="submit" value="Send"> <br /><br />
-			</form>
-		</div>
-	</body>
-</html>
+    <head>
+        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.4/angular.min.js"></script>
+        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
+        <script src="../public/js/app.js"></script>
+        <link rel="stylesheet" href="../public/stylesheets/login.css"></link>
 
+    </head>
+    <body>
+        <div class="login" ng-controller="MainCtrl">
+            <div class="login-banner">
+                <form class="login-form" ng-submit="postUsername()">
+                    <label>Username:</label>
+                    <input type="text" ng-model="username" placeholder="Enter Your Name" required autofocus>
+                    <label>Meeting name:</label>
+                    <input type="text" ng-model="meetingName" placeholder="Demo Meeting">
+                    <input type="submit" value="Send" class="success">
+                </form>
+            </div>
+        </div>
+    </body>
+</html>
diff --git a/labs/meteor-client/.gitignore b/labs/meteor-client/.gitignore
new file mode 100755
index 0000000000000000000000000000000000000000..6101d71bac328c54bc0def6a0907756f6c0f4c44
--- /dev/null
+++ b/labs/meteor-client/.gitignore
@@ -0,0 +1,4 @@
+app/packages
+app/build/
+npm-debug.log
+node_modules/
diff --git a/labs/meteor-client/Gruntfile.js b/labs/meteor-client/Gruntfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..ebe5fdf986fb39290ccc8e3a791d382394f8de2c
--- /dev/null
+++ b/labs/meteor-client/Gruntfile.js
@@ -0,0 +1,36 @@
+/* jshint node: true */
+'use strict';
+
+module.exports = function(grunt) {
+  // configure Grunt
+  grunt.initConfig({
+    // files to lint with the JSHint task
+    jshint: {
+      files: {
+        src: [
+          'Gruntfile.js'
+        ]
+      }
+    },
+
+    coffeelint: {
+      files: {
+        src: [
+          '**/*.coffee',
+          '!node_modules/**/*',
+          '!app/.meteor/**/*',
+          '!app/packages/**/*'
+        ]
+      }
+    }
+  });
+
+  // load the module containing the JSHint task
+  grunt.loadNpmTasks('grunt-contrib-jshint');
+  grunt.loadNpmTasks('grunt-coffeelint');
+
+  // register a default task to run JSHint
+  // (allows `grunt` rather than `grunt jshint`)
+  
+  grunt.registerTask('default', ['jshint', 'coffeelint']);
+};
diff --git a/labs/meteor-client/app/.meteor/.finished-upgraders b/labs/meteor-client/app/.meteor/.finished-upgraders
new file mode 100644
index 0000000000000000000000000000000000000000..68df3d8d0d0954319298b1c2b419dceebf4e4179
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/.finished-upgraders
@@ -0,0 +1,7 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
diff --git a/labs/meteor-client/app/.meteor/.gitignore b/labs/meteor-client/app/.meteor/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..40830374235df1c19661a2901b7ca73cc9499f3d
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/labs/meteor-client/app/.meteor/.id b/labs/meteor-client/app/.meteor/.id
new file mode 100644
index 0000000000000000000000000000000000000000..350c3adf10c0562a65ea842e9e50fc3919d539ac
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+#   - ensuring you don't accidentally deploy one app on top of another
+#   - providing package authors with aggregated statistics
+
+jrnkwdjvicqgy6gtl8
diff --git a/labs/meteor-client/app/.meteor/cordova-plugins b/labs/meteor-client/app/.meteor/cordova-plugins
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/cordova-plugins
@@ -0,0 +1 @@
+
diff --git a/labs/meteor-client/app/.meteor/packages b/labs/meteor-client/app/.meteor/packages
new file mode 100644
index 0000000000000000000000000000000000000000..c63074f728fc4fd918e41e7799f833a44737669a
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/packages
@@ -0,0 +1,24 @@
+# Meteor packages used by this project, one per line.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+standard-app-packages
+coffeescript
+mrt:redis@0.1.3
+arunoda:npm@0.2.6
+underscore
+amplify
+iron:router
+blaze
+less
+sanjo:jasmine
+francocatena:status
+mizzao:jquery-ui
+mrt:external-file-loader@0.1.4
+brentjanderson:winston-client@0.0.4
+mizzao:bootstrap-3
+duongthienduc:meteor-winston
+mizzao:timesync
+agnito:raphael
+
diff --git a/labs/meteor-client/app/.meteor/platforms b/labs/meteor-client/app/.meteor/platforms
new file mode 100644
index 0000000000000000000000000000000000000000..efeba1b50c759324cbdaafd7da6c31b030bf9600
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/labs/meteor-client/app/.meteor/release b/labs/meteor-client/app/.meteor/release
new file mode 100644
index 0000000000000000000000000000000000000000..fdc65835058566d8a94f89944c26eea03db1c4a6
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/release
@@ -0,0 +1 @@
+METEOR@1.0.2.1
diff --git a/labs/meteor-client/app/.meteor/versions b/labs/meteor-client/app/.meteor/versions
new file mode 100644
index 0000000000000000000000000000000000000000..4485a21952db63c574b00e747571efa2405573be
--- /dev/null
+++ b/labs/meteor-client/app/.meteor/versions
@@ -0,0 +1,84 @@
+agnito:raphael@0.1.0
+alanning:package-stubber@0.0.9
+amplify@1.0.0
+application-configuration@1.0.4
+arunoda:npm@0.2.6
+autoupdate@1.1.4
+base64@1.0.2
+binary-heap@1.0.2
+blaze@2.0.4
+blaze-tools@1.0.2
+boilerplate-generator@1.0.2
+brentjanderson:winston-client@0.2.0
+callback-hook@1.0.2
+check@1.0.3
+coffeescript@1.0.5
+ddp@1.0.13
+deps@1.0.6
+duongthienduc:meteor-winston@1.0.0
+ejson@1.0.5
+fastclick@1.0.2
+follower-livedata@1.0.3
+francocatena:status@1.0.3
+geojson-utils@1.0.2
+html-tools@1.0.3
+htmljs@1.0.3
+http@1.0.9
+id-map@1.0.2
+infinitedg:winston@0.7.3
+iron:controller@1.0.6
+iron:core@1.0.6
+iron:dynamic-template@1.0.6
+iron:layout@1.0.6
+iron:location@1.0.6
+iron:middleware-stack@1.0.6
+iron:router@1.0.6
+iron:url@1.0.6
+jquery@1.0.2
+json@1.0.2
+launch-screen@1.0.1
+less@1.0.12
+livedata@1.0.12
+logging@1.0.6
+meteor@1.1.4
+meteor-platform@1.2.1
+minifiers@1.1.3
+minimongo@1.0.6
+mizzao:bootstrap-3@3.3.1_1
+mizzao:build-fetcher@0.2.0
+mizzao:jquery-ui@1.11.2
+mizzao:timesync@0.2.2
+mobile-status-bar@1.0.2
+mongo@1.0.11
+mrt:external-file-loader@0.1.4
+mrt:redis@0.1.3
+observe-sequence@1.0.4
+ordered-dict@1.0.2
+practicalmeteor:chai@1.9.2_3
+practicalmeteor:loglevel@1.1.0_3
+random@1.0.2
+reactive-dict@1.0.5
+reactive-var@1.0.4
+reload@1.1.2
+retry@1.0.2
+routepolicy@1.0.3
+sanjo:jasmine@0.9.3
+sanjo:karma@1.1.3
+session@1.0.5
+spacebars@1.0.4
+spacebars-compiler@1.0.4
+standard-app-packages@1.0.4
+tap:http-methods@0.0.23
+tap:i18n@1.0.7
+templating@1.0.10
+tracker@1.0.4
+ui@1.0.5
+underscore@1.0.2
+url@1.0.3
+velocity:core@0.4.5
+velocity:meteor-stubs@1.0.0_2
+velocity:node-soft-mirror@0.2.8
+velocity:shim@0.1.0
+velocity:test-proxy@0.0.4
+webapp@1.1.5
+webapp-hashing@1.0.2
diff --git a/labs/meteor-client/app/client/globals.coffee b/labs/meteor-client/app/client/globals.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..123487c77c92b611133d8a4448831152065bff2e
--- /dev/null
+++ b/labs/meteor-client/app/client/globals.coffee
@@ -0,0 +1,323 @@
+@getBuildInformation = ->
+  appName = Meteor.config?.appName or "UNKNOWN NAME"
+  copyrightYear = Meteor.config?.copyrightYear or "UNKNOWN DATE"
+  dateOfBuild = Meteor.config?.dateOfBuild or "UNKNOWN DATE"
+  defaultWelcomeMessage = Meteor.config?.defaultWelcomeMessage or "UNKNOWN"
+  defaultWelcomeMessageFooter = Meteor.config?.defaultWelcomeMessageFooter or "UNKNOWN"
+  link = "<a href='http://bigbluebutton.org/' target='_blank'>http://bigbluebutton.org</a>"
+  bbbServerVersion = Meteor.config?.bbbServerVersion or "UNKNOWN VERSION"
+
+  {
+    'appName': appName
+    'copyrightYear': copyrightYear
+    'dateOfBuild': dateOfBuild
+    'defaultWelcomeMessage': defaultWelcomeMessage
+    'defaultWelcomeMessageFooter': defaultWelcomeMessageFooter
+    'link': link
+    'bbbServerVersion': bbbServerVersion
+  }
+
+# Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
+@colourToHex = (value) ->
+	hex = parseInt(value).toString(16)
+	hex = "0" + hex while hex.length < 6
+	"##{hex}"
+
+# color can be a number (a hex converted to int) or a string (e.g. "#ffff00")
+@formatColor = (color) ->
+  color ?= "0" # default value
+  if !color.toString().match(/\#.*/)
+    color = colourToHex(color)
+  color
+
+# thickness can be a number (e.g. "2") or a string (e.g. "2px")
+@formatThickness = (thickness) ->
+  thickness ?= "1" # default value
+  if !thickness.toString().match(/.*px$/)
+    "#" + thickness + "px" # leading "#" - to be compatible with Firefox
+  thickness
+
+@getCurrentSlideDoc = -> # returns only one document
+  currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
+  presentationId = currentPresentation?.presentation?.id
+  currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
+
+# retrieve account for selected user
+@getCurrentUserFromSession = ->
+  Meteor.Users.findOne(userId: getInSession("userId"))
+
+@getInSession = (k) -> SessionAmplify.get k
+
+@getMeetingName = ->
+  return Meteor.Meetings.findOne()?.meetingName or "your meeting"
+
+@getTime = -> # returns epoch in ms
+  (new Date).valueOf()
+
+@getTimeOfJoining = ->
+  Meteor.Users.findOne(userId: getInSession "userId")?.user?.time_of_joining
+
+@getPresentationFilename = ->
+  currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
+  currentPresentation?.presentation?.name
+
+Handlebars.registerHelper "colourToHex", (value) =>
+  @window.colourToHex(value)
+
+Handlebars.registerHelper 'equals', (a, b) -> # equals operator was dropped in Meteor's migration from Handlebars to Spacebars
+  a is b
+
+Handlebars.registerHelper "getCurrentMeeting", ->
+  Meteor.Meetings.findOne()
+
+Handlebars.registerHelper "getCurrentSlide", ->
+  currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
+  presentationId = currentPresentation?.presentation?.id
+  Meteor.Slides.find({"presentationId": presentationId, "slide.current": true})
+
+# retrieve account for selected user
+Handlebars.registerHelper "getCurrentUser", =>
+  @window.getCurrentUserFromSession()
+
+# Allow access through all templates
+Handlebars.registerHelper "getInSession", (k) -> SessionAmplify.get k
+
+Handlebars.registerHelper "getMeetingName", ->
+  return Meteor.Meetings.findOne()?.meetingName or "BigBlueButton"
+
+Handlebars.registerHelper "getShapesForSlide", ->
+  currentSlide = getCurrentSlideDoc()
+
+  # try to reuse the lines above
+  Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id})
+
+# retrieves all users in the meeting
+Handlebars.registerHelper "getUsersInMeeting", ->
+  # Users with raised hand last go first, then sorted by name
+  Meteor.Users.find({}, {sort: {'user.raise_hand': -1, 'user._sort_name': 1} })
+
+Handlebars.registerHelper "getWhiteboardTitle", ->
+  "Whiteboard: " + (getPresentationFilename() or "Loading...")
+
+Handlebars.registerHelper "isCurrentUser", (userId) ->
+  userId is BBB.getCurrentUser()?.userId
+
+Handlebars.registerHelper "isCurrentUserMuted", ->
+  BBB.amIMuted()
+
+Handlebars.registerHelper "isCurrentUserRaisingHand", ->
+  user = BBB.getCurrentUser()
+  user?.user?.raise_hand
+
+Handlebars.registerHelper "isCurrentUserSharingAudio", ->
+  BBB.amISharingAudio()
+
+Handlebars.registerHelper "isCurrentUserSharingVideo", ->
+  BBB.amISharingVideo()
+
+Handlebars.registerHelper "isCurrentUserTalking", ->
+  BBB.amITalking()
+
+Handlebars.registerHelper "isDisconnected", ->
+  return !Meteor.status().connected
+
+Handlebars.registerHelper "isUserListenOnly", (userId) ->
+  user = Meteor.Users.findOne({userId:userId})
+  return user?.user?.listenOnly
+
+Handlebars.registerHelper "isUserMuted", (userId) ->
+  BBB.isUserMuted(userId)
+
+Handlebars.registerHelper "isUserSharingAudio", (userId) ->
+  BBB.isUserSharingAudio(userId)
+
+Handlebars.registerHelper "isUserSharingVideo", (userId) ->
+  BBB.isUserSharingWebcam(userId)
+
+Handlebars.registerHelper "isUserTalking", (userId) ->
+  BBB.isUserTalking(userId)
+
+Handlebars.registerHelper "meetingIsRecording", ->
+  Meteor.Meetings.findOne()?.recorded # Should only ever have one meeting, so we dont need any filter and can trust result #1
+
+Handlebars.registerHelper "messageFontSize", ->
+  style: "font-size: #{getInSession("messageFontSize")}px;"
+
+Handlebars.registerHelper "pointerLocation", ->
+  currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
+  presentationId = currentPresentation?.presentation?.id
+  currentSlideDoc = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
+  pointer = currentPresentation?.pointer
+  pointer.x = (- currentSlideDoc.slide.x_offset * 2 + currentSlideDoc.slide.width_ratio * pointer.x) / 100
+  pointer.y = (- currentSlideDoc.slide.y_offset * 2 + currentSlideDoc.slide.height_ratio * pointer.y) / 100
+  pointer
+
+Handlebars.registerHelper "safeName", (str) ->
+  safeString(str)
+
+Handlebars.registerHelper "visibility", (section) ->
+  if getInSession "display_#{section}"
+    style: 'display:block;'
+  else
+    style: 'display:none;'
+
+# transform plain text links into HTML tags compatible with Flash client
+@linkify = (str) ->
+  www = /(^|[^\/])(www\.[\S]+($|\b))/img
+  http = /\b(https?:\/\/[0-9a-z+|.,:;\/&?_~%#=@!-]*[0-9a-z+|\/&_~%#=@-])/img
+  str = str.replace http, "<a href='event:$1'><u>$1</u></a>"
+  str = str.replace www, "$1<a href='event:http://$2'><u>$2</u></a>"
+
+# check the chat history of the user and add tabs for the private chats
+@populateChatTabs = (msg) ->
+  myUserId = getInSession "userId"
+  users = Meteor.Users.find().fetch()
+
+  # assuming that I only have access only to private messages where I am the sender or the recipient
+  myPrivateChats = Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT'}).fetch()
+
+  uniqueArray = []
+  for chat in myPrivateChats
+    if chat.message.to_userid is myUserId
+      uniqueArray.push({userId: chat.message.from_userid, username: chat.message.from_username})
+    if chat.message.from_userid is myUserId
+      uniqueArray.push({userId: chat.message.to_userid, username: chat.message.to_username})
+
+  #keep unique entries only
+  uniqueArray = uniqueArray.filter((itm, i, a) ->
+      i is a.indexOf(itm)
+    )
+
+  if msg.message.to_userid is myUserId
+    new_msg_userid = msg.message.from_userid
+  if msg.message.from_userid is myUserId
+    new_msg_userid = msg.message.to_userid
+
+  #insert the unique entries in the collection
+  for u in uniqueArray
+    tabs = getInSession('chatTabs')
+    if tabs.filter((tab) -> tab.userId == u.userId).length is 0 and u.userId is new_msg_userid
+      tabs.push {userId: u.userId, name: u.username, gotMail: false, class: 'privateChatTab'}
+      setInSession 'chatTabs', tabs
+
+@setInSession = (k, v) -> SessionAmplify.set k, v
+
+@safeString = (str) ->
+  if typeof str is 'string'
+    str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+
+@toggleCam = (event) ->
+  # Meteor.Users.update {_id: context._id} , {$set:{"user.sharingVideo": !context.sharingVideo}}
+  # Meteor.call('userToggleCam', context._id, !context.sharingVideo)
+
+@toggleChatbar = ->
+  setInSession "display_chatbar", !getInSession "display_chatbar"
+  setTimeout(redrawWhiteboard, 0)
+
+@toggleMic = (event) ->
+  u = Meteor.Users.findOne({userId:getInSession("userId")})
+  if u?
+    Meteor.call('muteUser', getInSession("meetingId"), u.userId, getInSession("userId"), getInSession("authToken"), not u.user.voiceUser.muted)
+
+@toggleNavbar = ->
+  setInSession "display_navbar", !getInSession "display_navbar"
+
+# toggle state of session variable
+@toggleUsersList = ->
+  setInSession "display_usersList", !getInSession "display_usersList"
+  setTimeout(redrawWhiteboard, 0)
+
+@toggleVoiceCall = (event) ->
+  if BBB.amISharingAudio()
+    hangupCallback = ->
+      console.log "left voice conference"
+    BBB.leaveVoiceConference hangupCallback #TODO should we apply role permissions to this action?
+  else
+    # create voice call params
+    joinCallback = (message) ->
+      console.log "started webrtc_call"
+    BBB.joinVoiceConference joinCallback # make the call #TODO should we apply role permissions to this action?
+  return false
+
+@toggleWhiteBoard = ->
+  setInSession "display_whiteboard", !getInSession "display_whiteboard"
+  setTimeout(redrawWhiteboard, 0)
+
+@toggleSlidingMenu = ->
+  if $('#sliding-menu').hasClass('sliding-menu-opened')
+    setInSession 'display_slidingMenu', false
+    $('#sliding-menu').removeClass('sliding-menu-opened')
+    $('#darkened-screen').css('display', 'none')
+    $(document).unbind('scroll')
+  else
+    setInSession 'display_slidingMenu', true
+    $('#sliding-menu').addClass('sliding-menu-opened')
+    $('#darkened-screen').css('display', 'block')
+    $(document).bind 'scroll', () ->
+      window.scrollTo(0, 0)
+
+# Starts the entire logout procedure.
+# meeting: the meeting the user is in
+# the user's userId
+@userLogout = (meeting, user) ->
+  Meteor.call("userLogout", meeting, user, getInSession("authToken"))
+  console.log "logging out #{Meteor.config.app.logOutUrl}"
+  document.location = Meteor.config.app.logOutUrl # navigate to logout
+
+# Clear the local user session
+@clearSessionVar = (callback) ->
+  delete SessionAmplify.keys['authToken']
+  delete SessionAmplify.keys['bbbServerVersion']
+  delete SessionAmplify.keys['chatTabs']
+  delete SessionAmplify.keys['dateOfBuild']
+  delete SessionAmplify.keys['display_chatPane']
+  delete SessionAmplify.keys['display_chatbar']
+  delete SessionAmplify.keys['display_navbar']
+  delete SessionAmplify.keys['display_usersList']
+  delete SessionAmplify.keys['display_whiteboard']
+  delete SessionAmplify.keys['inChatWith']
+  delete SessionAmplify.keys['meetingId']
+  delete SessionAmplify.keys['messageFontSize']
+  delete SessionAmplify.keys['tabsRenderedTime']
+  delete SessionAmplify.keys['userId']
+  delete SessionAmplify.keys['userName']
+  callback()
+
+# assign the default values for the Session vars
+@setDefaultSettings = ->
+  console.log "in setDefaultSettings"
+  setInSession "display_usersList", true
+  setInSession "display_navbar", true
+  setInSession "display_chatbar", true
+  setInSession "display_whiteboard", true
+  setInSession "display_chatPane", true
+  setInSession "inChatWith", 'PUBLIC_CHAT'
+  setInSession "messageFontSize", 12
+  setInSession 'display_slidingMenu', false
+
+
+@onLoadComplete = ->
+  setDefaultSettings()
+
+  Meteor.Users.find().observe({
+  removed: (oldDocument) ->
+    if oldDocument.userId is getInSession 'userId'
+      document.location = Meteor.config.app.logOutUrl
+  })
+
+# applies zooming to the stroke thickness
+@zoomStroke = (thickness) ->
+  currentSlide = @getCurrentSlideDoc()
+  ratio = (currentSlide?.slide.width_ratio + currentSlide?.slide.height_ratio) / 2
+  thickness * 100 / ratio
+
+# TODO TEMPORARY!!
+# must not have this in production
+@whoami = ->
+  console.log JSON.stringify
+    username: getInSession "userName"
+    userid: getInSession "userId"
+    authToken: getInSession "authToken"
+
+@listSessionVars = ->
+  console.log SessionAmplify.keys
diff --git a/labs/meteor-client/app/client/lib/.gitignore b/labs/meteor-client/app/client/lib/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..553695ee28337e39a499abf935f5775ec60d677e
--- /dev/null
+++ b/labs/meteor-client/app/client/lib/.gitignore
@@ -0,0 +1,2 @@
+custom.bootstrap.mixins.import.less
+custom.bootstrap.less
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee b/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..18747f6a8267d0616e203b78aaf545b8c7de3928
--- /dev/null
+++ b/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee
@@ -0,0 +1,355 @@
+###
+This file contains the BigBlueButton client APIs that will allow 3rd-party applications
+to embed the HTML5 client and interact with it through Javascript.
+
+HOW TO USE:
+Some APIs allow synchronous and asynchronous calls. When using asynchronous, the 3rd-party
+JS should register as listener for events listed at the bottom of this file. For synchronous,
+3rd-party JS should pass in a callback function when calling the API.
+
+For an example on how to use these APIs, see:
+
+https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/lib/3rd-party.js
+https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/3rd-party.html
+###
+
+@BBB = (->
+
+  BBB = {}
+
+  returnOrCallback = (res, callback) ->
+    if callback? and typeof callback is "function"
+      callback res
+    else
+      res
+
+  ###
+    Queryies the user object via it's id
+  ###
+  BBB.getUser = (userId) ->
+    Meteor.Users.findOne({userId: userId})
+
+  BBB.getCurrentUser = () ->
+    BBB.getUser(getInSession("userId"))
+
+  ###
+  Query if the current user is sharing webcam.
+
+  Param:
+  callback - function to return the result
+
+  If you want to instead receive an event with the result, register a listener
+  for AM_I_SHARING_CAM_RESP (see below).
+  ###
+  BBB.amISharingWebcam = (callback) ->
+    BBB.isUserSharingWebcam BBB.getCurrentUser()?.userId
+
+  ###
+
+  Query if another user is sharing her camera.
+
+  Param:
+  userID : the id of the user that may be sharing the camera
+  callback: function if you want to be informed synchronously. Don't pass a function
+  if you want to be informed through an event. You have to register for
+  IS_USER_PUBLISHING_CAM_RESP (see below).
+  ###
+  BBB.isUserSharingWebcam = (userId, callback) ->
+    BBB.getUser(userId)?.user?.webcam_stream?.length isnt 0
+
+  BBB.amITalking = (callback) ->
+    BBB.isUserTalking BBB.getCurrentUser()?.userId
+
+  BBB.isUserTalking = (userId, callback) ->
+    BBB.getUser(userId)?.user?.voiceUser?.talking
+
+  BBB.amISharingAudio = (callback) ->
+    BBB.isUserSharingAudio BBB.getCurrentUser()?.userId
+
+  BBB.isUserSharingAudio = (userId) ->
+    BBB.getUser(userId)?.user?.voiceUser?.joined
+
+  ###
+  Raise user's hand.
+
+  Param:
+  raiseHand - [true/false]
+  ###
+  BBB.raiseHand = (raiseHand) ->
+
+  ###
+  Issue a switch presenter command.
+
+  Param:
+  newPresenterUserID - the user id of the new presenter
+
+  3rd-party JS must listen for SWITCHED_PRESENTER (see below) to get notified
+  of switch presenter events.
+  ###
+  BBB.switchPresenter = (newPresenterUserID) ->
+
+  ###
+  Query if current user is presenter.
+
+  Params:
+  callback - function if you want a callback as response. Otherwise, you need to listen
+  for AM_I_PRESENTER_RESP (see below).
+  ###
+  BBB.amIPresenter = (callback) ->
+    returnOrCallback false, callback
+
+  ###
+  Eject a user.
+
+  Params:
+  userID - userID of the user you want to eject.
+  ###
+  BBB.ejectUser = (userID) ->
+
+  ###
+  Query who is presenter.
+
+  Params:
+  callback - function that gets executed for the response.
+  ###
+  BBB.getPresenterUserID = (callback) ->
+
+  ###
+  Query the current user's role.
+  Params:
+  callback - function if you want a callback as response. Otherwise, you need to listen
+  for GET_MY_ROLE_RESP (see below).
+  ###
+  BBB.getMyRole = (callback) ->
+    returnOrCallback "VIEWER", callback
+
+  ###
+  Query the current user's id.
+
+  Params:
+  callback - function that gets executed for the response.
+  ###
+  BBB.getMyUserID = (callback) ->
+    returnOrCallback getInSession("userId"), callback
+
+
+  BBB.getMyDBID = (callback) ->
+    returnOrCallback Meteor.Users.findOne({userId:getInSession("userId")})?._id, callback
+
+
+  BBB.getMyUserName = (callback) ->
+    name = getInSession "userName" # check if we actually have one in the session
+
+    if name?
+      name # great return it, no database query
+    else # we need it from the database
+      user = BBB.getCurrentUser()
+
+      if user?
+        name = BBB.getUserName(user.userId)
+        setInSession "userName", name # store in session for fast access next time
+        name
+
+  BBB.getMyVoiceBridge = (callback) ->
+    res = Meteor.Meetings.findOne({}).voiceConf
+    returnOrCallback res, callback
+
+  BBB.getUserName = (userId, callback) ->
+    returnOrCallback BBB.getUser(userId)?.user?.name, callback
+
+  ###
+  Query the current user's role.
+  Params:
+  callback - function if you want a callback as response. Otherwise, you need to listen
+  for GET_MY_ROLE_RESP (see below).
+  ###
+  BBB.getMyUserInfo = (callback) ->
+    result =
+      myUserID: BBB.getMyUserID()
+      myUsername: BBB.getMyUserName()
+      myAvatarURL: null
+      myRole: BBB.getMyRole()
+      amIPresenter: BBB.amIPresenter()
+      voiceBridge: BBB.getMyVoiceBridge()
+      dialNumber: null
+
+    returnOrCallback(result, callback)
+
+  ###
+  Query the meeting id.
+
+  Params:
+  callback - function that gets executed for the response.
+  ###
+  BBB.getMeetingID = (callback) ->
+
+  BBB.getInternalMeetingID = (callback) ->
+
+  ###
+  Join the voice conference.
+  ###
+  BBB.joinVoiceConference = (callback) ->
+    callIntoConference(BBB.getMyVoiceBridge(), callback)
+
+  ###
+  Leave the voice conference.
+  ###
+  BBB.leaveVoiceConference = (callback) ->
+    webrtc_hangup callback # sign out of call
+
+  ###
+  Share user's webcam.
+
+  Params:
+  publishInClient : (DO NOT USE - Unimplemented)
+  ###
+  BBB.shareVideoCamera = (publishInClient) ->
+
+  ###
+  Stop share user's webcam.
+  ###
+  BBB.stopSharingCamera = ->
+
+  ###
+    Indicates if a user is muted
+  ###
+  BBB.isUserMuted = (id) ->
+    BBB.getUser(id)?.user?.voiceUser?.muted
+
+  ###
+    Indicates if the current user is muted
+  ###
+  BBB.amIMuted = ->
+    BBB.isUserMuted(BBB.getCurrentUser().userId)
+
+  ###
+  Mute the current user.
+  ###
+  BBB.muteMe = ->
+
+  ###
+  Unmute the current user.
+  ###
+  BBB.unmuteMe = ->
+
+  ###
+  Mute all the users.
+  ###
+  BBB.muteAll = ->
+
+  ###
+  Unmute all the users.
+  ###
+  BBB.unmuteAll = ->
+
+  ###
+  Switch to a new layout.
+
+  Param:
+  newLayout : name of the layout as defined in layout.xml (found in /var/www/bigbluebutton/client/conf/layout.xml)
+  ###
+  BBB.switchLayout = (newLayout) ->
+
+  ###
+  Lock the layout.
+
+  Locking the layout means that users will have the same layout with the moderator that issued the lock command.
+  Other users won't be able to move or resize the different windows.
+  ###
+  BBB.lockLayout = (lock) ->
+
+  ###
+  Request to send a public chat
+  fromUserID - the external user id for the sender
+  fontColor  - the color of the font to display the message
+  localeLang - the 2-char locale code (e.g. en) for the sender
+  message    - the message to send
+  ###
+  BBB.sendPublicChatMessage = (fontColor, localeLang, message) ->
+
+  ###
+  Request to send a private chat
+  fromUserID - the external user id for the sender
+  fontColor  - the color of the font to display the message
+  localeLang - the 2-char locale code (e.g. en) for the sender
+  message    - the message to send
+  toUserID   - the external user id of the receiver
+  ###
+  BBB.sendPrivateChatMessage = (fontColor, localeLang, message, toUserID) ->
+
+  ###
+  Request to display a presentation.
+  presentationID - the presentation to display
+  ###
+  BBB.displayPresentation = (presentationID) ->
+
+  ###
+  Query the list of uploaded presentations.
+  ###
+  BBB.queryListOfPresentations = ->
+
+  ###
+  Request to delete a presentation.
+  presentationID - the presentation to delete
+  ###
+  BBB.deletePresentation = (presentationID) ->
+
+  BBB.webRTCConferenceCallStarted = ->
+
+  BBB.webRTCConferenceCallConnecting = ->
+
+  BBB.webRTCConferenceCallEnded = ->
+
+  BBB.webRTCConferenceCallFailed = (errorcode) ->
+
+  BBB.webRTCConferenceCallWaitingForICE = ->
+
+  BBB.webRTCCallProgressCallback = (progress) ->
+
+  BBB.webRTCEchoTestStarted = ->
+
+  BBB.webRTCEchoTestConnecting = ->
+
+  BBB.webRTCEchoTestFailed = (reason) ->
+
+  BBB.webRTCEchoTestWaitingForICE = ->
+
+  BBB.webRTCEchoTestEnded = ->
+
+  BBB.webRTCMediaRequest = ->
+
+  BBB.webRTCMediaSuccess = ->
+
+  BBB.webRTCMediaFail = ->
+
+  BBB.webRTCWebcamRequest = ->
+
+  BBB.webRTCWebcamRequestSuccess = ->
+
+  BBB.webRTCWebcamRequestFail = (reason) ->
+
+  # Third-party JS apps should use this to query if the BBB SWF file is ready to handle calls.
+
+  # ***********************************************************************************
+  # *       Broadcasting of events to 3rd-party apps.
+  # ************************************************************************************
+
+  ###
+  Stores the 3rd-party app event listeners **
+  ###
+  listeners = {}
+
+  ###
+  3rd-party apps should user this method to register to listen for events.
+  ###
+  BBB.listen = (eventName, handler) ->
+
+  ###
+  3rd-party app should use this method to unregister listener for a given event.
+  ###
+  BBB.unlisten = (eventName, handler) ->
+
+  BBB.init = (callback) ->
+
+  BBB
+)()
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/lib/custom.bootstrap.import.less b/labs/meteor-client/app/client/lib/custom.bootstrap.import.less
new file mode 100644
index 0000000000000000000000000000000000000000..0539c790a0c5bba6d12fb826726aaf9f38677ac6
--- /dev/null
+++ b/labs/meteor-client/app/client/lib/custom.bootstrap.import.less
@@ -0,0 +1,855 @@
+// This File is for you to modify!
+// It won't be overwritten as long as it exists.
+// You may include this file into your less files to benefit from
+// mixins and variables that bootstrap provides.
+
+@import "custom.bootstrap.mixins.import.less";
+
+
+// @import "bootstrap/less/variables.less"
+//
+// Variables
+// --------------------------------------------------
+
+
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+@gray-darker:            lighten(#000, 13.5%); // #222
+@gray-dark:              lighten(#000, 20%);   // #333
+@gray:                   lighten(#000, 33.5%); // #555
+@gray-light:             lighten(#000, 46.7%); // #777
+@gray-lighter:           lighten(#000, 93.5%); // #eee
+
+@brand-primary:         #428bca;
+@brand-success:         #5cb85c;
+@brand-info:            #5bc0de;
+@brand-warning:         #f0ad4e;
+@brand-danger:          #d9534f;
+
+
+//== Scaffolding
+//
+//## Settings for some of the most global styles.
+
+//** Background color for `<body>`.
+@body-bg:               #fff;
+//** Global text color on `<body>`.
+@text-color:            @gray-dark;
+
+//** Global textual link color.
+@link-color:            @brand-primary;
+//** Link hover color set via `darken()` function.
+@link-hover-color:      darken(@link-color, 15%);
+
+
+//== Typography
+//
+//## Font, line-height, and color for body text, headings, and more.
+
+@font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif;
+@font-family-serif:       Georgia, "Times New Roman", Times, serif;
+//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          14px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px
+@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px
+@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px
+@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px
+@font-size-h5:            @font-size-base;
+@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.428571429; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the `<body>`.
+@headings-font-family:    inherit;
+@headings-font-weight:    500;
+@headings-line-height:    1.1;
+@headings-color:          inherit;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path:          "../fonts/";
+//** File name for all font files.
+@icon-font-name:          "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     6px;
+@padding-base-horizontal:   12px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.33;
+@line-height-small:         1.5;
+
+@border-radius-base:        4px;
+@border-radius-large:       6px;
+@border-radius-small:       3px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for `<th>`s and `<td>`s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                normal;
+
+@btn-default-color:              #333;
+@btn-default-bg:                 #fff;
+@btn-default-border:             #ccc;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             darken(@btn-primary-bg, 5%);
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             darken(@btn-success-bg, 5%);
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                darken(@btn-info-bg, 5%);
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             darken(@btn-warning-bg, 5%);
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              darken(@btn-danger-bg, 5%);
+
+@btn-link-disabled-color:        @gray-light;
+
+
+//== Forms
+//
+//##
+
+//** `<input>` background color
+@input-bg:                       #fff;
+//** `<input disabled>` background color
+@input-bg-disabled:              @gray-lighter;
+
+//** Text color for `<input>`s
+@input-color:                    @gray;
+//** `<input>` border color
+@input-border:                   #ccc;
+//** `<input>` border radius
+@input-border-radius:            @border-radius-base;
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        @gray-light;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           @gray-lighter;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @gray-dark;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         #f5f5f5;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color:           #000;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs:                  480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min:              @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             ((720px + @grid-gutter-width));
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            ((940px + @grid-gutter-width));
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      ((1140px + @grid-gutter-width));
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    50px;
+@navbar-margin-bottom:             @line-height-computed;
+@navbar-border-radius:             @border-radius-base;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             #777;
+@navbar-default-bg:                #f8f8f8;
+@navbar-default-border:            darken(@navbar-default-bg, 6.5%);
+
+// Navbar links
+@navbar-default-link-color:                #777;
+@navbar-default-link-hover-color:          #333;
+@navbar-default-link-hover-bg:             transparent;
+@navbar-default-link-active-color:         #555;
+@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           #ddd;
+@navbar-default-toggle-icon-bar-bg:        #888;
+@navbar-default-toggle-border-color:       #ddd;
+
+
+// Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color:                      @gray-light;
+@navbar-inverse-bg:                         #222;
+@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);
+
+// Inverted navbar links
+@navbar-inverse-link-color:                 @gray-light;
+@navbar-inverse-link-hover-color:           #fff;
+@navbar-inverse-link-hover-bg:              transparent;
+@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color:        #444;
+@navbar-inverse-link-disabled-bg:           transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color:                @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color:          #fff;
+@navbar-inverse-brand-hover-bg:             transparent;
+
+// Inverted navbar toggle
+@navbar-inverse-toggle-hover-bg:            #333;
+@navbar-inverse-toggle-icon-bar-bg:         #fff;
+@navbar-inverse-toggle-border-color:        #333;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @gray-lighter;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+@nav-open-link-hover-color:                 #fff;
+
+//== Tabs
+@nav-tabs-border-color:                     #ddd;
+
+@nav-tabs-link-hover-border-color:          @gray-lighter;
+
+@nav-tabs-active-link-hover-bg:             @body-bg;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   #ddd;
+
+@nav-tabs-justified-link-border-color:            #ddd;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   @gray-lighter;
+@jumbotron-heading-color:        inherit;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             #3c763d;
+@state-success-bg:               #dff0d8;
+@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text:                #31708f;
+@state-info-bg:                  #d9edf7;
+@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text:             #8a6d3b;
+@state-warning-bg:               #fcf8e3;
+@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text:              #a94442;
+@state-danger-bg:                #f2dede;
+@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #000;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                rgba(0,0,0,.2);
+//** Popover fallback border color
+@popover-fallback-border-color:       #ccc;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 #fff;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         15px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   rgba(0,0,0,.2);
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   #e5e5e5;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 #f5f5f5;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list items on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list items
+@list-group-active-color:       @component-active-color;
+//** Background color of active list items
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+//** Text color for content within active list items
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list items
+@list-group-disabled-color:      @gray-light;
+//** Background color of disabled list items
+@list-group-disabled-bg:         @gray-lighter;
+//** Text color for content within disabled list items
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color:         #555;
+@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-heading-padding:       10px 15px;
+@panel-footer-padding:        @panel-heading-padding;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          @state-success-text;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @state-success-bg;
+
+@panel-info-text:             @state-info-text;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @state-info-bg;
+
+@panel-warning-text:          @state-warning-text;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @state-warning-bg;
+
+@panel-danger-text:           @state-danger-text;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @state-danger-bg;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 darken(@well-bg, 7%);
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           bold;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           bold;
+@close-color:                 #000;
+@close-text-shadow:           0 1px 0 #fff;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray-light;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset:        @component-offset-horizontal;
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
+
+
diff --git a/labs/meteor-client/app/client/lib/custom.bootstrap.json b/labs/meteor-client/app/client/lib/custom.bootstrap.json
new file mode 100644
index 0000000000000000000000000000000000000000..01f000e56cc1aca9e2bee5b52814c35d5d60a6a1
--- /dev/null
+++ b/labs/meteor-client/app/client/lib/custom.bootstrap.json
@@ -0,0 +1,51 @@
+{"modules": {
+  "normalize":            true,
+  "print":                true,
+
+  "scaffolding":          true,
+  "type":                 true,
+  "code":                 true,
+  "grid":                 true,
+  "tables":               true,
+  "forms":                true,
+  "buttons":              true,
+
+  "glyphicons":           true,
+  "button-groups":        true,
+  "input-groups":         true,
+  "navs":                 true,
+  "navbar":               true,
+  "breadcrumbs":          true,
+  "pagination":           true,
+  "pager":                true,
+  "labels":               true,
+  "badges":               true,
+  "jumbotron":            true,
+  "thumbnails":           true,
+  "alerts":               true,
+  "progress-bars":        true,
+  "media":                true,
+  "list-group":           true,
+  "panels":               true,
+  "wells":                true,
+  "close":                true,
+
+  "component-animations": true,
+  "dropdowns":            true,
+  "modals":               true,
+  "tooltip":              true,
+  "popovers":             true,
+  "carousel":             true,
+
+  "affix":                true,
+  "alert":                true,
+  "button":               true,
+  "collapse":             true,
+  "scrollspy":            true,
+  "tab":                  true,
+  "transition":           true,
+
+  "utilities":            true,
+  "responsive-utilities": true
+}}
+
diff --git a/labs/meteor-client/app/client/lib/scale.raphael.js b/labs/meteor-client/app/client/lib/scale.raphael.js
new file mode 100644
index 0000000000000000000000000000000000000000..4339ea9efa85fbe9dac93bd6e890060cd0775f46
--- /dev/null
+++ b/labs/meteor-client/app/client/lib/scale.raphael.js
@@ -0,0 +1,102 @@
+/*
+ * ScaleRaphael 0.8 by Zevan Rosser 2010
+ * For use with Raphael library : www.raphaeljs.com
+ * Licensed under the MIT license.
+ *
+ * www.shapevent.com/scaleraphael/
+ */
+(function(){
+  window.ScaleRaphael = function(container, width, height){
+    var wrapper = document.getElementById(container);
+    if (!wrapper.style.position) wrapper.style.position = "relative";
+    wrapper.style.width = width + "px";
+    wrapper.style.height = height + "px";
+    wrapper.style.overflow = "hidden";
+    var nestedWrapper;
+
+    if (Raphael.type == "VML"){
+      wrapper.innerHTML = "<rvml:group style='position : absolute; width: 1000px; height: 1000px; top: 0px; left: 0px' coordsize='1000,1000' class='rvml' id='vmlgroup'><\/rvml:group>";
+      nestedWrapper = document.getElementById("vmlgroup");
+    }else{
+      wrapper.innerHTML = "<div id='svggroup'><\/div>";
+      nestedWrapper = document.getElementById("svggroup");
+    }
+
+    var paper = new Raphael(nestedWrapper, width, height);
+    var vmlDiv;
+
+    if (Raphael.type == "SVG"){
+      paper.canvas.setAttribute("viewBox", "0 0 "+width+" "+height);
+    }else{
+      vmlDiv = wrapper.getElementsByTagName("div")[0];
+    }
+
+    paper.changeSize = function(w, h, center, clipping){
+      clipping = !clipping;
+
+      var ratioW = w / width;
+      var ratioH = h / height;
+      var scale = ratioW < ratioH ? ratioW : ratioH;
+
+      var newHeight = parseInt(height * scale);
+      var newWidth = parseInt(width * scale);
+
+      if (Raphael.type == "VML"){
+         // scale the textpaths
+       var txt = document.getElementsByTagName("textpath");
+        for (var i in txt){
+          var curr = txt[i];
+          if (curr.style){
+            if(!curr._fontSize){
+              var mod = curr.style.font.split("px");
+              curr._fontSize = parseInt(mod[0]);
+              curr._font = mod[1];
+            }
+            curr.style.font = curr._fontSize * scale + "px" + curr._font;
+          }
+        }
+        var newSize;
+
+        if (newWidth < newHeight){
+         newSize = newWidth * 1000 / width;
+        }else{
+         newSize = newHeight * 1000 / height;
+        }
+        newSize = parseInt(newSize);
+        nestedWrapper.style.width = newSize + "px";
+        nestedWrapper.style.height = newSize + "px";
+        if (clipping){
+          nestedWrapper.style.left = parseInt((w - newWidth) / 2) + "px";
+          nestedWrapper.style.top = parseInt((h - newHeight) / 2) + "px";
+        }
+        vmlDiv.style.overflow = "visible";
+      }
+
+      if (clipping){
+        newWidth = w;
+        newHeight = h;
+      }
+
+      wrapper.style.width = newWidth + "px";
+      wrapper.style.height = newHeight + "px";
+      paper.setSize(newWidth, newHeight);
+
+      if (center){
+        wrapper.style.position = "absolute";
+        wrapper.style.left = parseInt((w - newWidth) / 2) + "px";
+        wrapper.style.top = parseInt((h - newHeight) / 2) + "px";
+      }
+    }
+
+    paper.scaleAll = function(amount){
+      paper.changeSize(width * amount, height * amount);
+    }
+
+    paper.changeSize(width, height);
+
+    paper.w = width;
+    paper.h = height;
+
+    return paper;
+  }
+})();
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/main.coffee b/labs/meteor-client/app/client/main.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..2449332c3ce35ea34e5b1095c1104fa1eaef166b
--- /dev/null
+++ b/labs/meteor-client/app/client/main.coffee
@@ -0,0 +1,199 @@
+# Helper to load javascript libraries from the BBB server
+loadLib = (libname) ->
+  successCallback = ->
+
+  retryMessageCallback = (param) ->
+    #Meteor.log.info "Failed to load library", param
+    console.log "Failed to load library", param
+
+  Meteor.Loader.loadJs("http://#{window.location.hostname}/client/lib/#{libname}", successCallback, 10000).fail(retryMessageCallback)
+
+# These settings can just be stored locally in session, created at start up
+Meteor.startup ->
+
+  # Load SIP libraries before the application starts
+  loadLib('sip.js')
+  loadLib('bbb_webrtc_bridge_sip.js')
+
+  @SessionAmplify = _.extend({}, Session,
+    keys: _.object(_.map(amplify.store(), (value, key) ->
+      [
+        key
+        JSON.stringify(value)
+      ]
+    ))
+    set: (key, value) ->
+      Session.set.apply this, arguments
+      amplify.store key, value
+      return
+  )
+# 
+Template.footer.helpers
+  getFooterString: ->
+    info = getBuildInformation()
+    foot = "(c) #{info.copyrightYear} BigBlueButton Inc. [build #{info.bbbServerVersion} - #{info.dateOfBuild}] - For more information visit #{info.link}"
+
+Template.header.events
+  "click .audioFeedIcon": (event) ->
+    $('.audioFeedIcon').blur()
+    toggleSlidingMenu()
+    toggleVoiceCall @
+
+  "click .chatBarIcon": (event) ->
+    $(".tooltip").hide()
+    toggleSlidingMenu()
+    toggleChatbar()
+
+  "click .collapseButton": (event) ->
+    toggleSlidingMenu()
+    $(".tooltip").hide()
+    $('.collapseButton').blur()
+    $('.myNavbar').css('z-index', 1032)
+
+  "click .hideNavbarIcon": (event) ->
+    $(".tooltip").hide()
+    toggleNavbar()
+
+  "click .lowerHand": (event) ->
+    $(".tooltip").hide()
+    toggleSlidingMenu()
+    Meteor.call('userLowerHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken"))
+
+  "click .muteIcon": (event) ->
+    $(".tooltip").hide()
+    toggleMic @
+
+  "click .raiseHand": (event) ->
+    #Meteor.log.info "navbar raise own hand from client"
+    console.log "navbar raise own hand from client"
+    $(".tooltip").hide()
+    toggleSlidingMenu()
+    Meteor.call('userRaiseHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken"))
+    # "click .settingsIcon": (event) ->
+    #   alert "settings"
+
+  "click .signOutIcon": (event) ->
+    $('.signOutIcon').blur()
+    if window.matchMedia('(orientation: portrait)').matches
+      if $('#dialog').dialog('option', 'height') isnt 450
+        $('#dialog').dialog('option', 'width', '100%')
+        $('#dialog').dialog('option', 'height', 450)
+    else
+      if $('#dialog').dialog('option', 'height') isnt 115
+        $('#dialog').dialog('option', 'width', 270)
+        $('#dialog').dialog('option', 'height', 115)
+    $("#dialog").dialog("open")
+  "click .hideNavbarIcon": (event) ->
+    $(".tooltip").hide()
+    toggleNavbar()
+  # "click .settingsIcon": (event) ->
+  #   alert "settings"
+
+  "click .usersListIcon": (event) ->
+    $(".tooltip").hide()
+    toggleSlidingMenu
+    toggleUsersList()
+
+  "click .videoFeedIcon": (event) ->
+    $(".tooltip").hide()
+    toggleCam @
+
+  "click .whiteboardIcon": (event) ->
+    $(".tooltip").hide()
+    toggleSlidingMenu
+    toggleWhiteBoard()
+
+  "mouseout #navbarMinimizedButton": (event) ->
+    $("#navbarMinimizedButton").removeClass("navbarMinimizedButtonLarge")
+    $("#navbarMinimizedButton").addClass("navbarMinimizedButtonSmall")
+
+  "mouseover #navbarMinimizedButton": (event) ->
+    $("#navbarMinimizedButton").removeClass("navbarMinimizedButtonSmall")
+    $("#navbarMinimizedButton").addClass("navbarMinimizedButtonLarge")
+
+Template.slidingMenu.events
+  'click .audioFeedIcon': (event) ->
+    $('.audioFeedIcon').blur()
+    toggleSlidingMenu()
+    toggleVoiceCall @
+    if BBB.amISharingAudio()
+      $('.navbarTitle').css('width', '70%')
+    else
+      $('.navbarTitle').css('width', '55%')
+
+  'click .chatBarIcon': (event) ->
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    toggleChatbar()
+
+  'click .lowerHand': (event) ->
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    Meteor.call('userLowerHand', getInSession('meetingId'), getInSession('userId'), getInSession('userId'), getInSession('authToken'))
+
+  'click .raiseHand': (event) ->
+    console.log 'navbar raise own hand from client'
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    Meteor.call('userRaiseHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken"))
+
+  'click .usersListIcon': (event) ->
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    toggleUsersList()
+
+  'click .whiteboardIcon': (event) ->
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    toggleWhiteBoard()
+
+  'click .collapseButton': (event) ->
+    $('.tooltip').hide()
+    toggleSlidingMenu()
+    $('.collapseButton').blur()
+
+Template.main.helpers
+	setTitle: ->
+		document.title = "BigBlueButton #{window.getMeetingName() ? 'HTML5'}"
+
+Template.main.rendered = ->
+  $("#dialog").dialog(
+    modal: true
+    draggable: false
+    resizable: false
+    autoOpen: false
+    dialogClass: 'no-close logout-dialog'
+    buttons: [
+      {
+        text: 'Yes'
+        click: () ->
+          userLogout getInSession("meetingId"), getInSession("userId"), true
+          $(this).dialog("close")
+        class: 'btn btn-xs btn-primary active'
+      }
+      {
+        text: 'No'
+        click: () ->
+          $(this).dialog("close")
+          $(".tooltip").hide()
+        class: 'btn btn-xs btn-default'
+      }
+    ]
+    position:
+      my: 'right top'
+      at: 'right bottom'
+      of: '.signOutIcon'
+  )
+
+  $(window).resize( ->
+    $('#dialog').dialog('close')
+  )
+
+  $('#darkened-screen').click () ->
+    toggleSlidingMenu()
+
+Template.makeButton.rendered = ->
+  $('button[rel=tooltip]').tooltip()
+
+Template.recordingStatus.rendered = ->
+  $('button[rel=tooltip]').tooltip()
diff --git a/labs/meteor-client/app/client/main.html b/labs/meteor-client/app/client/main.html
new file mode 100755
index 0000000000000000000000000000000000000000..4b1d227e123dc0fa3e7345eead44e4a174b4f2df
--- /dev/null
+++ b/labs/meteor-client/app/client/main.html
@@ -0,0 +1,161 @@
+<template name="footer">
+   <div class="myFooter gradientBar navbar navbar-default navbar-fixed-bottom" role="navigation">
+      {{{getFooterString}}}
+   </div>
+</template>
+
+<template name="header">
+  {{#if getInSession "display_navbar"}}
+    <div id="navbar" class="myNavbar gradientBar navbar navbar-default navbar-fixed-top" role="navigation">
+      <div class="navbarUserButtons navbarSection">
+        <div id="collapseButtonSection">
+          {{#if getInSession "display_slidingMenu"}}
+            {{> makeButton btn_class="navbarButton collapseButton" i_class="chevron-left" rel="tooltip" data_placement="bottom" title="Collapse"}}
+          {{else}}
+            {{> makeButton btn_class="navbarButton collapseButton" i_class="chevron-right" rel="tooltip" data_placement="bottom" title="Expand"}}
+          {{/if}}
+        </div>
+        <div class='collapseSection'>
+          <!-- display/hide users list toggle -->
+          {{#if getInSession "display_usersList"}}
+            {{> makeButton btn_class="navbarIconToggleActive usersListIcon navbarButton collapseSectionButton" i_class="user" rel="tooltip" data_placement="bottom" title="Hide List of Users"}}
+          {{else}}
+            {{> makeButton btn_class="usersListIcon navbarButton collapseSectionButton" i_class="user" rel="tooltip" data_placement="bottom" title="Show List of Users"}}
+          {{/if}}
+
+          <!-- display/hide whiteboard toggle -->
+          {{#if getInSession "display_whiteboard"}}
+            {{> makeButton btn_class="navbarIconToggleActive whiteboardIcon navbarButton collapseSectionButton" i_class="pencil" rel="tooltip" data_placement="bottom" title="Hide Whiteboard"}}
+          {{else}}
+            {{> makeButton btn_class="whiteboardIcon navbarButton collapseSectionButton" i_class="pencil" rel="tooltip" data_placement="bottom" title="Show Whiteboard"}}
+          {{/if}}
+
+          <!-- display/hide chat bar toggle -->
+          {{#if getInSession "display_chatbar"}}
+            {{> makeButton btn_class="navbarIconToggleActive chatBarIcon navbarButton collapseSectionButton" i_class="comment" rel="tooltip" data_placement="bottom" title="Hide Message Pane"}}
+          {{else}}
+            {{> makeButton btn_class="chatBarIcon navbarButton collapseSectionButton" i_class="comment" rel="tooltip" data_placement="bottom" title="Show Message Pane"}}
+          {{/if}}
+
+          <!-- display/hide webcam streams toggle -->
+          <!-- {{#if isCurrentUserSharingVideo}}
+                 {{> makeButton btn_class="navbarIconToggleActive videoFeedIcon navbarButton" i_class="stop" sharingVideo=true rel="tooltip" data_placement="bottom" title="Hide Webcams"}}
+               {{else}}
+                 {{> makeButton btn_class="videoFeedIcon navbarButton" i_class="facetime-video" sharingVideo=false rel="tooltip" data_placement="bottom" title="Show Webcams"}}
+               {{/if}} -->
+        </div>
+        <div class='audioControllersSection'>
+          <!-- Join/hang up audio call -->
+          {{#if isCurrentUserSharingAudio}}
+            <div class='collapseSection'>
+              {{> makeButton btn_class="navbarIconToggleActive audioFeedIcon navbarButton audioButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="bottom" title="Leave Audio Call"}}
+            </div>
+            {{#if isCurrentUserMuted}}
+              {{> makeButton btn_class="muteIcon navbarButton audioButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="bottom" title="Unmute"}}
+            {{else}}
+              {{#if isCurrentUserTalking}}
+                {{> makeButton btn_class="navbarIconToggleActive muteIcon navbarButton audioButton" i_class="volume-up" sharingAudio=true rel="tooltip" data_placement="bottom" title="Mute"}}
+              {{else}}
+                {{> makeButton btn_class="navbarIconToggleActive muteIcon navbarButton audioButton" i_class="volume-down" sharingAudio=true rel="tooltip" data_placement="bottom" title="Mute"}}
+              {{/if}}
+            {{/if}}
+          {{else}}
+            <div class='collapseSection'>
+              {{> makeButton btn_class="audioFeedIcon navbarButton audioButton" i_class="headphones" sharingAudio=false rel="tooltip" data_placement="bottom" title="Join Audio Call"}}
+            </div>
+          {{/if}}
+        </div>
+        <div class='collapseSection'>
+          {{#if isCurrentUserRaisingHand}}
+            {{> makeButton btn_class="lowerHand navbarIconToggleActive navbarButton collapseSectionButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Lower your hand"}}
+          {{else}}
+            {{> makeButton btn_class="raiseHand navbarButton collapseSectionButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Raise your hand"}}
+          {{/if}}
+
+          {{> recordingStatus}}
+        </div>
+      </div>
+      <div class="navbarTitle navbarSection"><span>{{getMeetingName}}</span></div>
+      <div class="navbarSettingsButtons navbarSection">
+        <!-- {{> makeButton id="userId" btn_class="settingsIcon navbarButton" i_class="cog" rel="tooltip" data_placement="bottom" title="Settings"}} -->
+        <!-- {{> makeButton btn_class="hideNavbarIcon navbarButton" i_class="chevron-up" rel="tooltip" data_placement="bottom" title="Hide Navbar"}} -->
+        {{> makeButton btn_class="signOutIcon navbarButton" i_class="log-out" rel="tooltip" data_placement="bottom" title="Logout"}}
+      </div>
+    </div>
+    <div class="navbarFiller"></div>
+  {{else}}
+    {{> makeButton id="navbarMinimizedButton" btn_class="hideNavbarIcon navbarMinimizedButtonSmall" i_class="chevron-down" rel="tooltip" data_placement="bottom" title="Display Navbar"}}
+  {{/if}}
+</template>
+
+<template name="main">
+	{{setTitle}}
+	<body>
+        <div id="dialog" title="Confirm Logout">
+            <p>Are you sure you want to log out?</p>
+        </div>
+		<div id="main" class="mainContainer row-fluid">
+ 		{{#if isDisconnected}}
+			{{>status}}
+		{{else}}
+			<div>{{> header}}</div>
+			{{> whiteboard id="whiteboard" title=getWhiteboardTitle name="whiteboard"}}
+			{{> chatbar id="chat" title="Chat" name="chatbar"}}
+			{{> usersList id="users" name="usersList"}}
+			<audio id="remote-media" autoplay="autoplay"></audio>
+			{{> footer}}
+		{{/if}}
+		</div>
+        {{> slidingMenu}}
+        <div id='darkened-screen'></div>
+	</body>
+</template>
+
+<template name="recordingStatus">
+	<!-- Recording status of the meeting -->
+	{{#with getCurrentMeeting}}
+		{{#if intendedForRecording}} 
+			{{#if currentlyBeingRecorded}}
+				<button class="recordingStatus recordingStatusTrue" rel="tooltip" data-placement="bottom" title="This Meeting is Being Recorded"><span class="glyphicon glyphicon-record"></span> Recording</button>
+			{{else}}
+				<button class="recordingStatus recordingStatusFalse" rel="tooltip" data-placement="bottom" title="This Meeting is Not Currently Being Recorded"><span class="glyphicon glyphicon-record"></span></button>
+			{{/if}}
+		{{/if}}
+	{{/with}}
+</template>
+
+<template name='slidingMenu'>
+  <div class="sliding-menu" id="sliding-menu">
+    <div class="slideSection">
+      {{#if getInSession "display_usersList"}}
+        {{> makeButton btn_class="navbarIconToggleActive usersListIcon slideButton" i_class="user" rel="tooltip" data_placement="right" title="Hide List of Users"}}
+      {{else}}
+        {{> makeButton btn_class="usersListIcon slideButton" i_class="user" rel="tooltip" data_placement="right" title="Show List of Users"}}
+      {{/if}}
+
+      {{#if getInSession "display_whiteboard"}}
+        {{> makeButton btn_class="navbarIconToggleActive whiteboardIcon slideButton" i_class="pencil" rel="tooltip" data_placement="right" title="Hide Whiteboard"}}
+      {{else}}
+        {{> makeButton btn_class="whiteboardIcon slideButton" i_class="pencil" rel="tooltip" data_placement="right" title="Show Whiteboard"}}
+      {{/if}}
+
+      {{#if getInSession "display_chatbar"}}
+        {{> makeButton btn_class="navbarIconToggleActive chatBarIcon slideButton" i_class="comment" rel="tooltip" data_placement="right" title="Hide Message Pane"}}
+      {{else}}
+        {{> makeButton btn_class="chatBarIcon slideButton" i_class="comment" rel="tooltip" data_placement="right" title="Show Message Pane"}}
+      {{/if}}
+
+      {{#if isCurrentUserSharingAudio}}
+        {{> makeButton btn_class="navbarIconToggleActive audioFeedIcon slideButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="right" title="Leave Audio Call"}}
+      {{else}}
+        {{> makeButton btn_class="audioFeedIcon slideButton" i_class="headphones" sharingAudio=false rel="tooltip" data_placement="right" title="Join Audio Call"}}
+      {{/if}}
+
+      {{#if isCurrentUserRaisingHand}}
+        {{> makeButton btn_class="lowerHand navbarIconToggleActive slideButton" i_class="hand-up" rel="tooltip" data_placement="right" title="Lower your hand"}}
+      {{else}}
+        {{> makeButton btn_class="raiseHand slideButton" i_class="hand-up" rel="tooltip" data_placement="right" title="Raise your hand"}}
+      {{/if}}
+    </div>
+  </div>
+</template>
diff --git a/labs/meteor-client/app/client/stylesheets/colors.less b/labs/meteor-client/app/client/stylesheets/colors.less
new file mode 100755
index 0000000000000000000000000000000000000000..cad4bc012a41ef4190dd86cf70fc64f3ca47d954
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/colors.less
@@ -0,0 +1,7 @@
+/* sorted by brightness */
+@white: #fff, #f5f5f5, #eee;
+@lightGrey: #d7d7d7, #cccdd1, #ccc, #999;
+@darkGrey: #666, #60636a, #40434c, #32353e;
+@azure: #54a7db, #0099FF;
+@black: #000;
+@yellow: #E3E1B8, #F9DF6B;
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/stylesheets/common/chat.less b/labs/meteor-client/app/client/stylesheets/common/chat.less
new file mode 100755
index 0000000000000000000000000000000000000000..914d998e0375853c133fc19a95f53efa72e0b86a
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/common/chat.less
@@ -0,0 +1,210 @@
+@import "../colors";
+
+bottomEntry {
+  border: none;
+  padding-bottom: 0px;
+  padding-top: 0px;
+}
+
+#chat {
+  background-color: extract(@white, 2);
+  border: 1px solid extract(@lightGrey, 3);
+  float: left;
+  background: extract(@white, 1);
+  margin-left: 0.25%;
+  margin-right: 0.25%;
+  overflow: hidden;
+  -webkit-flex: 1 6 25%;
+  flex: 1 6 25%;
+}
+
+.chat {
+  list-style: none;
+  margin: 0px;
+  padding: 0px;
+  li {
+    margin: 0px;
+    padding: 15px;
+    word-wrap: break-word;
+    &:nth-child(even) {
+      background-color: rgb(245,245,245);
+    }
+    table {
+      width: 100%;
+    }
+  }
+}
+
+#chatbar-contents {
+  background-color: extract(@white, 1);
+  height: 90%;
+  padding-left: 0px;
+  padding-right: 0px;
+  width: 100%;
+}
+
+#chatbody {
+  height: 90%;
+  overflow-y: scroll;
+  padding-left: 0px;
+  padding-right: 0px;
+}
+
+.chatGreeting {
+  color: blue;
+  margin-top: 5px;
+}
+
+.chat-input-wrapper {
+  padding:10px;	
+}
+
+.chatNameSelector {
+  font-size: 14px;
+  &:hover {
+    color: black !important;
+    font-weight: bold;
+  }
+}
+
+#chat-options-bar {
+  border-bottom: 1px solid extract(@lightGrey, 3);
+  position: relative;
+  width: 100%;
+}
+
+#chat-user-list {
+  padding: 5px;
+}
+
+.closeTab {
+  background-color: red !important;
+  border-radius: 5px;
+  color: extract(@white, 1);
+  cursor: pointer;
+  font-size: 12px;
+  font-weight: bold;
+  margin-left: 5px;
+  margin-top: -3px;
+  opacity: 0.2 !important;
+  padding-left: 3px !important;
+  padding-right: 3px !important;
+  padding-top: 4px !important;
+  text-shadow: 0 1px 0 extract(@white, 1);
+  -webkit-appearance: none;
+
+  &:hover {
+    color: extract(@white, 1) !important;
+    opacity: 1 !important;
+  }
+}
+
+.gotUnreadMail {
+  background: extract(@yellow, 2) !important;
+}
+
+#MoreChatsDrop {
+  float: right;
+}
+
+#MoreChatsbutton {
+  margin-right: 10px;
+  margin-top: 5px;
+  padding-bottom: 0px;
+  padding-top: 0px;
+}
+
+#newMessageInput {
+  display: block;
+  float: left;
+  width: 77%;
+  resize: none;
+  padding:5px;
+  border-radius:4px;
+  border:1px solid extract(@lightGrey, 3);
+}
+
+.optionsBar {
+  height: 100%;
+  padding-left: 15px;
+  padding-top: 15px;
+}
+
+.panel-footer {
+  bottom: 0px;
+  padding: 10px;
+  position: relative;
+  background: extract(@white, 1);
+}
+
+.private-chat-user-box {
+  border: left 1px grey;
+  height: 60%;
+  padding: 5px;
+  width: 90%;
+}
+
+.private-chat-user-list {
+  font-size: 12px;
+  :hover {
+    background: extract(@azure, 2);
+    font-size: 14px;
+    font-style: italic;
+  }
+}
+
+#sendMessageButton {
+  background-color: extract(@azure, 1);
+  margin-left: 8px;
+  color: extract(@white, 1);
+}
+
+#tabsList {
+  display: table;
+  margin: 0;
+  padding: 0;
+  table-layout: fixed;
+  text-align: center;
+  width: 100%;
+  a {
+    display: block;
+    min-height: 100%;
+    /* overflow: hidden; */
+    padding: 4px 10px;
+    text-decoration: none;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    color: extract(@lightGrey, 4);
+    margin: 0;
+    border-top: 0;
+    border-radius: 0;
+  }
+  li:first-child a {
+    border-left: 0;
+  }
+  &:before, &:after {
+    content: none;
+  }
+}
+
+.tab { 
+  font-size: 14px;
+  height:auto !important;
+  &:hover {
+    border: 1px solid extract(@azure, 2);
+    border-bottom: none;
+  }
+}
+
+.timestamp {
+  text-align: right;
+}
+
+.dropdown {
+  float: left;
+  .dropdown-menu {
+    height: 80px;
+    overflow-y: scroll;
+    right: 0px;
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/common/style.less b/labs/meteor-client/app/client/stylesheets/common/style.less
new file mode 100755
index 0000000000000000000000000000000000000000..13d9d3810f3d521bdcf01ee95d5cdc0950896655
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/common/style.less
@@ -0,0 +1,241 @@
+@import "../colors";
+
+.radius(@size: 4px) {
+  -moz-border-radius: @size;
+  -webkit-border-radius: @size;
+  border-radius: @size;
+}
+
+.linear-gradient(@color1, @color2) {
+  background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */
+  background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */
+  background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */
+  background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */
+}
+
+body {
+  background: extract(@white, 3);
+  bottom: 0;
+  color: extract(@darkGrey, 1);
+  left: 0;
+  right: 0;
+}
+
+.btn {
+  background-color: extract(@white, 2);
+}
+
+.component {
+  .radius;
+  background: extract(@white, 1);
+  border: 1px solid extract(@lightGrey, 3);
+  float: left;
+  height: 100%;
+  margin-top: 10px;
+}
+
+.extraConversationScrollableMenu {
+  height: auto;
+  max-height: 200px;
+  overflow-x: hidden;
+}
+
+.mainContainer {
+  height: 100%;
+}
+
+.myFooter {
+  color: black;
+  font-size: 10px;
+  max-height: 20px;
+  padding-top: 13px;
+  text-align: center;
+}
+
+.myNavbar {
+  margin-bottom: 0.5%;
+  &.gradientBar {
+    .linear-gradient(rgb(72,76,85), rgb(65,68,77));
+  }
+  .btn {
+    .linear-gradient(rgb(72,76,85), rgb(65,68,77));
+    border-left: 1px solid extract(@darkGrey, 2);
+    border-right: 1px solid extract(@darkGrey, 4);
+    &.navbarIconToggleActive {
+      background: extract(@darkGrey, 3);
+      border-bottom: 4px solid extract(@azure, 1);
+    }
+    i {
+      color: extract(@white, 1);
+    }
+  }
+}
+
+.myNavbarMinimized {
+  background: extract(@white, 3);
+  height: 20px;
+  margin-bottom: 0.2%;
+  margin-top: 0px;
+  min-width: 900px;
+  padding-top: 0px;
+  text-align: right;
+}
+
+.navbar {
+  min-height: 40px !important;
+}
+
+.navbarFiller {
+  width: 100%;
+}
+
+.navbarIconToggleActive i {
+  color: extract(@lightGrey, 2);
+}
+
+#navbarMinimizedButton {
+  margin-bottom: 0px;
+  margin-left: 2px;
+  margin-right: 20px;
+  margin-top: 0px;
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+
+.navbarMinimizedButtonSmall {
+  height: 10px;
+  width: 40px;
+}
+
+.navbarMinimizedButtonLarge {
+  height: 50px;
+  width: 40px;
+}
+
+.navbarSettingsButtons {
+  float: right;
+  .btn {
+    display: block;
+    float: right;
+  }
+}
+
+.navbarTitle {
+  color: extract(@white, 1);
+  display: block;
+  float: left;
+  font-weight: bold;
+  text-align: center;
+}
+
+.navbarUserButtons .btn {
+  border-radius: 0;
+  display: block;
+  float: left;
+  &:hover {
+    background: extract(@darkGrey, 3);
+  }
+}
+
+.navbarSettingsButtons .btn:hover { 
+  background: extract(@darkGrey, 3);
+}
+
+.panel-footer {
+  padding: 0;
+}
+
+.recordingStatus {
+  background: none!important;
+  border: none;
+  margin-left: 10px;
+  padding: 0!important;
+}
+
+.recordingStatusFalse {
+  color: maroon;
+}
+
+.recordingStatusTrue {
+  color: green;
+}
+
+.ScrollableWindow {
+  height: 100%;
+  overflow-y: scroll;
+}
+
+.tab {
+  height: 40px;
+}
+
+.table > thead > tr > th,
+.table > tbody > tr > th,
+.table > tfoot > tr > th,
+.table > thead > tr > td,
+.table > tbody > tr > td,
+.table > tfoot > tr > td {
+  border-top: 0px;
+}
+
+.title {
+  border-bottom: 1px solid extract(@lightGrey, 1);
+  color: extract(@darkGrey, 1);
+  font-weight: bold;
+  line-height: 2em;
+  margin: 0;
+  padding-bottom: 5px;
+  padding-left: 10px;
+  padding-top: 5px;
+}
+
+#main {
+  padding-top: 50px;
+  padding-left: 0.25%;
+  padding-right: 0.25%;
+  display: -webkit-flex;
+  display: flex;
+}
+
+/* Custom alert box */
+
+.no-close .ui-dialog-titlebar-close {
+  display: none; /* no close button */
+}
+
+.logout-dialog.ui-dialog {
+  .ui-widget-header {
+    color: extract(@white, 1);
+    font-weight: bold;
+    background: extract(@darkGrey, 3);
+  }
+  .ui-dialog-content {
+    font-weight: bold;
+    text-align: center;
+  }
+}
+
+.logout-dialog.ui-widget-content {
+  background: extract(@white, 3);
+  border: 5px solid extract(@darkGrey, 3);
+}
+
+.custom-button {
+  background-color: extract(@yellow, 1);
+}
+
+.fullScreenPresentation {
+  width: 90% !important;
+  height: 90% !important;
+}
+
+.halfScreen {
+  width: 44% !important;
+  height: 80% !important;
+}
+
+.quarterScreen {
+  width: 22% !important;
+  height: 80% !important;
+}
diff --git a/labs/meteor-client/app/client/stylesheets/common/users.less b/labs/meteor-client/app/client/stylesheets/common/users.less
new file mode 100755
index 0000000000000000000000000000000000000000..b07f1ef30c8b4209eb4238f9430beab7d8eae7f8
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/common/users.less
@@ -0,0 +1,55 @@
+#content {
+  margin-top: 10px;
+  overflow: hidden;
+}
+
+#usericons {
+  margin-left: 10px;
+  float: right;
+  span {
+    margin-left: 3px;
+    padding: 2px;
+    border: 1px solid white;
+  }
+  .raisedHandIcon, .muteIcon {
+    &:hover { 
+      cursor: pointer;
+      border: 1px solid #f2f2f2;
+      background-color: #f2f2f2;
+      border-radius: 4px;
+    }
+  }
+}
+
+#username {
+  float:left;
+}
+
+#users {
+  margin-left: 0.25%;
+  margin-right: 0.25%;
+  padding-bottom: 10px; /*min-width:230px;*/
+  -webkit-flex: 1 6 20%;
+  flex: 1 6 20%;
+}
+
+#user-contents {
+  height: 95%;
+  padding-bottom: 10px;
+}
+
+.userCurrent {
+  font-weight: bold;
+}
+
+/*.userNameContainer {
+  border-right: 1px solid extract(@lightGrey, 3);
+  padding-right: 0px;
+}*/
+
+.userNameEntry {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100px;
+}
diff --git a/labs/meteor-client/app/client/stylesheets/common/whiteboard.less b/labs/meteor-client/app/client/stylesheets/common/whiteboard.less
new file mode 100755
index 0000000000000000000000000000000000000000..fc66702642ff7cbdd2eb28943c510d425112bf4f
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/common/whiteboard.less
@@ -0,0 +1,26 @@
+#whiteboard {
+  padding: 0 !important;
+  margin-left: 0.25%;
+  margin-right: 0.25%;
+  -webkit-flex: 3 1 53%;
+  flex: 3 1 53%;
+}
+
+#whiteboard-paper {
+  background-color: white !important;
+  /*height: auto !important;*/
+  margin-left: auto;
+  margin-right: auto;
+  #svggroup {
+    display: block;
+    height: 100% !important;
+    width: 100% !important;
+  }
+}
+
+#whiteboard-navbar {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100%;
+}
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/stylesheets/landscape/chat.less b/labs/meteor-client/app/client/stylesheets/landscape/chat.less
new file mode 100755
index 0000000000000000000000000000000000000000..98faa09dfa37069e59c508583e05924f3d9eb441
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/landscape/chat.less
@@ -0,0 +1,23 @@
+@import "../colors";
+
+@media all and (orientation: landscape) {
+  #chat {
+    min-height: 500px;
+    -webkit-order: 3;
+    order: 3;
+  }
+
+  #newMessageInput {
+    height: 40px;
+  }
+
+  #sendMessageButton {
+    height: 40px;
+  }
+
+  #tabsList {
+    a {
+      font-size:12px;
+    }
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/landscape/style.less b/labs/meteor-client/app/client/stylesheets/landscape/style.less
new file mode 100755
index 0000000000000000000000000000000000000000..d79c57d7c917cae2e0818c2fac2c0012d8c5441d
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/landscape/style.less
@@ -0,0 +1,81 @@
+@import "../colors";
+
+@media all and (orientation: landscape) {
+  .radius(@size: 4px) {
+    -moz-border-radius: @size;
+    -webkit-border-radius: @size;
+    border-radius: @size;
+  }
+
+  .linear-gradient(@color1, @color2) {
+    background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */
+    background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */
+    background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */
+    background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */
+  }
+
+  body {
+    height: 100%;
+    min-width: 768px;
+    position: absolute;
+    top: 0;
+  }
+
+  .myNavbar {
+    min-width: 768px;
+  }
+
+  .navbarButton {
+    height: 50px;
+    width: 51.2px;
+  }
+
+  .navbarFiller {
+    height: 50px;
+  }
+
+  .navbarSection {
+    min-width: 256px;
+    width: 33%;
+  }
+
+  .navbarTitle {
+    font-size: 16px;
+    padding-top: 13px;
+    width: 30% !important;
+  }
+
+  .navbarUserButtons {
+    float: left;
+  }
+
+  .title {
+    font-size: 14px;
+  }
+
+  #main {
+    -webkit-flex-flow: row;
+    flex-flow: row;
+  }
+
+  #collapseButtonSection {
+    display: none;
+  }
+
+  .collapseSection {
+    display: block !important;
+  }
+
+  .logout-dialog.ui-dialog {
+    .ui-widget-header {
+      font-size: 12px;
+    }
+    .ui-dialog-content {
+      font-size: 11px;
+    }
+  }
+
+  .logout-dialog.ui-widget-content {
+    font-size: 10px;
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/landscape/users.less b/labs/meteor-client/app/client/stylesheets/landscape/users.less
new file mode 100755
index 0000000000000000000000000000000000000000..c3e743a81758adfd22b8055300ec02ff3820b1a7
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/landscape/users.less
@@ -0,0 +1,21 @@
+@media all and (orientation: landscape) {
+  #username {
+    display:block;
+    width:70%;
+  }
+
+  #users {
+    -webkit-order: 1;
+    order: 1;
+  }
+
+  #user-contents {
+    .userlist { 
+      padding:10px;
+    }
+  }
+
+  .userNameEntry {
+    height: 20px;
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less b/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less
new file mode 100755
index 0000000000000000000000000000000000000000..348e6bd75262e6715c53af5ba995708cbd50fec0
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less
@@ -0,0 +1,6 @@
+@media all and (orientation: landscape) {
+  #whiteboard {
+    -webkit-order: 2;
+    order: 2;
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/portrait/chat.less b/labs/meteor-client/app/client/stylesheets/portrait/chat.less
new file mode 100755
index 0000000000000000000000000000000000000000..10d0aa52356fb764ad27ac3ca7c470bd0d6f340d
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/portrait/chat.less
@@ -0,0 +1,38 @@
+@import "../colors";
+
+@media all and (orientation: portrait) {
+  #chat {
+    -webkit-order: 2;
+    order: 2;
+    min-height: 40%;
+    max-height: 40%;
+  }
+
+  .chat {
+    li {
+      td {
+        font-size: 30px;
+      }
+      div {
+        font-size: 15px;
+      }
+    }
+  }
+
+  #newMessageInput {
+    height: 60px;
+    font-size: 25px;
+  }
+
+  #sendMessageButton {
+    height: 60px;
+    width: 100px;
+    font-size: 30px;
+  }
+
+  #tabsList {
+    a {
+      font-size: 25px;
+    }
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/portrait/style.less b/labs/meteor-client/app/client/stylesheets/portrait/style.less
new file mode 100755
index 0000000000000000000000000000000000000000..bd58daeea531a0e3f37c86ccd36ab4ec2b500902
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/portrait/style.less
@@ -0,0 +1,146 @@
+@import "../colors";
+
+@media all and (orientation: portrait) {
+  .radius(@size: 4px) {
+    -moz-border-radius: @size;
+    -webkit-border-radius: @size;
+    border-radius: @size;
+  }
+
+  .linear-gradient(@color1, @color2) {
+    background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */
+    background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */
+    background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */
+    background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */
+  }
+
+  body {
+    position: relative;
+    top: 15px;
+  }
+
+  .navbarButton {
+    height: 100px;
+    width: 15%;
+    min-width: 60px;
+  }
+
+  .navbarTitle {
+    font-size: 30px;
+    padding-top: 30px;
+    padding-left: 5px;
+    overflow: hidden;
+    height: 72px;
+    width: 70%;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .title {
+    font-size: 30px;
+  }
+
+  #main {
+    -webkit-flex-flow: column;
+    flex-flow: column;
+    flex-direction: column;
+    min-height: 125%;
+    max-height: 125%;
+  }
+
+  .glyphicon {
+    font-size: 35px;
+  }
+
+  .myNavbar {
+    min-width: 630px;
+  }
+
+  .collapseSection {
+    display: none;
+  }
+
+  .logout-dialog.ui-dialog {
+    .ui-widget-header {
+      font-size: 40px;
+    }
+  }
+
+  .logout-dialog.ui-widget-content {
+    font-size: 280%;
+  }
+
+  .ui-dialog-buttonset {
+    width: 100%;
+  }
+
+  .ui-dialog-buttonset button {
+    width: 40%;
+    margin-left: 5% !important;
+    margin-right: 5% !important;
+  }
+
+  /* Sliding menu */
+
+  .sliding-menu {
+    width: 15%;
+    height: 100%;
+    position: fixed;
+    top: 0;
+    left: -15%;
+    z-index: 1031;
+    &.sliding-menu-opened {
+      left: 0px;
+    }
+    a {
+      border-bottom: 1px solid #258ecd;
+      padding: 1em;
+    }
+  }
+
+  .slideSection {
+    float: left;
+    margin-top: 101px;
+    height: 100%;
+    width: 100%;
+  }
+
+  .slideButton {
+    display: block;
+    width: 100%;
+    height: calc(~'20% - 20px');
+  }
+
+  .slideSection {
+    margin-bottom: 0.5%;
+    &.gradientBar {
+      .linear-gradient(rgb(72,76,85), rgb(65,68,77));
+    }
+    .btn {
+      .linear-gradient(rgb(72,76,85), rgb(65,68,77));
+      border-left: 1px solid extract(@darkGrey, 2);
+      border-right: 1px solid extract(@darkGrey, 4);
+      &.navbarIconToggleActive {
+        background: extract(@darkGrey, 3);
+        border-bottom: 4px solid extract(@azure, 1);
+      }
+      i {
+        color: extract(@white, 1);
+      }
+    }
+  }
+
+  #darkened-screen {
+    display: none;
+    background: black;
+    opacity: 0.7;
+    z-index: 1030;
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/portrait/users.less b/labs/meteor-client/app/client/stylesheets/portrait/users.less
new file mode 100755
index 0000000000000000000000000000000000000000..bcc7eed7cb78c6b61645fda88a9c9c6118c4ccaf
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/portrait/users.less
@@ -0,0 +1,16 @@
+@media all and (orientation: portrait) {
+  #users {
+    -webkit-order: 3;
+    order: 3;
+    min-height: 20%;
+    max-height: 20%;
+  }
+
+  .userNameEntry {
+    height: 35px;
+    font-size: 30px;
+    strong {
+      font-size: 25px;
+    }
+  }
+}
diff --git a/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less b/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less
new file mode 100755
index 0000000000000000000000000000000000000000..752e668862ccf7c5d261c362f0f2159341bcf142
--- /dev/null
+++ b/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less
@@ -0,0 +1,6 @@
+@media all and (orientation: portrait) {
+  #whiteboard {
+    -webkit-order: 1;
+    order: 1;
+  }
+}
diff --git a/labs/meteor-client/app/client/views/chat/chat_bar.coffee b/labs/meteor-client/app/client/views/chat/chat_bar.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..702b245f09c3ad8252751e7687d8839ca806d62c
--- /dev/null
+++ b/labs/meteor-client/app/client/views/chat/chat_bar.coffee
@@ -0,0 +1,341 @@
+# --------------------------------------------------------------------------------------------------------------------
+# If a function's last line is the statement false that represents the function returning false
+# A function such as a click handler will continue along with the propogation and default behaivour if not stopped
+# Returning false stops propogation/prevents default. You cannot always use the event object to call these methods
+# Because most Meteor event handlers set the event object to the exact context of the event which does not
+# allow you to simply call these methods.
+# --------------------------------------------------------------------------------------------------------------------
+
+@activateBreakLines = (str) ->
+  if typeof str is 'string'
+    res = str.replace /\\n/gim, '<br/>'
+    res = res.replace /\r/gim, '<br/>'
+
+@detectUnreadChat = ->
+  #if the current tab is not the same as the tab we just published in
+  Meteor.Chat.find({}).observe({
+    added: (chatMessage) =>
+      findDestinationTab = ->
+        if chatMessage.message?.chat_type is "PUBLIC_CHAT"
+          "PUBLIC_CHAT"
+        else
+          chatMessage.message?.from_userid
+      Tracker.autorun (comp) ->
+        tabsTime = getInSession('tabsRenderedTime')
+        if tabsTime? and chatMessage.message.from_userid isnt "SYSTEM_MESSAGE" and chatMessage.message.from_time - tabsTime > 0
+          populateChatTabs(chatMessage) # check if we need to open a new tab
+          destinationTab = findDestinationTab()
+          if destinationTab isnt getInSession "inChatWith"
+            setInSession 'chatTabs', getInSession('chatTabs').map((tab) ->
+              tab.gotMail = true if tab.userId is destinationTab
+              tab
+            )
+        comp.stop()
+    })
+
+# This method returns all messages for the user. It looks at the session to determine whether the user is in
+# private or public chat. If true is passed, messages returned are from before the user joined. Else, the messages are from after the user joined
+@getFormattedMessagesForChat = ->
+  chattingWith = getInSession('inChatWith')
+  if chattingWith is 'PUBLIC_CHAT' # find all public and system messages
+    return Meteor.Chat.find({'message.chat_type': $in: ["SYSTEM_MESSAGE","PUBLIC_CHAT"]},{sort: {'message.from_time': 1}}).fetch()
+  else
+    unless chattingWith is 'OPTIONS'
+      return Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT', $or: [{'message.to_userid': chattingWith},{'message.from_userid': chattingWith}]}).fetch()
+
+# Scrolls the message container to the bottom. The number of pixels to scroll down is the height of the container
+Handlebars.registerHelper "autoscroll", ->
+  $('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
+  false
+
+Handlebars.registerHelper "grabChatTabs", ->
+  if getInSession('chatTabs') is undefined
+    initTabs = [
+      userId: "PUBLIC_CHAT"
+      name: "Public"
+      gotMail: false
+      class: "publicChatTab"
+    ,
+      userId: "OPTIONS"
+      name: "Options"
+      gotMail: false
+      class: "optionsChatTab"
+    ]
+    setInSession 'chatTabs', initTabs
+  getInSession('chatTabs')[0..3]
+
+@sendMessage = ->
+  message = linkify $('#newMessageInput').val() # get the message from the input box
+  unless (message?.length > 0 and (/\S/.test(message))) # check the message has content and it is not whitespace
+    return # do nothing if invalid message
+
+  chattingWith = getInSession('inChatWith')
+
+  if chattingWith isnt "PUBLIC_CHAT"
+    toUsername = Meteor.Users.findOne(userId: chattingWith)?.user.name
+
+  messageForServer = { # construct message for server
+    "message": message
+    "chat_type": if chattingWith is "PUBLIC_CHAT" then "PUBLIC_CHAT" else "PRIVATE_CHAT"
+    "from_userid": getInSession("userId")
+    "from_username": BBB.getMyUserName()
+    "from_tz_offset": "240"
+    "to_username": if chattingWith is "PUBLIC_CHAT" then "public_chat_username" else toUsername
+    "to_userid": if chattingWith is "PUBLIC_CHAT" then "public_chat_userid" else chattingWith
+    "from_lang": "en"
+    "from_time": getTime()
+    "from_color": "0x000000"
+    # "from_color": "0x#{getInSession("messageColor")}"
+  }
+
+  Meteor.call "sendChatMessagetoServer", getInSession("meetingId"), messageForServer, getInSession("userId"), getInSession("authToken")
+
+  $('#newMessageInput').val '' # Clear message box
+
+Template.chatbar.helpers
+  getCombinedMessagesForChat: ->
+    msgs = getFormattedMessagesForChat()
+    len = msgs?.length # get length of messages
+    i = 0
+    while i < len # Must be a do while, for loop compiles and stores the length of array which can change inside the loop!
+      if msgs[i].message.from_userid isnt 'System' # skip system messages
+        j = i+1 # Start looking at messages right after the current one
+
+        while j < len
+          deleted = false
+          if msgs[j].message.from_userid isnt 'System' # Ignore system messages
+            # Check if the time discrepancy between the two messages exceeds window for grouping
+            if (parseFloat(msgs[j].message.from_time)-parseFloat(msgs[i].message.from_time)) >= 60000 # 60 seconds/1 minute
+              break # Messages are too far between, so them seperated and stop joining here
+
+            if msgs[i].message.from_userid is msgs[j].message.from_userid # Both messages are from the same user
+              msgs[i].message.message += "\r#{msgs[j].message.message}" # Combine the messages
+              msgs.splice(j,1) # Delete the message from the collection
+              deleted = true
+            else break # Messages are from different people, move on
+            #
+          else break # This is the break point in the chat, don't merge
+          #
+          len = msgs.length
+          ++j if not deleted
+      #
+      ++i
+      len = msgs.length
+
+    msgs
+
+  userExists: ->
+    if getInSession('inChatWith') in ["PUBLIC_CHAT", "OPTIONS"]
+      return true
+    else
+      return Meteor.Users.findOne({userId: getInSession('inChatWith')})?
+
+# When chatbar gets rendered, launch the auto-check for unread chat
+Template.chatbar.rendered = ->
+  detectUnreadChat()
+
+# When message gets rendered, scroll to the bottom
+Template.message.rendered = ->
+  $('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight)
+  false
+
+Template.chatInput.events
+  'click #sendMessageButton': (event) ->
+    $('#sendMessageButton').blur()
+    sendMessage()
+
+  'keypress #newMessageInput': (event) -> # user pressed a button inside the chatbox
+    key = (if event.charCode then event.charCode else (if event.keyCode then event.keyCode else 0))
+
+    if event.shiftKey and (key is 13)
+      event.preventDefault()
+      $("#newMessageInput").append("\r") # Change newline character
+      return
+
+    if key is 13 # Check for pressing enter to submit message
+      event.preventDefault()
+      sendMessage()
+      $('#newMessageInput').val("")
+      return false
+
+Template.chatInput.rendered  = ->
+   $('input[rel=tooltip]').tooltip()
+   $('button[rel=tooltip]').tooltip()
+
+Template.extraConversations.events
+	"click .extraConversation": (event) ->
+		console.log "extra conversation"
+		user = @
+		console.log user
+		console.log "#{user.name} #{user.userId}"
+		# put this conversation in the 3rd position in the chat tabs collection (after public and options)
+		# Take all the tabs and turn into an array
+		tabArray = getInSession('chatTabs')
+
+		# find the index of the selected tab
+		index = do ->
+			for value, idx in tabArray
+				if value.userId is user.userId
+					selected = value
+					return idx
+			null
+
+		if index?
+			# take object
+			selected = tabArray[index]
+
+			if selected?
+				# remove it
+				tabArray.splice(index, 1)
+				# insert it at the 3rd index
+				tabArray.splice(2, 0, selected)
+				# update collection
+				setInSession 'chatTabs', tabArray
+
+Template.extraConversations.helpers
+  getExtraConversations: ->
+    getInSession('chatTabs')[4..]
+
+  tooManyConversations: ->
+    return false if getInSession('chatTabs') is undefined
+    getInSession('chatTabs').length > 4
+
+Template.message.helpers
+  sanitizeAndFormat: (str) ->
+    if typeof str is 'string'
+      # First, replace replace all tags with the ascii equivalent (excluding those involved in anchor tags)
+      res = str.replace(/&/g, '&amp;').replace(/<(?![au\/])/g, '&lt;').replace(/\/([^au])>/g, '$1&gt;').replace(/([^=])"(?!>)/g, '$1&quot;');
+      res = toClickable res
+      res = activateBreakLines res
+
+  toClockTime: (epochTime) ->
+    if epochTime is null
+      return ""
+    local = new Date()
+    offset = local.getTimezoneOffset()
+    epochTime = epochTime - offset * 60000 # 1 min = 60 s = 60,000 ms
+    dateObj = new Date(epochTime)
+    hours = dateObj.getUTCHours()
+    minutes = dateObj.getUTCMinutes()
+    if minutes < 10
+      minutes = "0" + minutes
+    hours + ":" + minutes
+
+Template.optionsBar.events
+  'click .private-chat-user-entry': (event) -> # clicked a user's name to begin private chat
+    tabs = getInSession('chatTabs')
+    _this = @
+
+    # if you are starting a private chat
+    if tabs.filter((tab) -> tab.userId is _this.userId).length is 0
+      userName = Meteor.Users.findOne({userId: _this.userId})?.user?.name
+      tabs.push {userId: _this.userId, name: userName, gotMail: false, class: 'privateChatTab'}
+      setInSession 'chatTabs', tabs
+
+    setInSession 'display_chatPane', true
+    setInSession "inChatWith", _this.userId
+    $("#newMessageInput").focus()
+
+Template.optionsBar.helpers
+  thereArePeopletoChatWith: -> # Subtract 1 for the current user. Returns whether there are other people in the chat
+    # TODO: Add a check for the count to only include users who are allowed to private chat
+    (Meteor.Users.find({'meetingId': getInSession("meetingId")}).count()-1) >= 1
+
+Template.optionsBar.rendered = ->
+  $('div[rel=tooltip]').tooltip()
+
+Template.optionsFontSize.events
+  "click .fontSizeSelector": (event) ->
+    selectedFontSize = parseInt(event.target.id)
+    if selectedFontSize
+      setInSession "messageFontSize", selectedFontSize
+    else setInSession "messageFontSize", 12
+
+Template.tabButtons.events
+  'click .close': (event) -> # user closes private chat
+    setInSession 'inChatWith', 'PUBLIC_CHAT'
+    setInSession 'display_chatPane', true
+    console.log "userId: #{@userId}"
+    _this = @
+    tabs = getInSession('chatTabs')
+    if tabs.filter((tab) -> tab.userId is _this.userId).length > 0
+      tabs = $.grep(tabs, (t) -> t.userId isnt _this.userId)
+      setInSession 'chatTabs', tabs
+
+    return false # stops propogation/prevents default
+
+  'click .gotUnreadMail': (event) ->
+    #chatTabs.update({userId: @userId}, {$set: {gotMail: false}})
+    _this = @
+    setInSession 'chatTabs', getInSession('chatTabs').map((tab) ->
+      tab.gotMail = false if tab.userId is _this.userId
+      tab
+    )
+
+  'click .optionsChatTab': (event) ->
+    console.log "options"
+    setInSession "inChatWith", "OPTIONS"
+    setInSession 'display_chatPane', false
+
+  'click .privateChatTab': (event) ->
+    console.log "private:"
+    console.log @
+    setInSession "inChatWith", @userId
+    setInSession 'display_chatPane', true
+
+  'click .publicChatTab': (event) ->
+    console.log "public"
+    setInSession "inChatWith", "PUBLIC_CHAT"
+    setInSession 'display_chatPane', true
+
+  'click .tab': (event) ->
+    unless getInSession "inChatWith" is "OPTIONS"
+      $("#newMessageInput").focus()
+      console.log "tab"
+
+Template.tabButtons.helpers
+  hasGotUnreadMailClass: (gotMail) ->
+    if gotMail
+      return "gotUnreadMail"
+    else
+      return ""
+
+  isTabActive: (userId) ->
+    if getInSession("inChatWith") is userId
+      return "active"
+
+  makeSafe: (string) ->
+    safeString(string)
+
+Template.tabButtons.rendered = ->
+  Tracker.autorun (comp) ->
+    setInSession 'tabsRenderedTime', TimeSync.serverTime()
+    if getInSession('tabsRenderedTime') isnt undefined
+      comp.stop()
+
+# make links received from Flash client clickable in HTML
+@toClickable = (str) ->
+  if typeof str is 'string'
+    res = str.replace /<a href='event:/gim, "<a target='_blank' href='"
+    res = res.replace /<a href="event:/gim, '<a target="_blank" href="'
+
+Template.message.helpers
+  toClockTime: (epochTime) ->
+    if epochTime is null
+      return ""
+    local = new Date()
+    offset = local.getTimezoneOffset()
+    epochTime = epochTime - offset * 60000 # 1 min = 60 s = 60,000 ms
+    dateObj = new Date(epochTime)
+    hours = dateObj.getUTCHours()
+    minutes = dateObj.getUTCMinutes()
+    if minutes < 10
+      minutes = "0" + minutes
+    hours + ":" + minutes
+
+  sanitizeAndFormat: (str) ->
+    if typeof str is 'string'
+      # First, replace replace all tags with the ascii equivalent (excluding those involved in anchor tags)
+      res = str.replace(/&/g, '&amp;').replace(/<(?![au\/])/g, '&lt;').replace(/\/([^au])>/g, '$1&gt;').replace(/([^=])"(?!>)/g, '$1&quot;');
+      res = toClickable res
+      res = activateBreakLines res
diff --git a/labs/meteor-client/app/client/views/chat/chat_bar.html b/labs/meteor-client/app/client/views/chat/chat_bar.html
new file mode 100755
index 0000000000000000000000000000000000000000..138c4594a160b6fb27ecf6f19848375107091315
--- /dev/null
+++ b/labs/meteor-client/app/client/views/chat/chat_bar.html
@@ -0,0 +1,147 @@
+<template name="chatbar">
+	<div id="{{id}}" {{visibility name}} class="component">
+		<div id="chatbar-contents">
+            <h3 class="title gradientBar">
+                <span class="glyphicon glyphicon-comment"></span>
+                {{title}}
+                {{> extraConversations}}
+            </h3>
+			{{>tabButtons}} <!-- Display public/options tabs, and private chat tabs -->
+			{{#if getInSession "display_chatPane"}}
+				{{#if userExists}}
+					<div id="chatbody">
+						<ul class="chat">
+							{{#each getCombinedMessagesForChat}}
+								{{#if message}}
+									<li {{messageFontSize}}>{{> message}}</li>
+								{{/if}}
+							{{/each}}
+							<bottomEntry></bottomEntry> <!-- Intentionally blank, fixes issue of last message being cut off while scrolling -->
+						</ul>
+					</div>
+					<div class="panel-footer">{{> chatInput}}</div>
+				{{else}}
+					<div id="chatbody">
+						<ul class="chat">
+							{{#each getCombinedMessagesForChat}}
+								{{#if message}}
+									<li {{messageFontSize}}>{{> message}}</li>
+								{{/if}}
+							{{/each}}
+							<bottomEntry></bottomEntry> <!-- Intentionally blank, fixes issue of last message being cut off while scrolling -->
+							<li>The user has left</li>
+						</ul>
+					</div>
+				{{/if}}
+			{{else}}
+				{{> optionsBar}}
+			{{/if}}
+		</div>
+	</div>
+</template>
+
+<template name="chatInput">
+	<div class="chat-input-wrapper">
+		<textarea id="newMessageInput" placeholder="Write a message..." rel="tooltip" data-placement="top" title="Write a new message" autofocus></textarea>
+		<button type="submit" id="sendMessageButton" class="btn" rel="tooltip" data-placement="top" title="Click to send your message">
+		Send
+		</button>
+	</div>
+</template>
+
+<template name="chatOptions">
+	<p>Chat Options:</p>
+	{{> optionsFontSize}}
+</template>
+
+<template name="extraConversations">
+    {{#if tooManyConversations}}
+        <div id="MoreChatsDrop" class="btn-group">
+            <button type="button" id="MoreChatsbutton" class="btn btn-default dropdown-toggle" data-toggle="dropdown">More Chats<span class="caret"></span></button>
+            <ul class="dropdown-menu extraConversationScrollableMenu" role="menu">
+                {{#each getExtraConversations}}
+                    <li class="extraConversation" id="{{safeName name}}"><a href="#">{{safeName name}}</a></li>
+                {{/each}}
+            </ul>
+        </div>
+    {{/if}}
+</template>
+
+<!-- Displays and styles an individual message in the chat -->
+<template name="message">
+    <span style="float:left;">
+        {{#if message.from_username}}
+            <span {{messageFontSize}} class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{message.from_username}}">
+                <strong>{{message.from_username}}</strong>
+            </span>
+        {{/if}}
+    </span>
+    <span style="float:right;">
+                {{#if message.from_time}}
+                {{toClockTime message.from_time}}
+                <span class="glyphicon glyphicon-time"></span>
+            {{/if}}
+    </span>
+    <br/>
+    <div style="color:{{colourToHex message.from_color}}">{{{sanitizeAndFormat message.message}}}</div>
+    {{autoscroll}}
+</template>
+
+<!-- Displays the list of options available -->
+<template name="optionsBar">
+	<div class="optionsBar">
+		{{#if thereArePeopletoChatWith}} <!-- There are people we can chat with, display the user list -->
+			<p>Select a person to chat with privately</p>
+			<div class="private-chat-user-box" rel="tooltip" data-placement="top" title="Select a participant to open a private chat">
+				{{#Layout template="scrollWindow" id="privateChatUserScrollWindow"}}
+					{{#contentFor region="scrollContents"}}
+                        <div class="private-chat-user-list">
+                            {{#each getUsersInMeeting}}
+                            <div class="private-chat-user-entry">{{#unless isCurrentUser userId}}{{user.name}}{{/unless}}</div>
+                            {{/each}}
+                        </div>
+					{{/contentFor}}
+				{{/Layout}}
+			</div>
+		{{else}}
+			<p>There are no participants to chat with right now.</p>
+		{{/if}}
+		<br/>
+		{{> chatOptions}}
+	</div>
+</template>
+
+<template name="optionsFontSize">
+	<div class="dropdown">
+		<span {{messageFontSize}}>Chat Message Font Size: </span>
+		<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown">
+			Font Size ({{getInSession "messageFontSize"}})
+			<span class="caret"></span>
+		</button>
+
+		<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
+			<li role="presentation"><a class="fontSizeSelector" id="8" role="menuitem" tabindex="-1" href="#">8</a></li>
+			<li role="presentation"><a class="fontSizeSelector" id="10" role="menuitem" tabindex="-1" href="#">10</a></li>
+			<li role="presentation"><a class="fontSizeSelector" id="12" role="menuitem" tabindex="-1" href="#">12</a></li>
+			<li role="presentation"><a class="fontSizeSelector" id="14" role="menuitem" tabindex="-1" href="#">14</a></li>
+			<li role="presentation"><a class="fontSizeSelector" id="16" role="menuitem" tabindex="-1" href="#">16</a></li>
+			<li role="presentation"><a class="fontSizeSelector" id="18" role="menuitem" tabindex="-1" href="#">18</a></li>
+		</ul>
+	</div>
+</template>
+
+<!-- Display buttons on the chat tab, public, options, and all the private chat tabs -->
+<template name="tabButtons">
+  <ul id="tabsList" class="nav nav-tabs">
+    {{#each grabChatTabs}}
+      <li class="{{isTabActive userId}} tab {{makeSafe class}}">
+        <a href='#' data-toggle='tab' id="#{{makeSafe name}}" class="{{hasGotUnreadMailClass gotMail}} chatNameSelector">
+          {{makeSafe name}}
+          {{#if equals class "privateChatTab"}}
+            <button class="close closeTab" type="button"><sup><b>X</b></sup></button>
+          {{/if}}
+        </a>
+      </li>
+    {{/each}}
+  </ul>
+</template>
diff --git a/labs/meteor-client/app/client/views/layout.html b/labs/meteor-client/app/client/views/layout.html
new file mode 100755
index 0000000000000000000000000000000000000000..a666cb28a9a2330884a9a544c7dca45b7712e8f2
--- /dev/null
+++ b/labs/meteor-client/app/client/views/layout.html
@@ -0,0 +1,3 @@
+<template name="layout">
+	{{> yield}}
+</template>
diff --git a/labs/meteor-client/app/client/views/sharedTemplates.html b/labs/meteor-client/app/client/views/sharedTemplates.html
new file mode 100755
index 0000000000000000000000000000000000000000..33d44282691ef3531cbb4e58652e0db2b664fae4
--- /dev/null
+++ b/labs/meteor-client/app/client/views/sharedTemplates.html
@@ -0,0 +1,12 @@
+<template name="scrollWindow">
+    {{> yield}}
+    <div class="ScrollableWindow" id="{{id}}">
+        {{> yield region="scrollContents"}}
+    </div>
+</template>
+
+<template name="makeButton">
+   <button type="submit" id="{{id}}" class="{{btn_class}} btn" {{isDisabled}} rel="{{rel}}" data-placement="{{data_placement}}" title="{{title}}">
+      <i class="glyphicon glyphicon-{{i_class}}"></i>
+   </button>
+</template>
diff --git a/labs/meteor-client/app/client/views/users/user_item.coffee b/labs/meteor-client/app/client/views/users/user_item.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..24f33884fef64af9754012f0a27b5f2e730931c6
--- /dev/null
+++ b/labs/meteor-client/app/client/views/users/user_item.coffee
@@ -0,0 +1,11 @@
+Template.displayUserIcons.events
+  'click .muteIcon': (event) ->
+    toggleMic @
+
+  'click .raisedHandIcon': (event) ->
+    # the function to call 'userLowerHand'
+    # the meeting id
+    # the _id of the person whose land is to be lowered
+    # the userId of the person who is lowering the hand
+    console.log "lower hand- client click handler"
+    Meteor.call('userLowerHand', getInSession("meetingId"), @userId, getInSession("userId"), getInSession("authToken"))
diff --git a/labs/meteor-client/app/client/views/users/user_item.html b/labs/meteor-client/app/client/views/users/user_item.html
new file mode 100755
index 0000000000000000000000000000000000000000..d24f2208abc39a7ada8738398d4c4334b92281c6
--- /dev/null
+++ b/labs/meteor-client/app/client/views/users/user_item.html
@@ -0,0 +1,72 @@
+
+
+<template name="displayUserIcons">
+  {{#if isUserSharingVideo userId}}
+    <span class="userListSettingIcon glyphicon glyphicon-facetime-video" rel="tooltip" data-placement="bottom" title="{{user.name}} is sharing their webcam"></span>
+  {{/if}}
+
+  {{#if isUserSharingAudio userId}}
+    {{#if isCurrentUser userId}}
+      {{#if isUserMuted userId}}
+        <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-off" rel="tooltip" data-placement="bottom" title="Unmute yourself"></span>
+      {{else}}
+        {{#if isCurrentUserTalking}}
+          <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-up" rel="tooltip" data-placement="bottom" title="is talking"></span>
+        {{else}}
+          <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-down" rel="tooltip" data-placement="bottom" title="is not talking"></span>
+        {{/if}}
+      {{/if}}
+    {{else}}
+      {{#if isUserMuted userId}}
+        <span class="userListSettingIcon glyphicon glyphicon-volume-off" rel="tooltip" data-placement="bottom" title="{{user.name}} is muted"></span>
+      {{else}}
+        {{#if isUserTalking userId}}
+          <span class="userListSettingIcon glyphicon glyphicon-volume-up" rel="tooltip" data-placement="bottom" title="{{user.name}} is talking"></span>
+        {{else}}
+          <span class="userListSettingIcon glyphicon glyphicon-volume-down" rel="tooltip" data-placement="bottom" title="{{user.name}} is not talking"></span>
+        {{/if}}
+      {{/if}}
+    {{/if}}
+  {{/if}}
+
+  {{#if isUserListenOnly userId}}
+    <span class="userListSettingIcon glyphicon glyphicon-headphones" title="Listening only"></span>
+  {{/if}}
+
+  {{#if user.presenter}}
+    <span class="userListSettingIcon glyphicon glyphicon-picture" rel="tooltip" data-placement="bottom" title="{{user.name}} is the presenter"></span>
+  {{else}}
+    {{#if equals user.role "MODERATOR"}}
+      <span class="userListSettingIcon glyphicon glyphicon-user" rel="tooltip" data-placement="bottom" title="{{user.name}} is a moderator"></span>
+    {{/if}}
+  {{/if}}
+
+  {{#if user.raise_hand}}
+    {{#if isCurrentUser userId}}
+      <span class="raisedHandIcon userListSettingIcon glyphicon glyphicon-hand-up" rel="tooltip" data-placement="bottom" title="Lower your hand"></span>
+    {{else}}
+      <span class="userListSettingIcon glyphicon glyphicon-hand-up" rel="tooltip" data-placement="bottom" title="{{user.name}} has raised their hand"></span>
+    {{/if}}
+  {{/if}}
+</template>
+
+<template name="usernameEntry">
+  {{#if isCurrentUser userId}}
+    <span class="userCurrent userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}} (you)">
+      {{user.name}} (you)
+    </span>
+  {{else}}
+    <span class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}}">
+      {{user.name}}
+    </span>
+  {{/if}}
+</template>
+
+<template name="userItem">
+  <div id="username">
+    {{> usernameEntry}}
+  </div>
+  <div id="usericons">
+    {{> displayUserIcons}}
+  </div>
+</template>
diff --git a/labs/meteor-client/app/client/views/users/user_list.coffee b/labs/meteor-client/app/client/views/users/user_list.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..96d0fbcb2c217db88bb21c2d005db624d661d49a
--- /dev/null
+++ b/labs/meteor-client/app/client/views/users/user_list.coffee
@@ -0,0 +1,3 @@
+Template.usersList.helpers
+  getMeetingSize: -> # Retreieve the number of users in the chat, or "error" string
+    return Meteor.Users.find().count()
diff --git a/labs/meteor-client/app/client/views/users/users_list.html b/labs/meteor-client/app/client/views/users/users_list.html
new file mode 100755
index 0000000000000000000000000000000000000000..97db348ea756ae2a7fde0c00aaaea5bf42c6caa3
--- /dev/null
+++ b/labs/meteor-client/app/client/views/users/users_list.html
@@ -0,0 +1,19 @@
+<template name="usersList">
+	<div id="{{id}}" {{visibility name}} class="component">
+		<h3 class="title gradientBar"><span class="glyphicon glyphicon-user"></span> Participants: {{getMeetingSize}} User(s)</h3>
+
+		<div id="user-contents">
+			{{#Layout template="scrollWindow" id="publicUserScrollWindow"}}
+				{{#contentFor region="scrollContents"}}
+					<div class="userlist">
+						{{#each getUsersInMeeting}}
+							<div id="content">
+								{{>userItem}}
+							</div>
+						{{/each}}
+					</div>
+				{{/contentFor}}
+			{{/Layout}}
+		</div>
+	</div>
+</template>
diff --git a/labs/meteor-client/app/client/views/whiteboard/slide.coffee b/labs/meteor-client/app/client/views/whiteboard/slide.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..6100336af6ba108140f773cf82467d8f00203511
--- /dev/null
+++ b/labs/meteor-client/app/client/views/whiteboard/slide.coffee
@@ -0,0 +1,115 @@
+Template.slide.rendered = ->
+  currentSlide = getCurrentSlideDoc()
+  pic = new Image()
+  pic.onload = ->
+    setInSession 'slideOriginalWidth', this.width
+    setInSession 'slideOriginalHeight', this.height
+    $(window).resize( ->
+      redrawWhiteboard()
+    )
+    if window.matchMedia('(orientation: portrait)').matches
+      $('#whiteboard-paper').height($('#whiteboard-paper').width() * this.height / this.width)
+    else
+      # set the slide height to the max available
+      $('#whiteboard-paper').height($('#whiteboard').height())
+    if currentSlide?.slide?.png_uri?
+      createWhiteboardPaper (wpm) ->
+        displaySlide wpm
+  pic.src = currentSlide?.slide?.png_uri
+
+@createWhiteboardPaper = (callback) =>
+  @whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper')
+  callback(@whiteboardPaperModel)
+
+@displaySlide = (wpm) ->
+  currentSlide = getCurrentSlideDoc()
+  wpm.create()
+  adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
+  wpm._displayPage(currentSlide?.slide?.png_uri, getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
+  manuallyDisplayShapes()
+  wpm.scale(adjustedDimensions.width, adjustedDimensions.height)
+
+@manuallyDisplayShapes = ->
+  currentSlide = getCurrentSlideDoc()
+  wpm = @whiteboardPaperModel
+  shapes = Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).fetch()
+  for s in shapes
+    shapeInfo = s.shape?.shape or s?.shape
+    shapeType = shapeInfo?.type
+
+    if shapeType isnt "text"
+      len = shapeInfo.points.length
+      for num in [0..len] # the coordinates must be in the range 0 to 1
+        shapeInfo?.points[num] = shapeInfo?.points[num] / 100
+    wpm?.makeShape(shapeType, shapeInfo)
+    wpm?.updateShape(shapeType, shapeInfo)
+
+
+# calculates and returns the best fitting {width, height} pair
+# based on the image's original width and height
+@scaleSlide = (originalWidth, originalHeight) ->
+
+  # the size of the whiteboard space (frame) where the slide will be displayed
+  boardWidth = $("#whiteboard").width()
+  boardHeight = null # see under
+
+  # calculate boardHeight
+  whiteboardBottom = $("#whiteboard").offset().top + $("#whiteboard").height()
+  footerTop = $(".myFooter").offset().top
+  if footerTop < whiteboardBottom
+    boardHeight = footerTop - $("#whiteboard").offset().top - $("#whiteboard-navbar").height() - 10
+  else
+    boardHeight = $("#whiteboard").height() - $("#whiteboard-navbar").height() - 10
+
+  # this is the best fitting pair
+  adjustedWidth = null
+  adjustedHeight = null
+
+
+  # the slide image is in portrait orientation
+  if originalWidth <= originalHeight
+    adjustedWidth = boardHeight * originalWidth / originalHeight
+    if boardWidth < adjustedWidth
+      adjustedHeight = boardHeight * boardWidth / adjustedWidth
+      adjustedWidth = boardWidth
+    else
+      adjustedHeight = boardHeight
+
+  # ths slide image is in landscape orientation
+  else
+    adjustedHeight = boardWidth * originalHeight / originalWidth
+    if boardHeight < adjustedHeight
+      adjustedWidth = boardWidth * boardHeight / adjustedHeight
+      adjustedHeight = boardHeight
+    else
+      adjustedWidth = boardWidth
+
+  { width: adjustedWidth, height: adjustedHeight }
+
+Template.slide.helpers
+  updatePointerLocation: (pointer) ->
+    if whiteboardPaperModel?
+      wpm = whiteboardPaperModel
+      wpm?.moveCursor(pointer.x, pointer.y)
+
+#### SHAPE ####
+Template.shape.rendered = ->
+  # @data is the shape object coming from the {{#each}} in the html file
+  shapeInfo = @data.shape?.shape or @data.shape
+  shapeType = shapeInfo?.type
+
+  if shapeType isnt "text"
+    len = shapeInfo.points.length
+    for num in [0..len] # the coordinates must be in the range 0 to 1
+      shapeInfo.points[num] = shapeInfo.points[num] / 100
+
+  if whiteboardPaperModel?
+    wpm = whiteboardPaperModel
+    wpm?.makeShape(shapeType, shapeInfo)
+    wpm?.updateShape(shapeType, shapeInfo)
+
+Template.shape.destroyed = ->
+  if whiteboardPaperModel?
+    wpm = whiteboardPaperModel
+    wpm.clearShapes()
+    manuallyDisplayShapes()
diff --git a/labs/meteor-client/app/client/views/whiteboard/slide.html b/labs/meteor-client/app/client/views/whiteboard/slide.html
new file mode 100755
index 0000000000000000000000000000000000000000..8ad52c131d45192053337a18241fa95c3b66d823
--- /dev/null
+++ b/labs/meteor-client/app/client/views/whiteboard/slide.html
@@ -0,0 +1,9 @@
+<template name="slide">
+	{{#each getShapesForSlide}}
+		{{> shape}}
+	{{/each}}
+	{{updatePointerLocation pointerLocation}}
+</template>
+
+<template name="shape">
+</template>
diff --git a/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee b/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..4e801baf776b572e5d26122ee5ae0e1975e8d712
--- /dev/null
+++ b/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee
@@ -0,0 +1,12 @@
+@redrawWhiteboard = () ->
+  if window.matchMedia('(orientation: portrait)').matches
+    $('#whiteboard').height($('#whiteboard').width() * getInSession('slideOriginalHeight') / getInSession('slideOriginalWidth') + $('#whiteboard-navbar').height() + 10)
+  else if $('#whiteboard').height() isnt $('#users').height() + 10
+    $('#whiteboard').height($('#users').height() + 10)
+  adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight'))
+  wpm = whiteboardPaperModel
+  wpm.clearShapes()
+  wpm.clearCursor()
+  manuallyDisplayShapes()
+  wpm.scale(adjustedDimensions.width, adjustedDimensions.height)
+  wpm.createCursor()
diff --git a/labs/meteor-client/app/client/views/whiteboard/whiteboard.html b/labs/meteor-client/app/client/views/whiteboard/whiteboard.html
new file mode 100755
index 0000000000000000000000000000000000000000..e680882df3d628adfcd2f781b06361c636376ce1
--- /dev/null
+++ b/labs/meteor-client/app/client/views/whiteboard/whiteboard.html
@@ -0,0 +1,13 @@
+<template name="whiteboard">
+	<div id="{{id}}" {{visibility name}} class="component gradientBar">
+		<h3 id="whiteboard-navbar" class="title">
+			<span class="glyphicon glyphicon-pencil"></span>
+			{{title}}
+		</h3>
+		{{#each getCurrentSlide}}
+			{{> slide}}
+		{{/each}}
+		<div id="whiteboard-paper">
+		</div>
+	</div>
+</template>
diff --git a/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee b/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..eb16119a91d5849f53c235bc5bcc3ef8efee7ffd
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee
@@ -0,0 +1,26 @@
+# A base class for whiteboard tools
+class @WhiteboardToolModel
+
+  initialize: (@paper) ->
+    console.log "paper:" + @paper
+    @gh = 0
+    @gw = 0
+    @obj = 0
+    # the defintion of this shape, kept so we can redraw the shape whenever needed
+    @definition = []
+
+  #set the size of the paper
+  # @param  {number} @gh    gh parameter
+  # @param  {number} @gw    gw parameter
+  setPaperSize: (@gh, @gw) ->
+
+  setOffsets: (@xOffset, @yOffset) ->
+
+  setPaperDimensions: (@paperWidth, @paperHeight) ->
+    # TODO: can't we simply take the width and the height from `@paper`?
+
+  getDefinition: () ->
+    @definition
+
+  hide: () ->
+    @obj.hide() if @obj?
diff --git a/labs/meteor-client/app/client/whiteboard_models/utils.coffee b/labs/meteor-client/app/client/whiteboard_models/utils.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..b79c4c4f105ad28bd4aa6593f69f27577b0c48a2
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/utils.coffee
@@ -0,0 +1,25 @@
+# General utility methods
+
+Meteor.methods
+  # POST request using javascript
+  # @param  {string} path   path of submission
+  # @param  {string} params parameters to submit
+  # @param  {string} method method of submission ("post" is default)
+  # @return {undefined}
+  postToUrl: (path, params, method) ->
+    method = method or "post"
+    form = $("<form></form>")
+    form.attr
+      "method" : method,
+      "action" : path
+    for key of params
+      if params.hasOwnProperty(key)
+        $hiddenField = $ "input"
+        $hiddenField.attr
+          "type" : "hidden",
+          "name" : key,
+          "value" : params[key]
+        form.append $hiddenField
+
+    $('body').append form
+    form.submit()
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..429a73c0b7e4832be5e823c3483174b06c01d8f6
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee
@@ -0,0 +1,43 @@
+# The cursor/pointer in the whiteboard
+class @WhiteboardCursorModel
+
+  constructor: (@paper, @radius=null, @color=null) ->
+    @radius ?= 6
+    @color ?= "#ff6666" # a pinkish red
+    @cursor = null
+    @paper
+
+  draw: () =>
+    @cursor = @paper.circle(0, 0, @radius)
+    @cursor.attr
+      "fill": @color
+      "stroke": @color
+      "opacity": "0.8"
+    $(@cursor.node).on "mousewheel", _.bind(@_onMouseWheel, @)
+
+  toFront: () ->
+    @cursor.toFront() if @cursor?
+
+  setRadius: (value) ->
+    if @cursor?
+      @cursor.attr r: value
+
+  setPosition: (x, y) ->
+    if @cursor?
+      @cursor.attr
+        cx: x
+        cy: y
+
+  undrag: () ->
+    @cursor.undrag() if @cursor?
+
+  drag: (onMove, onStart, onEnd, target=null) ->
+    if @cursor?
+      target or= @
+      @cursor.drag _.bind(onMove, target), _.bind(onStart, target), _.bind(onEnd, target)
+
+  _onMouseWheel: (e, delta) ->
+    @trigger("cursor:mousewheel", e, delta)
+
+  remove: () ->
+    @cursor.remove()
\ No newline at end of file
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..adaa7cfa23e3b644feec467d2fa1c3832ed4f13b
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee
@@ -0,0 +1,151 @@
+# An ellipse in the whiteboard
+class @WhiteboardEllipseModel extends WhiteboardToolModel
+
+    constructor: (@paper) ->
+      super @paper
+
+      # the defintion of this shape, kept so we can redraw the shape whenever needed
+      # format: top left x, top left y, bottom right x, bottom right y, stroke color, thickness
+      @definition = [0, 0, 0, 0, "#000", "0px"]
+
+    # Make an ellipse on the whiteboard
+    # @param    {[type]} x                 the x value of the top left corner
+    # @param    {[type]} y                 the y value of the top left corner
+    # @param    {string} colour        the colour of the object
+    # @param    {number} thickness the thickness of the object's line(s)
+    make: (info) ->
+      #console.log "Whiteboard - Making ellipse: "
+      #console.log info
+      if info?.points?
+        x = info.points[0]
+        y = info.points[1]
+        color = info.color
+        thickness = info.thickness
+
+        @obj = @paper.ellipse(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0)
+        @obj.attr "stroke", formatColor(color)
+        @obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
+        @definition = [x, y, y, x, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
+
+      @obj
+
+    # Update ellipse drawn
+    # @param    {number} x1 the x value of the top left corner in percent of current slide size
+    # @param    {number} y1 the y value of the top left corner in percent of current slide size
+    # @param    {number} x2 the x value of the bottom right corner in percent of current slide size
+    # @param    {number} y2 the y value of the bottom right corner in percent of current slide size
+    # @param    {boolean} square (draw a circle or not
+    update: (info) ->
+      #console.log info
+      if info?.points?
+        x1 = info.points[0]
+        y1 = info.points[1]
+        x2 = info.points[2]
+        y2 = info.points[3]
+
+        circle = info.square
+
+        if @obj?
+          [x1, x2] = [x2, x1] if x2 < x1
+
+          if y2 < y1
+            [y1, y2] = [y2, y1]
+            reversed = true
+
+          #if the control key is pressed then the width and height of the ellipse are equal (a circle)
+          #we calculate this by making the y2 coord equal to the y1 coord plus the width of x2-x1 and corrected for the slide size
+          if circle
+            if reversed # if reveresed, the y1 coordinate gets updated, not the y2 coordinate
+              y1 = y2 - (x2 - x1) * @gw / @gh
+            else
+              y2 = y1 + (x2 - x1) * @gw / @gh
+
+          coords =
+            x1: x1
+            x2: x2
+            y1: y1
+            y2: y2
+
+          #console.log(coords)
+
+          rx = (x2 - x1) / 2
+          ry = (y2 - y1) / 2
+
+          r =
+            rx: rx * @gw
+            ry: ry * @gh
+            cx: (rx + x1) * @gw + @xOffset
+            cy: (ry + y1) * @gh + @yOffset
+
+          @obj.attr(r)
+
+          #console.log( "@gw: " + @gw + "\n@gh: " + @gh + "\n@xOffset: " + @xOffset + "\n@yOffset: " + @yOffset );
+          # we need to update all these values, specially for when shapes are drawn backwards
+          @definition[0] = x1
+          @definition[1] = y1
+          @definition[2] = x2
+          @definition[3] = y2
+
+    # Draw an ellipse on the whiteboard
+    # @param    {number} x1 the x value of the top left corner
+    # @param    {number} y1 the y value of the top left corner
+    # @param    {number} x2 the x value of the bottom right corner
+    # @param    {number} y2 the y value of the bottom right corner
+    # @param    {string} colour        the colour of the object
+    # @param    {number} thickness the thickness of the object's line(s)
+    draw: (x1, y1, x2, y2, colour, thickness) ->
+        [x1, x2] = [x2, x1] if x2 < x1
+        [y1, y2] = [y2, y1] if y2 < y1
+
+        rx = (x2 - x1) / 2
+        ry = (y2 - y1) / 2
+        x = (rx + x1) * @gw + @xOffset
+        y = (ry + y1) * @gh + @yOffset
+        elip = @paper.ellipse(x, y, rx * @gw, ry * @gh)
+        elip.attr Utils.strokeAndThickness(colour, thickness)
+        elip
+
+    # When first starting drawing the ellipse
+    # @param    {number} x the x value of cursor at the time in relation to the left side of the browser
+    # @param    {number} y the y value of cursor at the time in relation to the top of the browser
+    # TODO: moved here but not finished
+    dragOnStart: (x, y) ->
+        # sx = (@paperWidth - @gw) / 2
+        # sy = (@paperHeight - @gh) / 2
+        # # find the x and y values in relation to the whiteboard
+        # @ellipseX = (x - @containerOffsetLeft - sx + @xOffset)
+        # @ellipseY = (y - @containerOffsetTop - sy + @yOffset)
+        # globals.connection.emitMakeShape "ellipse",
+        #     [ @ellipseX / @paperWidth, @ellipseY / @paperHeight, @currentColour, @currentThickness ]
+
+    # When first starting to draw an ellipse
+    # @param    {number} dx the difference in the x value at the start as opposed to the x value now
+    # @param    {number} dy the difference in the y value at the start as opposed to the y value now
+    # @param    {number} x the x value of cursor at the time in relation to the left side of the browser
+    # @param    {number} y the y value of cursor at the time in relation to the top of the browser
+    # @param    {Event} e     the mouse event
+    # TODO: moved here but not finished
+    dragOnMove: (dx, dy, x, y, e) ->
+        # # if shift is pressed, draw a circle instead of ellipse
+        # dy = dx if @shiftPressed
+        # dx = dx / 2
+        # dy = dy / 2
+        # # adjust for negative values as well
+        # x = @ellipseX + dx
+        # y = @ellipseY + dy
+        # dx = (if dx < 0 then -dx else dx)
+        # dy = (if dy < 0 then -dy else dy)
+        # globals.connection.emitUpdateShape "ellipse",
+        #     [ x / @paperWidth, y / @paperHeight, dx / @paperWidth, dy / @paperHeight ]
+
+    # When releasing the mouse after drawing the ellipse
+    # @param    {Event} e the mouse event
+    # TODO: moved here but not finished
+    dragOnStop: (e) ->
+        # attrs = undefined
+        # attrs = @currentEllipse.attrs if @currentEllipse?
+        # if attrs?
+        #     globals.connection.emitPublishShape "ellipse",
+        #         [ attrs.cx / @gw, attrs.cy / @gh, attrs.rx / @gw, attrs.ry / @gh,
+        #             @currentColour, @currentThickness ]
+        # @currentEllipse = null # late updates will be blocked by this
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..54f7246e47162c674da193600f3e77c2e8262f22
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee
@@ -0,0 +1,192 @@
+MAX_PATHS_IN_SEQUENCE = 30
+
+# A line in the whiteboard
+# Note: is used to draw lines from the pencil tool and from the line tool, this is why some
+# methods can receive different set of parameters.
+# TODO: Maybe this should be split in WhiteboardPathModel for the pencil and
+#       WhiteboardLineModel for the line tool
+class @WhiteboardLineModel extends WhiteboardToolModel
+
+  constructor: (@paper) ->
+    super @paper
+
+    # the defintion of this shape, kept so we can redraw the shape whenever needed
+    # format: svg path, stroke color, thickness
+    @definition = ["", "#000", "0px"]
+
+  # Creates a line in the paper
+  # @param  {number} x         the x value of the line start point as a percentage of the original width
+  # @param  {number} y         the y value of the line start point as a percentage of the original height
+  # @param  {string} colour    the colour of the shape to be drawn
+  # @param  {number} thickness the thickness of the line to be drawn
+  make: (info) ->
+
+    if info?.points?
+      x = info.points[0]
+      y = info.points[1]
+      color = info.color
+      thickness = info.thickness
+
+      x1 = x * @gw + @xOffset
+      y1 = y * @gh + @yOffset
+      path = "M" + x1 + " " + y1 + " L" + x1 + " " + y1
+      pathPercent = "M" + x + " " + y + " L" + x + " " + y
+      @obj = @paper.path(path)
+      @obj.attr "stroke", formatColor(color)
+      @obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
+      @obj.attr({"stroke-linejoin": "round"})
+      @obj.attr "stroke-linecap", "round"
+
+      @definition = [pathPercent, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
+
+    @obj
+
+  # Update the line dimensions
+  # @param  {number}  x1         1) the x of the first point
+  #                              2) the next x point to be added to the line
+  # @param  {number}  y1         1) the y of the first point
+  #                              2) the next y point to be added to the line
+  # @param  {number,boolean} x2  1) the x of the second point
+  #                              2) true if the line should be added to the current line,
+  #                                 false if it should replace the last point
+  # @param  {number}         y2  1) the y of the second point
+  #                              2) undefined
+  update: (info) ->
+
+    if info?.points?
+      x1 = info.points[0]
+      y1 = info.points[1]
+      x2 = info.points[2]
+      y2 = info.points[3]
+
+      if @obj?
+        path = @_buildPath(info.points)
+
+        @definition[0] = path
+
+        path = @_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset)
+        @obj.attr path: path
+
+  # Draw a line on the paper
+  # @param  {number,string} x1 1) the x value of the first point
+  #                            2) the string path
+  # @param  {number,string} y1 1) the y value of the first point
+  #                            2) the colour
+  # @param  {number} x2        1) the x value of the second point
+  #                            2) the thickness
+  # @param  {number} y2        1) the y value of the second point
+  #                            2) undefined
+  # @param  {string} colour    1) the colour of the shape to be drawn
+  #                            2) undefined
+  # @param  {number} thickness 1) the thickness of the line to be drawn
+  #                            2) undefined
+  draw: (x1, y1, x2, y2, colour, thickness) ->
+
+    # if the drawing is from the pencil tool, it comes as a path first
+    # if _.isString(x1)
+    #   colour = y1
+    #   thickness = x2
+    #   path = x1
+
+    # # if the drawing is from the line tool, it comes with two points
+    # else
+    #   path = @_buildPath(points)
+
+    # line = @paper.path(@_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset))
+    # line.attr Utils.strokeAndThickness(colour, thickness)
+    # line.attr({"stroke-linejoin": "round"})
+    # line
+
+  # When dragging for drawing lines starts
+  # @param  {number} x the x value of the cursor
+  # @param  {number} y the y value of the cursor
+  # TODO: moved here but not finished
+  dragOnStart: (x, y) ->
+    # # find the x and y values in relation to the whiteboard
+    # sx = (@paperWidth - @gw) / 2
+    # sy = (@paperHeight - @gh) / 2
+    # @lineX = x - @containerOffsetLeft - sx + @xOffset
+    # @lineY = y - @containerOffsetTop - sy + @yOffset
+    # values = [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ]
+    # globals.connection.emitMakeShape "line", values
+
+  # As line drawing drag continues
+  # @param  {number} dx the difference between the x value from _lineDragStart and now
+  # @param  {number} dy the difference between the y value from _lineDragStart and now
+  # @param  {number} x  the x value of the cursor
+  # @param  {number} y  the y value of the cursor
+  # TODO: moved here but not finished
+  dragOnMove: (dx, dy, x, y) ->
+    # sx = (@paperWidth - @gw) / 2
+    # sy = (@paperHeight - @gh) / 2
+    # [cx, cy] = @_currentSlideOffsets()
+    # # find the x and y values in relation to the whiteboard
+    # @cx2 = x - @containerOffsetLeft - sx + @xOffset
+    # @cy2 = y - @containerOffsetTop - sy + @yOffset
+    # if @shiftPressed
+    #   globals.connection.emitUpdateShape "line", [ @cx2 / @paperWidth, @cy2 / @paperHeight, false ]
+    # else
+    #   @currentPathCount++
+    #   if @currentPathCount < MAX_PATHS_IN_SEQUENCE
+    #     globals.connection.emitUpdateShape "line", [ @cx2 / @paperHeight, @cy2 / @paperHeight, true ]
+    #   else if @obj?
+    #     @currentPathCount = 0
+    #     # save the last path of the line
+    #     @obj.attrs.path.pop()
+    #     path = @obj.attrs.path.join(" ")
+    #     @obj.attr path: (path + "L" + @lineX + " " + @lineY)
+
+    #     # scale the path appropriately before sending
+    #     pathStr = @obj.attrs.path.join(",")
+    #     globals.connection.emitPublishShape "path",
+    #       [ @_scaleLinePath(pathStr, 1 / @gw, 1 / @gh),
+    #         @currentColour, @currentThickness ]
+    #     globals.connection.emitMakeShape "line",
+    #       [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ]
+    #   @lineX = @cx2
+    #   @lineY = @cy2
+
+  # Drawing line has ended
+  # @param  {Event} e the mouse event
+  # TODO: moved here but not finished
+  dragOnEnd: (e) ->
+    # if @obj?
+    #   path = @obj.attrs.path
+    #   @obj = null # any late updates will be blocked by this
+    #   # scale the path appropriately before sending
+    #   globals.connection.emitPublishShape "path",
+    #     [ @_scaleLinePath(path.join(","), 1 / @gw, 1 / @gh),
+    #       @currentColour, @currentThickness ]
+
+  _buildPath: (points) ->
+    path = ""
+
+    if points and points.length >= 2
+      path += "M #{points[0]} #{points[1]}"
+      i = 2
+
+      while i < points.length
+        path += "L#{points[i]} #{points[i + 1]}"
+        i += 2
+
+      path += "Z"
+      path
+
+  # Scales a path string to fit within a width and height of the new paper size
+  # @param  {number} w width of the shape as a percentage of the original width
+  # @param  {number} h height of the shape as a percentage of the original height
+  # @return {string}   the path string after being manipulated to new paper size
+  _scaleLinePath: (string, w, h, xOffset=0, yOffset=0) ->
+    path = null
+    points = string.match(/(\d+[.]?\d*)/g)
+    len = points.length
+    j = 0
+
+    # go through each point and multiply it by the new height and width
+    while j < len
+      if j isnt 0
+        path += "L" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
+      else
+        path = "M" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
+      j += 2
+    path
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..d1addb56a438aa4d89fdbb2102a5e98e3af64523
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee
@@ -0,0 +1,346 @@
+# "Paper" which is the Raphael term for the entire SVG object on the webpage.
+# This class deals with this SVG component only.
+class Meteor.WhiteboardPaperModel
+
+  # Container must be a DOM element
+  constructor: (@container) ->
+    # a WhiteboardCursorModel
+    @cursor = null
+
+    # all slides in the presentation indexed by url
+    @slides = {}
+
+    @panX = null
+    @panY = null
+
+    @current = {}
+
+    # the slide being shown
+    @current.slide = null
+
+    # a raphaeljs set with all the shapes in the current slide
+    @current.shapes = null
+    # a list of shapes as passed to this client when it receives `all_slides`
+    # (se we are able to redraw the shapes whenever needed)
+    @current.shapeDefinitions = []
+
+    @zoomLevel = 1
+    @shiftPressed = false
+    @currentPathCount = 0
+
+    @_updateContainerDimensions()
+
+    @zoomObserver = null
+
+    @adjustedWidth = 0
+    @adjustedHeight = 0
+
+    @widthRatio = 100
+    @heightRatio = 100
+
+  # Initializes the paper in the page.
+  # Can't do these things in initialize() because by then some elements
+  # are not yet created in the page.
+  create: ->
+    # paper is embedded within the div#slide of the page.
+    # @raphaelObj ?= ScaleRaphael(@container, "900", "500")
+
+    h = $("#"+@container).height()
+    w = $("#"+@container).width()
+
+    @raphaelObj ?= ScaleRaphael(@container, w, h)
+    @raphaelObj ?= ScaleRaphael(@container, $container.innerHeight(), $container.innerWidth())
+
+    @raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
+
+    @createCursor()
+
+    if @slides
+      @rebuild()
+    else
+      @slides = {} # if previously loaded
+    unless navigator.userAgent.indexOf("Firefox") is -1
+      @raphaelObj.renderfix()
+
+    @raphaelObj
+
+  # Re-add the images to the paper that are found
+  # in the slides array (an object of urls and dimensions).
+  rebuild: ->
+    @current.slide = null
+    for url of @slides
+      if @slides.hasOwnProperty(url)
+        @addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight()
+
+  scale: (width, height) ->
+    @raphaelObj?.changeSize(width, height)
+
+  # Add an image to the paper.
+  # @param {string} url the URL of the image to add to the paper
+  # @param {number} width   the width of the image (in pixels)
+  # @param {number} height   the height of the image (in pixels)
+  # @return {Raphael.image} the image object added to the whiteboard
+  addImageToPaper: (url, width, height) ->
+    @_updateContainerDimensions()
+
+    # solve for the ratio of what length is going to fit more than the other
+    max = Math.max(width / @containerWidth, height / @containerHeight)
+    # fit it all in appropriately
+    url = @_slideUrl(url)
+    sw = width / max
+    sh = height / max
+    #cx = (@containerWidth / 2) - (width / 2)
+    #cy = (@containerHeight / 2) - (height / 2)
+    img = @raphaelObj.image(url, cx = 0, cy = 0, width, height)
+
+    # sw slide width as percentage of original width of paper
+    # sh slide height as a percentage of original height of paper
+    # x-offset from top left corner as percentage of original width of paper
+    # y-offset from top left corner as percentage of original height of paper
+    @slides[url] = new WhiteboardSlideModel(img.id, url, img, width, height, sw, sh, cx, cy)
+
+    unless @current.slide?
+      img.toBack()
+      @current.slide = @slides[url]
+    else if @current.slide.url is url
+      img.toBack()
+    else
+      img.hide()
+
+    # TODO: other places might also required an update in these dimensions
+    @_updateContainerDimensions()
+
+    @_updateZoomRatios()
+    @cursor.setRadius(6 * @widthRatio / 100)
+
+    img
+
+  # Removes all the images from the Raphael paper.
+  removeAllImagesFromPaper: ->
+    for url of @slides
+      if @slides.hasOwnProperty(url)
+        @raphaelObj.getById(@slides[url]?.getId())?.remove()
+        #@trigger('paper:image:removed', @slides[url].getId()) # Removes the previous image preventing images from being redrawn over each other repeatedly
+    @slides = {}
+    @current.slide = null
+
+
+  # Switches the tool and thus the functions that get
+  # called when certain events are fired from Raphael.
+  # @param  {string} tool the tool to turn on
+  # @return {undefined}
+  setCurrentTool: (tool) ->
+    @currentTool = tool
+    console.log "setting current tool to", tool
+    switch tool
+      when "line"
+        @cursor.undrag()
+        @current.line = @_createTool(tool)
+        @cursor.drag(@current.line.dragOnMove, @current.line.dragOnStart, @current.line.dragOnEnd)
+      when "rectangle"
+        @cursor.undrag()
+        @current.rectangle = @_createTool(tool)
+        @cursor.drag(@current.rectangle.dragOnMove, @current.rectangle.dragOnStart, @current.rectangle.dragOnEnd)
+      else
+        console.log "ERROR: Cannot set invalid tool:", tool
+
+  # Clear all shapes from this paper.
+  clearShapes: ->
+    if @current.shapes?
+      @current.shapes.forEach (element) ->
+        element.remove()
+      @currentShapes = []
+      @currentShapesDefinitions = []
+    @clearCursor()
+    @createCursor()
+
+  clearCursor: ->
+    @cursor?.remove()
+
+  createCursor: ->
+    @cursor = new WhiteboardCursorModel(@raphaelObj)
+    @cursor.setRadius(6 * @widthRatio / 100)
+    @cursor.draw()
+
+  # Updated a shape `shape` with the data in `data`.
+  # TODO: check if the objects exist before calling update, if they don't they should be created
+  updateShape: (shape, data) ->
+    @current[shape].update(data)
+
+  # Make a shape `shape` with the data in `data`.
+  makeShape: (shape, data) ->
+    data.thickness *= @adjustedWidth / 1000
+
+    tool = null
+
+    @current[shape] = @_createTool(shape)
+    toolModel = @current[shape]
+    tool = @current[shape].make(data)
+
+    if tool?
+      @current.shapes ?= @raphaelObj.set()
+      @current.shapes.push(tool)
+      @current.shapeDefinitions.push(toolModel.getDefinition())
+
+  # Update the cursor position on screen
+  # @param  {number} x the x value of the cursor as a percentage of the width
+  # @param  {number} y the y value of the cursor as a percentage of the height
+  moveCursor: (x, y) ->
+    [cx, cy] = @_currentSlideOffsets()
+    [slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
+    @cursor.setPosition(x * slideWidth + cx, y * slideHeight + cy)
+
+    #if the slide is zoomed in then move the cursor based on where the viewBox is looking
+    if @viewBoxXpos? && @viewBoxYPos?  && @viewBoxWidth? && @viewBoxHeight?
+      @cursor.setPosition( @viewBoxXpos + x * @viewBoxWidth, @viewBoxYPos + y * @viewBoxHeight )
+
+  zoomAndPan: (widthRatio, heightRatio, xOffset, yOffset) ->
+    console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}"
+    newX = - xOffset * 2 * @adjustedWidth / 100
+    newY = - yOffset * 2 * @adjustedHeight / 100
+    newWidth = @adjustedWidth * widthRatio / 100
+    newHeight = @adjustedHeight * heightRatio / 100
+    @raphaelObj.setViewBox(newX, newY, newWidth, newHeight) # zooms and pans
+
+  setAdjustedDimensions: (width, height) ->
+    @adjustedWidth = width
+    @adjustedHeight = height
+
+  # Update the dimensions of the container.
+  _updateContainerDimensions: ->
+    #console.log "update Container Dimensions"
+
+    $container = $('#whiteboard-paper')
+    @containerWidth = $container.innerWidth()
+    @containerHeight = $container.innerHeight()
+
+    @containerOffsetLeft = $container.offset()?.left
+    @containerOffsetTop = $container.offset()?.top
+
+  _updateZoomRatios: ->
+    currentSlideDoc = getCurrentSlideDoc()
+    @widthRatio = currentSlideDoc.slide.width_ratio
+    @heightRatio = currentSlideDoc.slide.height_ratio
+
+  # Retrieves an image element from the paper.
+  # The url must be in the slides array.
+  # @param  {string} url        the url of the image (must be in slides array)
+  # @return {Raphael.image}     return the image or null if not found
+  _getImageFromPaper: (url) ->
+    if @slides[url]
+      id = @slides[url].getId()
+      return @raphaelObj.getById(id) if id?
+    null
+
+  _currentSlideDimensions: ->
+    if @current.slide? then @current.slide.getDimensions() else [0, 0]
+
+  _currentSlideOriginalDimensions: ->
+    if @current.slide? then @current.slide.getOriginalDimensions() else [0, 0]
+
+  _currentSlideOffsets: ->
+    if @current.slide? then @current.slide.getOffsets() else [0, 0]
+
+  # Wrapper method to create a tool for the whiteboard
+  _createTool: (type) ->
+    switch type
+      when "pencil"
+        model = WhiteboardLineModel
+      when "path", "line"
+        model = WhiteboardLineModel
+      when "rectangle"
+        model = WhiteboardRectModel
+      when "ellipse"
+        model = WhiteboardEllipseModel
+      when "triangle"
+        model = WhiteboardTriangleModel
+      when "text"
+        model = WhiteboardTextModel
+
+    if model?
+      [slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
+      [xOffset, yOffset] = @_currentSlideOffsets()
+      [width, height] = @_currentSlideDimensions()
+
+      tool = new model(@raphaelObj)
+      # TODO: why are the parameters inverted and it works?
+      tool.setPaperSize(slideHeight, slideWidth)
+      tool.setOffsets(xOffset, yOffset)
+      tool.setPaperDimensions(width,height)
+      tool
+    else
+      null
+
+  # Adds the base url (the protocol+server part) to `url` if needed.
+  _slideUrl: (url) ->
+    if url?.match(/http[s]?:/)
+      url
+    else
+      console.log "The url '#{url}'' did not match the expected format of: http/s"
+      #globals.presentationServer + url
+
+  #Changes the currently displayed page/slide (if any) with this one
+  #@param {data} message object containing the "presentation" object
+  _displayPage: (data, originalWidth, originalHeight) ->
+    @removeAllImagesFromPaper()
+
+    @_updateContainerDimensions()
+    boardWidth = @containerWidth
+    boardHeight = @containerHeight
+
+    currentSlide = getCurrentSlideDoc()
+
+    # TODO currentSlide undefined in some cases - will check later why
+    imageWidth = boardWidth * (currentSlide?.slide.width_ratio/100) or boardWidth
+    imageHeight = boardHeight * (currentSlide?.slide.height_ratio/100) or boardHeight
+
+    # console.log "xBegin: #{xBegin}"
+    # console.log "xEnd: #{xEnd}"
+    # console.log "yBegin: #{yBegin}"
+    # console.log "yEnd: #{yEnd}"
+    # console.log "boardWidth: #{boardWidth}"
+    # console.log "boardHeight: #{boardHeight}"
+    #console.log "imageWidth: #{imageWidth}"
+    #console.log "imageHeight: #{imageHeight}"
+
+    currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
+    presentationId = currentPresentation?.presentation?.id
+    currentSlideCursor = Meteor.Slides.find({"presentationId": presentationId, "slide.current": true})
+
+    if @zoomObserver isnt null
+      @zoomObserver.stop()
+    _this = this
+    @zoomObserver = currentSlideCursor.observe # watching the current slide changes
+      changed: (newDoc, oldDoc) ->
+        if originalWidth <= originalHeight
+          @adjustedWidth = boardHeight * originalWidth / originalHeight
+          @adjustedHeight = boardHeight
+        else
+          @adjustedHeight = boardWidth * originalHeight / originalWidth
+          @adjustedWidth = boardWidth
+
+        _this.zoomAndPan(newDoc.slide.width_ratio, newDoc.slide.height_ratio,
+          newDoc.slide.x_offset, newDoc.slide.y_offset)
+
+        oldRatio = (oldDoc.slide.width_ratio + oldDoc.slide.height_ratio) / 2
+        newRatio = (newDoc.slide.width_ratio + newDoc.slide.height_ratio) / 2
+
+        _this?.current?.shapes?.forEach (shape) ->
+          shape.attr "stroke-width", shape.attr('stroke-width') * oldRatio  / newRatio
+
+        _this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100)
+
+    if originalWidth <= originalHeight
+      # square => boardHeight is the shortest side
+      @adjustedWidth = boardHeight * originalWidth / originalHeight
+      $('#whiteboard-paper').width(@adjustedWidth)
+      @addImageToPaper(data, @adjustedWidth, boardHeight)
+      @adjustedHeight = boardHeight
+    else
+      @adjustedHeight = boardWidth * originalHeight / originalWidth
+      $('#whiteboard-paper').height(@adjustedHeight)
+      @addImageToPaper(data, boardWidth, @adjustedHeight)
+      @adjustedWidth = boardWidth
+
+    @zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio,
+      currentSlide.slide.x_offset, currentSlide.slide.y_offset)
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..90cb4ed2a940b3d8ed7606caac83ce594074b2d2
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee
@@ -0,0 +1,145 @@
+# A rectangle in the whiteboard
+class @WhiteboardRectModel extends WhiteboardToolModel
+  constructor: (@paper) ->
+    super @paper
+
+    # the defintion of this shape, kept so we can redraw the shape whenever needed
+    # format: x1, y1, x2, y2, stroke color, thickness
+    @definition = [0, 0, 0, 0, "#000", "0px"]
+    @paper
+
+  # Creates a rectangle in the paper
+  # @param  {number} x         the x value of the top left corner
+  # @param  {number} y         the y value of the top left corner
+  # @param  {string} colour    the colour of the object
+  # @param  {number} thickness the thickness of the object's line(s)
+  make: (startingData) =>
+    x = startingData.points[0]
+    y = startingData.points[1]
+    color = startingData.color
+    thickness = startingData.thickness
+
+    @obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1)
+    @obj.attr "stroke", formatColor(color)
+    @obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
+    @definition =
+      shape: "rect"
+      data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
+    @obj
+
+  # Update the rectangle dimensions
+  # @param  {number} x1 the x value of the top left corner
+  # @param  {number} y1 the y value of the top left corner
+  # @param  {number} x2 the x value of the bottom right corner
+  # @param  {number} y2 the y value of the bottom right corner
+  # @param  {boolean} square (draw a square or not)
+  update: (startingData) ->
+
+    x1 = startingData.points[0]
+    y1 = startingData.points[1]
+    x2 = startingData.points[2]
+    y2 = startingData.points[3]
+
+    square = startingData.square
+    if @obj?
+      [x1, x2] = [x2, x1] if x2 < x1
+
+      if y2 < y1
+          [y1, y2] = [y2, y1]
+          reversed = true
+
+      if square
+          if reversed #if reveresed, the y1 coordinate gets updated, not the y2 coordinate
+              y1 = y2 - (x2 - x1) * @gw / @gh
+          else
+              y2 = y1 + (x2 - x1) * @gw / @gh
+
+      x = x1 * @gw + @xOffset
+      y = y1 * @gh + @yOffset
+      width = (x2 * @gw + @xOffset) - x
+      height = (y2 * @gh + @yOffset) - y
+      #if !square
+      @obj.attr
+        x: x
+        y: y
+        width: width
+        height: height
+      ###else
+        @obj.attr
+          x: x
+          y: y
+          width: width
+          height: width###
+
+      # we need to update all these values, specially for when shapes are drawn backwards
+      @definition.data[0] = x1
+      @definition.data[1] = y1
+      @definition.data[2] = x2
+      @definition.data[3] = y2
+
+  # Draw a rectangle on the paper
+  # @param  {number} x1        the x value of the top left corner
+  # @param  {number} y1        the y value of the top left corner
+  # @param  {number} x2        the x value of the bottom right corner
+  # @param  {number} y2        the y value of the bottom right corner
+  # @param  {string} colour    the colour of the object
+  # @param  {number} thickness the thickness of the object's line(s)
+  draw: (x1, y1, x2, y2, colour, thickness) ->
+    [x1, x2] = [x2, x1] if x2 < x1
+    [y1, y2] = [y2, y1] if y2 < y1
+
+    x = x1 * @gw
+    y = y1 * @gh
+    r = @paper.rect(x + @xOffset, y + @yOffset, (x2 * @gw) - x, (y2 * @gh) - y, 1)
+    r.attr Meteor.call("strokeAndThickness", colour, thickness)
+    r
+
+  # Creating a rectangle has started
+  # @param  {number} x the x value of cursor at the time in relation to the left side of the browser
+  # @param  {number} y the y value of cursor at the time in relation to the top of the browser
+  # TODO: moved here but not finished
+  dragOnStart: (x, y) ->
+    # sx = (@paperWidth - @gw) / 2
+    # sy = (@paperHeight - @gh) / 2
+    # # find the x and y values in relation to the whiteboard
+    # @cx2 = (x - @containerOffsetLeft - sx + @xOffset) / @paperWidth
+    # @cy2 = (y - @containerOffsetTop - sy + @yOffset) / @paperHeight
+    # globals.connection.emitMakeShape "rect",
+    #   [ @cx2, @cy2, @currentColour, @currentThickness ]
+
+  # Adjusting rectangle continues
+  # @param  {number} dx the difference in the x value at the start as opposed to the x value now
+  # @param  {number} dy the difference in the y value at the start as opposed to the y value now
+  # @param  {number} x the x value of cursor at the time in relation to the left side of the browser
+  # @param  {number} y the y value of cursor at the time in relation to the top of the browser
+  # @param  {Event} e  the mouse event
+  # TODO: moved here but not finished
+  dragOnMove: (dx, dy, x, y, e) ->
+    # # if shift is pressed, make it a square
+    # dy = dx if @shiftPressed
+    # dx = dx / @paperWidth
+    # dy = dy / @paperHeight
+    # # adjust for negative values as well
+    # if dx >= 0
+    #   x1 = @cx2
+    # else
+    #   x1 = @cx2 + dx
+    #   dx = -dx
+    # if dy >= 0
+    #   y1 = @cy2
+    # else
+    #   y1 = @cy2 + dy
+    #   dy = -dy
+    # globals.connection.emitUpdateShape "rect", [ x1, y1, dx, dy ]
+
+  # When rectangle finished being drawn
+  # @param  {Event} e the mouse event
+  # TODO: moved here but not finished
+  dragOnEnd: (e) ->
+    # if @obj?
+    #   attrs = @obj.attrs
+    #   if attrs?
+    #     globals.connection.emitPublishShape "rect",
+    #       [ attrs.x / @gw, attrs.y / @gh, attrs.width / @gw, attrs.height / @gh,
+    #         @currentColour, @currentThickness ]
+    # @obj = null
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..3a882b8988188f8f480673902c94930debcfd4bc
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee
@@ -0,0 +1,21 @@
+# A slide in the whiteboard
+class @WhiteboardSlideModel
+
+  # TODO: check if we really need original and display width and heights separate or if they can be the same
+  constructor: (@id, @url, @img, @originalWidth, @originalHeight, @displayWidth, @displayHeight, @xOffset=0, @yOffset=0) ->
+
+  getWidth: -> @displayWidth
+
+  getHeight: -> @displayHeight
+
+  getOriginalWidth: -> @originalWidth
+
+  getOriginalHeight: -> @originalHeight
+
+  getId: -> @id
+
+  getDimensions: -> [@getWidth(), @getHeight()]
+
+  getOriginalDimensions: -> [@getOriginalWidth(), @getOriginalHeight()]
+
+  getOffsets: -> [@xOffset, @yOffset]
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..83fad347386e7558959e73b350e4a860e8b39256
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee
@@ -0,0 +1,248 @@
+# A text in the whiteboard
+class @WhiteboardTextModel extends WhiteboardToolModel
+
+  constructor: (@paper) ->
+    super @paper
+    # the defintion of this shape, kept so we can redraw the shape whenever needed
+    # format: x, y, width, height, colour, fontSize, calcFontSize, text
+    @definition = [0, 0, 0, 0, "#000", 0, 0, ""]
+
+  # Make a text on the whiteboard
+  make: (startingData) ->
+    #console.log "making text:" + JSON.stringify startingData
+
+    x = startingData.x
+    y = startingData.y
+    width = startingData.textBoxWidth
+    height = startingData.textBoxHeight
+    colour = formatColor(startingData.fontColor)
+    fontSize = startingData.fontSize
+    calcFontSize = startingData.calcedFontSize
+    text = startingData.text
+
+    @definition =
+      shape: "text"
+      data: [x, y, width, height, colour, fontSize, calcFontSize, text]
+
+    #calcFontSize = (calcFontSize/100 * @gh)
+    x = (x * @gw) + @xOffset
+    y = (y * @gh) + @yOffset + calcFontSize
+    width = width/100 * @gw
+
+    @obj = @paper.text(x/100, y/100, "")
+    @obj.attr
+      "fill": colour
+      "font-family": "Arial" # TODO: make dynamic
+      "font-size": calcFontSize
+    @obj.node.style["text-anchor"] = "start" # force left align
+    @obj.node.style["textAnchor"] = "start"  # for firefox, 'cause they like to be different
+    @obj
+
+  # Update text shape drawn
+  # @param  {object} the object containing the shape info
+  update: (startingData) ->
+    #console.log "updating text" + JSON.stringify startingData
+
+    x = startingData.x
+    y = startingData.y
+    maxWidth = startingData.textBoxWidth
+    height = startingData.textBoxHeight
+    colour = formatColor(startingData.fontColor)
+    fontSize = startingData.fontSize
+    calcFontSize = startingData.calcedFontSize
+    myText = startingData.text
+
+    if @obj?
+      @definition.data = [x, y, maxWidth, height, colour, fontSize, calcFontSize, myText]
+
+      calcFontSize = (calcFontSize/100 * @gh)
+      x = (x * @gw)/100 + @xOffset
+      maxWidth = maxWidth/100 * @gw
+
+      @obj.attr
+        "fill": colour
+        "font-family": "Arial" # TODO: make dynamic
+        "font-size": calcFontSize
+      cell = @obj.node
+      while cell? and cell.hasChildNodes()
+        cell.removeChild(cell.firstChild)
+
+      # used code from textFlow lib http://www.carto.net/papers/svg/textFlow/
+      # but had to merge it here because "cell" was bigger than what the stack could take
+
+      #extract and add line breaks for start
+      dashArray = new Array()
+      dashFound = true
+      indexPos = 0
+      cumulY = 0
+      svgNS = "http://www.w3.org/2000/svg"
+      while dashFound is true
+        result = myText.indexOf("-", indexPos)
+        if result is -1
+          #could not find a dash
+          dashFound = false
+        else
+          dashArray.push result
+          indexPos = result + 1
+      #split the text at all spaces and dashes
+      words = myText.split(/[\s-]/)
+      line = ""
+      dy = 0
+      curNumChars = 0
+      computedTextLength = 0
+      myTextNode = undefined
+      tspanEl = undefined
+      i = 0
+      while i < words.length
+        word = words[i]
+        curNumChars += word.length + 1
+        if computedTextLength > maxWidth or i is 0
+          if computedTextLength > maxWidth
+            tempText = tspanEl.firstChild.nodeValue
+            tempText = tempText.slice(0, (tempText.length - words[i - 1].length - 2)) #the -2 is because we also strip off white space
+            tspanEl.firstChild.nodeValue = tempText
+
+          #alternatively one could use textLength and lengthAdjust, however, currently this is not too well supported in SVG UA's
+          tspanEl = document.createElementNS(svgNS, "tspan")
+          tspanEl.setAttributeNS null, "x", x
+          tspanEl.setAttributeNS null, "dy", dy
+          myTextNode = document.createTextNode(line)
+          tspanEl.appendChild myTextNode
+          cell.appendChild tspanEl
+          if checkDashPosition(dashArray, curNumChars - 1)
+            line = word + "-"
+          else
+            line = word + " "
+          line = words[i - 1] + " " + line  unless i is 0
+          dy = calcFontSize
+          cumulY += dy
+        else
+          if checkDashPosition(dashArray, curNumChars - 1)
+            line += word + "-"
+          else
+            line += word + " "
+        tspanEl.firstChild.nodeValue = line
+        computedTextLength = tspanEl.getComputedTextLength()
+        if i is words.length - 1
+          if computedTextLength > maxWidth
+            tempText = tspanEl.firstChild.nodeValue
+            tspanEl.firstChild.nodeValue = tempText.slice(0, (tempText.length - words[i].length - 1))
+            tspanEl = document.createElementNS(svgNS, "tspan")
+            tspanEl.setAttributeNS null, "x", x
+            tspanEl.setAttributeNS null, "dy", dy
+            myTextNode = document.createTextNode(words[i])
+            tspanEl.appendChild myTextNode
+            cell.appendChild tspanEl
+        i++
+      cumulY
+
+
+  #this function checks if there should be a dash at the given position, instead of a blank
+  checkDashPosition = (dashArray, pos) ->
+    result = false
+    i = 0
+    while i < dashArray.length
+      result = true  if dashArray[i] is pos
+      i++
+    result
+
+
+  # Draw a text on the whiteboard
+  # @param  {string} colour    the colour of the object
+  # @param  {number} thickness the thickness of the object's line(s)
+  # draw: (x, y, width, height, colour, fontSize, calcFontSize, text) ->
+  #   calcFontSize = (calcFontSize/100 * @gh)
+  #   x = x * @gw + @xOffset
+  #   y = (y * @gh) + @yOffset + calcFontSize
+  #   width = width/100 * @gw
+  #   #colour = Utils.strokeAndThickness(colour)["stroke"]
+    
+
+  #   el = @paper.text(x, y, "")
+  #   el.attr
+  #     fill: Meteor.call("strokeAndThickness",colour, false)
+  #     "font-family": "Arial" # TODO: make dynamic
+  #     "font-size": calcFontSize
+  #   el.node.style["text-anchor"] = "start" # force left align
+  #   el.node.style["textAnchor"] = "start"  # for firefox, 'cause they like to be different
+  #   Meteor.call("textFlow", text, el.node, width, x, calcFontSize, false)
+  #   el
+
+  # When first dragging the mouse to create the textbox size
+  # @param  {number} x the x value of cursor at the time in relation to the left side of the browser
+  # @param  {number} y the y value of cursor at the time in relation to the top of the browser
+  # TODO: moved here but not finished nor tested
+  # _textStart: (x, y) ->
+  #   [sw, sh] = @_currentSlideDimensions()
+  #   [cx, cy] = @_currentSlideOffsets()
+  #   if @currentText?
+  #     globals.connection.emitPublishShape "text",
+  #       [ @textbox.value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh,
+  #         @textbox.clientWidth, 16, @currentColour, "Arial", 14 ]
+  #     globals.connection.emitTextDone()
+  #   @textbox.value = ""
+  #   @textbox.style.visibility = "hidden"
+  #   @textX = x
+  #   @textY = y
+  #   sx = (@containerWidth - @gw) / 2
+  #   sy = (@containerHeight - @gh) / 2
+  #   @cx2 = (x - @containerOffsetLeft - sx + cx) / sw
+  #   @cy2 = (y - @containerOffsetTop - sy + cy) / sh
+  #   @_makeRect @cx2, @cy2, "#000", 1
+  #   globals.connection.emitMakeShape "rect", [ @cx2, @cy2, "#000", 1 ]
+
+  # Finished drawing the rectangle that the text will fit into
+  # @param  {Event} e the mouse event
+  # TODO: moved here but not finished nor tested
+  # _textStop: (e) ->
+  #   @currentRect.hide() if @currentRect?
+  #   [sw, sh] = @_currentSlideDimensions()
+  #   [cx, cy] = @_currentSlideOffsets()
+  #   tboxw = (e.pageX - @textX)
+  #   tboxh = (e.pageY - @textY)
+  #   if tboxw >= 14 or tboxh >= 14 # restrict size
+  #     @textbox.style.width = tboxw * (@gw / sw) + "px"
+  #     @textbox.style.visibility = "visible"
+  #     @textbox.style["font-size"] = 14 + "px"
+  #     @textbox.style["fontSize"] = 14 + "px" # firefox
+  #     @textbox.style.color = @currentColour
+  #     @textbox.value = ""
+  #     sx = (@containerWidth - @gw) / 2
+  #     sy = (@containerHeight - @gh) / 2
+  #     x = @textX - @containerOffsetLeft - sx + cx + 1 # 1px random padding
+  #     y = @textY - @containerOffsetTop - sy + cy
+  #     @textbox.focus()
+
+  #     # if you click outside, it will automatically sumbit
+  #     @textbox.onblur = (e) =>
+  #       if @currentText
+  #         globals.connection.emitPublishShape "text",
+  #           [ @value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh,
+  #             @textbox.clientWidth, 16, @currentColour, "Arial", 14 ]
+  #         globals.connection.emitTextDone()
+  #       @textbox.value = ""
+  #       @textbox.style.visibility = "hidden"
+
+  #     # if user presses enter key, then automatically submit
+  #     @textbox.onkeypress = (e) ->
+  #       if e.keyCode is "13"
+  #         e.preventDefault()
+  #         e.stopPropagation()
+  #         @onblur()
+
+  #     # update everyone with the new text at every change
+  #     _paper = @
+  #     @textbox.onkeyup = (e) ->
+  #       @style.color = _paper.currentColour
+  #       @value = @value.replace(/\n{1,}/g, " ").replace(/\s{2,}/g, " ")
+  #       globals.connection.emitUpdateShape "text",
+  #         [ @value, x / _paper.sw, (y + (14 * (_paper.sh / _paper.gh))) / _paper.sh,
+  #           tboxw * (_paper.gw / _paper.sw), 16, _paper.currentColour, "Arial", 14 ]
+
+  # The server has said the text is finished,
+  # so set it to null for the next text object
+  # TODO: moved here but not finished nor tested
+  # textDone: ->
+  #   if @currentText?
+  #     @currentText = null
+  #     @currentRect.hide() if @currentRect?
diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..e32da4c8c4563d77d3aef03a45faabcde6fdbf1a
--- /dev/null
+++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee
@@ -0,0 +1,104 @@
+# A triangle in the whiteboard
+class @WhiteboardTriangleModel extends WhiteboardToolModel
+
+  constructor: (@paper) ->
+    super @paper
+
+    # the defintion of this shape, kept so we can redraw the shape whenever needed
+    # format: x1, y1, x2, y2, stroke color, thickness
+    @definition = [0, 0, 0, 0, "#000", "0px"]
+
+  # Make a triangle on the whiteboard
+  # @param  {[type]} x         the x value of the top left corner
+  # @param  {[type]} y         the y value of the top left corner
+  # @param  {string} colour    the colour of the object
+  # @param  {number} thickness the thickness of the object's line(s)
+  make: (info) ->
+    if info?.points?
+      x = info.points[0]
+      y = info.points[1]
+      color = info.color
+      thickness = info.thickness
+
+      path = @_buildPath(x, y, x, y, x, y)
+      @obj = @paper.path(path)
+      @obj.attr "stroke", formatColor(color)
+      @obj.attr "stroke-width", zoomStroke(formatThickness(thickness))
+      @obj.attr({"stroke-linejoin": "round"})
+
+      @definition = [x, y, x, y, @obj.attrs["stroke"], @obj.attrs["stroke-width"]]
+
+    @obj
+
+  # Update triangle drawn
+  # @param  {number} x1 the x value of the top left corner
+  # @param  {number} y1 the y value of the top left corner
+  # @param  {number} x2 the x value of the bottom right corner
+  # @param  {number} y2 the y value of the bottom right corner
+  update: (info) ->
+    if info?.points?
+      x1 = info.points[0]
+      y1 = info.points[1]
+      x2 = info.points[2]
+      y2 = info.points[3]
+
+      if @obj?
+        [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
+
+        path = @_buildPath(xTop * @gw + @xOffset, yTop * @gh + @yOffset,
+                           xBottomLeft * @gw + @xOffset, yBottomLeft * @gh + @yOffset,
+                           xBottomRight * @gw + @xOffset, yBottomRight * @gh + @yOffset)
+        @obj.attr path: path
+
+        @definition[0] = x1
+        @definition[1] = y1
+        @definition[2] = x2
+        @definition[3] = y2
+
+  # Draw a triangle on the whiteboard
+  # @param  {number} x1 the x value of the top left corner
+  # @param  {number} y1 the y value of the top left corner
+  # @param  {number} x2 the x value of the bottom right corner
+  # @param  {number} y2 the y value of the bottom right corner
+  # @param  {string} colour    the colour of the object
+  # @param  {number} thickness the thickness of the object's line(s)
+  draw: (x1, y1, x2, y2, colour, thickness) ->
+    [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2)
+    path = @_buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight)
+    path = @_scaleTrianglePath(path, @gw, @gh, @xOffset, @yOffset)
+    triangle = @paper.path(path)
+    triangle.attr Utils.strokeAndThickness(colour, thickness)
+    triangle.attr({"stroke-linejoin": "round"})
+    triangle
+
+  _getCornersFromPoints: (x1, y1, x2, y2) ->
+    xTop = (((x2 - x1) / 2) + x1)
+    yTop = y1
+    xBottomLeft = x1
+    yBottomLeft = y2
+    xBottomRight = x2
+    yBottomRight = y2
+    [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight]
+
+  _buildPath: (xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) ->
+    "M#{xTop},#{yTop},#{xBottomLeft},#{yBottomLeft},#{xBottomRight},#{yBottomRight}z"
+
+  # Scales a triangle path string to fit within a width and height of the new paper size
+  # @param  {number} w width of the shape as a percentage of the original width
+  # @param  {number} h height of the shape as a percentage of the original height
+  # @return {string}   the path string after being manipulated to new paper size
+  _scaleTrianglePath: (string, w, h, xOffset=0, yOffset=0) ->
+    path = null
+    points = string.match(/(\d+[.]?\d*)/g)
+    len = points.length
+    j = 0
+
+    # go through each point and multiply it by the new height and width
+    path = "M"
+    while j < len
+      path += "," unless j is 0
+      path += "" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset)
+      j += 2
+    path + "z"
+
+WhiteboardTriangleModel
diff --git a/labs/meteor-client/app/collections/collections.coffee b/labs/meteor-client/app/collections/collections.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..49987e25c01fc64aea070a8367777c523be89c46
--- /dev/null
+++ b/labs/meteor-client/app/collections/collections.coffee
@@ -0,0 +1,6 @@
+Meteor.Users = new Meteor.Collection("bbb_users")
+Meteor.Chat = new Meteor.Collection("bbb_chat")
+Meteor.Meetings = new Meteor.Collection("meetings")
+Meteor.Presentations = new Meteor.Collection("presentations")
+Meteor.Shapes = new Meteor.Collection("shapes")
+Meteor.Slides = new Meteor.Collection("slides")
diff --git a/labs/meteor-client/app/config.coffee b/labs/meteor-client/app/config.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..e918fa4fc47d350f2bd8ebb414fa0dce225e4158
--- /dev/null
+++ b/labs/meteor-client/app/config.coffee
@@ -0,0 +1,60 @@
+# TODO: should be split on server and client side
+# # Global configurations file
+
+config = {}
+
+# Default global variables
+config.appName = 'BigBlueButton HTML5 Client'
+config.bbbServerVersion = '0.9.0'
+config.copyrightYear = '2014'
+config.dateOfBuild = 'Sept 25, 2014' #TODO
+config.defaultWelcomeMessage = 'Welcome to %%CONFNAME%%!\r\rFor help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.\r\rTo join the audio bridge click the headset icon (upper-left hand corner).  Use a headset to avoid causing background noise for others.\r\r\r'
+config.defaultWelcomeMessageFooter = "This server is running a build of <a href='https://code.google.com/p/bigbluebutton/wiki/090Overview' target='_blank'><u>BigBlueButton #{config.bbbServerVersion}</u></a>."
+
+config.maxUsernameLength = 30
+config.maxChatLength = 140
+
+## Application configurations
+config.app = {}
+
+# server ip
+config.app.serverIP = "http://192.168.0.119"
+config.app.logOutUrl = "http://192.168.0.119:4000" # TODO temporary
+
+# port for the HTML5 client
+config.app.htmlClientPort = "3000"
+
+# Configs for redis
+config.redis = {}
+config.redis.host = "127.0.0.1"
+config.redis.post = "6379"
+config.redis.timeout = 5000
+config.redis.channels = {}
+config.redis.channels.fromBBBApps = "bigbluebutton:from-bbb-apps:*"
+config.redis.channels.toBBBApps = {}
+config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*"
+config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat"
+config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting"
+config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users"
+config.redis.channels.toBBBApps.voice = "bigbluebutton:to-bbb-apps:voice"
+config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard"
+
+# Logging
+config.log = {}
+
+if Meteor.isServer
+  config.log.path = if process?.env?.NODE_ENV is "production"
+    "/var/log/bigbluebutton/bbbnode.log"
+  else
+    # logs in the directory immediatly before the meteor app
+     process.env.PWD + '/../log/development.log'
+
+  # Setting up a logger in Meteor.log
+  winston = Winston #Meteor.require 'winston'
+  file = config.log.path
+  transports = [ new winston.transports.Console(), new winston.transports.File { filename: file } ]
+
+  Meteor.log = new winston.Logger
+    transports: transports
+
+Meteor.config = config
diff --git a/labs/meteor-client/app/lib/router.coffee b/labs/meteor-client/app/lib/router.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..3f811a168143ceece570d6df0190d9a46d22ae3c
--- /dev/null
+++ b/labs/meteor-client/app/lib/router.coffee
@@ -0,0 +1,38 @@
+@Router.configure layoutTemplate: 'layout'
+
+@Router.map ->
+  @route "login",
+    path: "/login"
+    action: ->
+      meetingId = @params.query.meeting_id
+      userId = @params.query.user_id
+      authToken = @params.query.auth_token
+
+      if meetingId? and userId? and authToken?
+        Meteor.call("validateAuthToken", meetingId, userId, authToken)
+
+        applyNewSessionVars = ->
+          setInSession("authToken", authToken)
+          setInSession("meetingId", meetingId)
+          setInSession("userId", userId)
+          Router.go('/')
+
+        clearSessionVar(applyNewSessionVars)
+
+  @route "main",
+    path: "/"
+    onBeforeAction: ->
+      console.log "in main. onBeforeAction"
+      authToken = getInSession 'authToken'
+      meetingId = getInSession 'meetingId'
+      userId = getInSession 'userId'
+      console.log "currently #{authToken} #{meetingId} #{userId}"
+      Meteor.subscribe 'chat', meetingId, userId, authToken, ->
+        Meteor.subscribe 'shapes', meetingId, ->
+          Meteor.subscribe 'slides', meetingId, ->
+            Meteor.subscribe 'meetings', meetingId, ->
+              Meteor.subscribe 'presentations', meetingId, ->
+                Meteor.subscribe 'users', meetingId, userId, authToken, ->
+                  console.log "done subscribing"
+                  onLoadComplete()
+      @render('main')
diff --git a/labs/meteor-client/app/packages.json b/labs/meteor-client/app/packages.json
new file mode 100644
index 0000000000000000000000000000000000000000..077404aaa415171f6d7c021d41143c361ea058f4
--- /dev/null
+++ b/labs/meteor-client/app/packages.json
@@ -0,0 +1,3 @@
+{
+  
+}
\ No newline at end of file
diff --git a/labs/meteor-client/app/server/bbblogger.coffee b/labs/meteor-client/app/server/bbblogger.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..462d683c1ea6da63bb30e30a8837c4d12d847272
--- /dev/null
+++ b/labs/meteor-client/app/server/bbblogger.coffee
@@ -0,0 +1,17 @@
+###
+bunyan = Meteor.require 'bunyan'
+
+logger = bunyan.createLogger({
+  name: 'bbbnode',
+  streams: [
+    {
+      level: 'debug',
+      stream: process.stdout,
+    },
+    {
+      level: 'info',
+      path: Meteor.config.log.path
+    }
+  ]
+})
+###
diff --git a/labs/meteor-client/app/server/collection_methods/chat.coffee b/labs/meteor-client/app/server/collection_methods/chat.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..66727f023c344fed4010e1b4b7c427dfe4f7d388
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/chat.coffee
@@ -0,0 +1,76 @@
+Meteor.methods
+	# meetingId: the id of the meeting
+	# chatObject: the object including info on the chat message, including the text
+	# requesterUserId: the userId of the user sending chat
+	# requesterToken: the authToken of the requester
+	sendChatMessagetoServer: (meetingId, chatObject, requesterUserId, requesterToken) ->
+		chatType = chatObject.chat_type
+		recipient = chatObject.to_userid
+		eventName = null
+		action = ->
+			if chatType is "PUBLIC_CHAT"
+				eventName = "send_public_chat_message_request"
+				return 'chatPublic'
+			else
+				eventName = "send_private_chat_message_request"
+				if recipient is requesterUserId
+					return 'chatSelf' #not allowed
+				else
+					return 'chatPrivate'
+
+		if isAllowedTo(action(), meetingId, requesterUserId, requesterToken) and chatObject.from_userid is requesterUserId
+			message =
+				header :
+					timestamp: new Date().getTime()
+					name: eventName
+				payload: 
+					message: chatObject
+					meeting_id: meetingId
+					requester_id: chatObject.from_userid
+
+			Meteor.log.info "publishing chat to redis"
+			publish Meteor.config.redis.channels.toBBBApps.chat, message
+		return
+
+	deletePrivateChatMessages: (userId, contact_id) ->
+		# if authorized pass through
+		requester = Meteor.Users.findOne({userId: userId})
+		contact = Meteor.Users.findOne({_id: contact_id})
+		deletePrivateChatMessages(requester.userId, contact.userId)
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+@addChatToCollection = (meetingId, messageObject) ->
+	# manually convert time from 1.408645053653E12 to 1408645053653 if necessary
+	# (this is the time_from that the Flash client outputs)
+	messageObject.from_time = (messageObject.from_time).toString().split('.').join("").split("E")[0]
+
+	if messageObject.from_userid? and messageObject.to_userid?
+		entry =
+			meetingId: meetingId
+			message:
+				chat_type: messageObject.chat_type
+				message: messageObject.message
+				to_username: messageObject.to_username
+				from_tz_offset: messageObject.from_tz_offset
+				from_color: messageObject.from_color
+				to_userid: messageObject.to_userid
+				from_userid: messageObject.from_userid
+				from_time: messageObject.from_time
+				from_username: messageObject.from_username
+				from_lang: messageObject.from_lang
+
+		id = Meteor.Chat.insert(entry)
+		Meteor.log.info "added chat id=[#{id}]:#{messageObject.message}." #" Chat.size is now #{Meteor.Chat.find({meetingId: meetingId}).count()}"
+
+
+# called on server start and meeting end
+@clearChatCollection = (meetingId) ->
+	if meetingId?
+		Meteor.Chat.remove({meetingId: meetingId}, Meteor.log.info "cleared Chat Collection (meetingId: #{meetingId}!")
+	else
+		Meteor.Chat.remove({}, Meteor.log.info "cleared Chat Collection (all meetings)!")
+
+# --------------------------------------------------------------------------------------------
+# end Private methods on server
+# --------------------------------------------------------------------------------------------
diff --git a/labs/meteor-client/app/server/collection_methods/meetings.coffee b/labs/meteor-client/app/server/collection_methods/meetings.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..348d08bd74bd4ae5076e04b3ee024737b3c97a8f
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/meetings.coffee
@@ -0,0 +1,47 @@
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+@addMeetingToCollection = (meetingId, name, intendedForRecording, voiceConf, duration) ->
+	#check if the meeting is already in the collection
+	unless Meteor.Meetings.findOne({meetingId: meetingId})?
+		currentlyBeingRecorded = false # defaut value
+		id = Meteor.Meetings.insert(
+			meetingId: meetingId,
+			meetingName: name,
+			intendedForRecording: intendedForRecording,
+			currentlyBeingRecorded: currentlyBeingRecorded,
+			voiceConf: voiceConf,
+			duration: duration)
+		Meteor.log.info "added meeting _id=[#{id}]:meetingId=[#{meetingId}]:name=[#{name}]:duration=[#{duration}]:voiceConf=[#{voiceConf}]."
+
+
+@clearMeetingsCollection = (meetingId) ->
+	if meetingId?
+		Meteor.Meetings.remove({meetingId: meetingId}, Meteor.log.info "cleared Meetings Collection (meetingId: #{meetingId}!")
+	else
+		Meteor.Meetings.remove({}, Meteor.log.info "cleared Meetings Collection (all meetings)!")
+
+
+@removeMeetingFromCollection = (meetingId) ->
+	if Meteor.Meetings.findOne({meetingId: meetingId})?
+		Meteor.log.info "end of meeting #{meetingId}. Clear the meeting data from all collections"
+		# delete all users in the meeting
+		clearUsersCollection(meetingId)
+
+		# delete all slides in the meeting
+		clearSlidesCollection(meetingId)
+
+		# delete all shapes in the meeting
+		clearShapesCollection(meetingId)
+
+		# delete all presentations in the meeting
+		clearPresentationsCollection(meetingId)
+
+		# delete all chat messages in the meeting
+		clearChatCollection(meetingId)
+
+		# delete the meeting
+		clearMeetingsCollection(meetingId)
+# --------------------------------------------------------------------------------------------
+# end Private methods on server
+# --------------------------------------------------------------------------------------------
diff --git a/labs/meteor-client/app/server/collection_methods/presentations.coffee b/labs/meteor-client/app/server/collection_methods/presentations.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..036ed708aff293f790de2ecd256feba29665fd5d
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/presentations.coffee
@@ -0,0 +1,38 @@
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+@addPresentationToCollection = (meetingId, presentationObject) ->
+  #check if the presentation is already in the collection
+  unless Meteor.Presentations.findOne({meetingId: meetingId, 'presentation.id': presentationObject.id})?
+    entry =
+      meetingId: meetingId
+      presentation:
+        id: presentationObject.id
+        name: presentationObject.name
+        current: presentationObject.current
+
+      pointer: #initially we have no data about the cursor
+        x: 0.0
+        y: 0.0
+
+    id = Meteor.Presentations.insert(entry)
+    #Meteor.log.info "presentation added id =[#{id}]:#{presentationObject.id} in #{meetingId}. Presentations.size is now #{Meteor.Presentations.find({meetingId: meetingId}).count()}"
+
+@removePresentationFromCollection = (meetingId, presentationId) ->
+  if meetingId? and presentationId? and Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})?
+    id = Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})
+    if id?
+      Meteor.Presentations.remove(id._id)
+      Meteor.log.info "----removed presentation[" + presentationId + "] from " + meetingId
+
+
+# called on server start and meeting end
+@clearPresentationsCollection = (meetingId) ->
+  if meetingId?
+    Meteor.Presentations.remove({meetingId: meetingId}, Meteor.log.info "cleared Presentations Collection (meetingId: #{meetingId}!")
+  else
+    Meteor.Presentations.remove({}, Meteor.log.info "cleared Presentations Collection (all meetings)!")
+
+# --------------------------------------------------------------------------------------------
+# end Private methods on server
+# --------------------------------------------------------------------------------------------
diff --git a/labs/meteor-client/app/server/collection_methods/shapes.coffee b/labs/meteor-client/app/server/collection_methods/shapes.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..2f73effb9d3ae8ac9bca7336a8a38a00cd437efd
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/shapes.coffee
@@ -0,0 +1,97 @@
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+@addShapeToCollection = (meetingId, whiteboardId, shapeObject) ->
+  if shapeObject?.shape_type is "text"
+    Meteor.log.info "we are dealing with a text shape and the event is:#{shapeObject.status}"
+
+    entry =
+      meetingId: meetingId
+      whiteboardId: whiteboardId
+      shape:
+        type: shapeObject.shape.type
+        textBoxHeight: shapeObject.shape.textBoxHeight
+        backgroundColor: shapeObject.shape.backgroundColor
+        fontColor: shapeObject.shape.fontColor
+        status: shapeObject.shape.status
+        dataPoints: shapeObject.shape.dataPoints
+        x: shapeObject.shape.x
+        textBoxWidth: shapeObject.shape.textBoxWidth
+        whiteboardId: shapeObject.shape.whiteboardId
+        fontSize: shapeObject.shape.fontSize
+        id: shapeObject.shape.id
+        y: shapeObject.shape.y
+        calcedFontSize: shapeObject.shape.calcedFontSize
+        text: shapeObject.shape.text
+        background: shapeObject.shape.background
+
+    if shapeObject.status is "textEdited" or shapeObject.status is "textPublished"
+      # only keep the final version of the text shape
+      removeTempTextShape = (callback) ->
+        Meteor.Shapes.remove({'shape.id':shapeObject.shape.id})
+        # for s in Meteor.Shapes.find({'shape.id':shapeObject.shape.id}).fetch()
+        #   Meteor.log.info "there is this shape: #{s.shape.text}"
+        callback()
+
+      removeTempTextShape( ->
+        # display as the prestenter is typing
+        id = Meteor.Shapes.insert(entry)
+        Meteor.log.info "#{shapeObject.status} substituting the temp shapes with the newer one"
+      )
+
+  else
+    # the mouse button was released - the drawing is complete
+    # TODO: pencil messages currently don't send draw_end and are labeled all as DRAW_START
+    if shapeObject?.status is "DRAW_END" or (shapeObject?.status is "DRAW_START" and shapeObject?.shape_type is "pencil")
+      entry =
+        meetingId: meetingId
+        whiteboardId: whiteboardId
+        shape:
+          wb_id: shapeObject.wb_id
+          shape_type: shapeObject.shape_type
+          status: shapeObject.status
+          id: shapeObject.id
+          shape:
+            type: shapeObject.shape.type
+            status: shapeObject.shape.status
+            points: shapeObject.shape.points
+            whiteboardId: shapeObject.shape.whiteboardId
+            id: shapeObject.shape.id
+            square: shapeObject.shape.square
+            transparency: shapeObject.shape.transparency
+            thickness: shapeObject.shape.thickness
+            color: shapeObject.shape.color
+
+      id = Meteor.Shapes.insert(entry)
+
+@removeAllShapesFromSlide = (meetingId, whiteboardId) ->
+  Meteor.log.info "removeAllShapesFromSlide__" + whiteboardId
+  if meetingId? and whiteboardId? and Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId})?
+    shapesOnSlide = Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch()
+    Meteor.log.info "number of shapes:" + shapesOnSlide.length
+    for s in shapesOnSlide
+      Meteor.log.info "shape=" + s.shape.id
+      id = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": s.shape.id})
+      if id?
+        Meteor.Shapes.remove(id._id)
+        Meteor.log.info "----removed shape[" + s.shape.id + "] from " + whiteboardId
+        Meteor.log.info "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch().length
+
+@removeShapeFromSlide = (meetingId, whiteboardId, shapeId) ->
+  shapeToRemove = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": shapeId})
+  if meetingId? and whiteboardId? and shapeId? and shapeToRemove?
+    Meteor.Shapes.remove(shapeToRemove._id)
+    Meteor.log.info "----removed shape[" + shapeId + "] from " + whiteboardId
+    Meteor.log.info "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).count()
+
+
+# called on server start and meeting end
+@clearShapesCollection = (meetingId) ->
+  if meetingId?
+    Meteor.Shapes.remove({meetingId: meetingId}, Meteor.log.info "cleared Shapes Collection (meetingId: #{meetingId}!")
+  else
+    Meteor.Shapes.remove({}, Meteor.log.info "cleared Shapes Collection (all meetings)!")
+
+# --------------------------------------------------------------------------------------------
+# end Private methods on server
+# --------------------------------------------------------------------------------------------
diff --git a/labs/meteor-client/app/server/collection_methods/slides.coffee b/labs/meteor-client/app/server/collection_methods/slides.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..04128babbe5e937c55abf4b3ef9589753b5754dc
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/slides.coffee
@@ -0,0 +1,52 @@
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+@displayThisSlide = (meetingId, newSlideId, slideObject) ->
+	presentationId = newSlideId.split("/")[0] # grab the presentationId part of the slideId
+	# change current to false for the old slide
+	Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"slide.current": false}})
+	# for the new slide: remove the version which came with presentation_shared_message from the Collection
+	# to avoid using old data (this message contains everything we need for the new slide)
+	removeSlideFromCollection(meetingId, newSlideId)
+	# add the new slide to the collection
+	addSlideToCollection(meetingId, presentationId, slideObject)
+
+
+@addSlideToCollection = (meetingId, presentationId, slideObject) ->
+	unless Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideObject.id})?
+		entry =
+			meetingId: meetingId
+			presentationId: presentationId
+			slide:
+				height_ratio: slideObject.height_ratio
+				y_offset: slideObject.y_offset
+				num: slideObject.num
+				x_offset: slideObject.x_offset
+				current: slideObject.current
+				png_uri: slideObject.png_uri
+				txt_uri: slideObject.txt_uri
+				id: slideObject.id
+				width_ratio: slideObject.width_ratio
+				swf_uri: slideObject.swf_uri
+				thumb_uri: slideObject.thumb_uri
+
+		id = Meteor.Slides.insert(entry)
+		#Meteor.log.info "added slide id =[#{id}]:#{slideObject.id} in #{meetingId}. Now there are #{Meteor.Slides.find({meetingId: meetingId}).count()} slides in the meeting"
+
+@removeSlideFromCollection = (meetingId, slideId) ->
+	if meetingId? and slideId? and Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})?
+		id = Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})
+		if id?
+			Meteor.Slides.remove(id._id)
+			Meteor.log.info "----removed slide[" + slideId + "] from " + meetingId
+
+# called on server start and meeting end
+@clearSlidesCollection = (meetingId) ->
+	if meetingId?
+		Meteor.Slides.remove({meetingId: meetingId}, Meteor.log.info "cleared Slides Collection (meetingId: #{meetingId}!")
+	else
+		Meteor.Slides.remove({}, Meteor.log.info "cleared Slides Collection (all meetings)!")
+
+# --------------------------------------------------------------------------------------------
+# end Private methods on server
+# --------------------------------------------------------------------------------------------
diff --git a/labs/meteor-client/app/server/collection_methods/users.coffee b/labs/meteor-client/app/server/collection_methods/users.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..5843d9c56701c9622be1702a24009e268c32c56b
--- /dev/null
+++ b/labs/meteor-client/app/server/collection_methods/users.coffee
@@ -0,0 +1,270 @@
+# --------------------------------------------------------------------------------------------
+# Public methods on server
+# All these method must first authenticate the user before it calls the private function counterpart below
+# which sends the request to bbbApps. If the method is modifying the media the current user is sharing,
+# you should perform the request before sending the request to bbbApps. This allows the user request to be performed
+# immediately, since they do not require permission for things such as muting themsevles. 
+# --------------------------------------------------------------------------------------------
+Meteor.methods
+  # meetingId: the meetingId of the meeting the user[s] is in
+  # toMuteUserId: the userId of the user to be [un]muted
+  # requesterUserId: the userId of the requester
+  # requesterToken: the authToken of the requester
+  # mutedBoolean: true for muting, false for unmuting
+  muteUser: (meetingId, toMuteUserId, requesterUserId, requesterToken, mutedBoolean) ->
+    action = ->
+      if mutedBoolean
+        if toMuteUserId is requesterUserId
+          return 'muteSelf'
+        else
+          return 'muteOther'
+      else
+        if toMuteUserId is requesterUserId
+          return 'unmuteSelf'
+        else
+          return 'unmuteOther'
+
+    if isAllowedTo(action(), meetingId, requesterUserId, requesterToken)
+      message =
+        payload:
+          userid: toMuteUserId
+          meeting_id: meetingId
+          mute: mutedBoolean
+          requester_id: requesterUserId
+        header:
+          timestamp: new Date().getTime()
+          name: "mute_user_request"
+          version: "0.0.1"
+
+      Meteor.log.info "publishing a user mute #{mutedBoolean} request for #{toMuteUserId}"
+
+      publish Meteor.config.redis.channels.toBBBApps.voice, message
+      updateVoiceUser meetingId, {'web_userid': toMuteUserId, talking:false, muted:mutedBoolean}
+    return
+
+  # meetingId: the meetingId which both users are in 
+  # toLowerUserId: the userid of the user to have their hand lowered
+  # loweredByUserId: userId of person lowering
+  # loweredByToken: the authToken of the requestor
+  userLowerHand: (meetingId, toLowerUserId, loweredByUserId, loweredByToken) ->
+    action = ->
+      if toLowerUserId is loweredByUserId
+        return 'lowerOwnHand'
+      else
+        return 'lowerOthersHand'
+
+    if isAllowedTo(action(), meetingId, loweredByUserId, loweredByToken)
+      message =
+        payload:
+          userid: toLowerUserId
+          meeting_id: meetingId
+          raise_hand: false
+          lowered_by: loweredByUserId
+        header:
+          timestamp: new Date().getTime()
+          name: "user_lowered_hand_message"
+          version: "0.0.1"
+
+      # publish to pubsub
+      publish Meteor.config.redis.channels.toBBBApps.users, message
+    return
+
+  # meetingId: the meetingId which both users are in 
+  # toRaiseUserId: the userid of the user to have their hand lowered
+  # raisedByUserId: userId of person lowering
+  # raisedByToken: the authToken of the requestor
+  userRaiseHand: (meetingId, toRaiseUserId, raisedByUserId, raisedByToken) ->
+    action = ->
+      if toRaiseUserId is raisedByUserId
+        return 'raiseOwnHand'
+      else
+        return 'raiseOthersHand'
+
+    if isAllowedTo(action(), meetingId, raisedByUserId, raisedByToken)
+      message =
+        payload:
+          userid: toRaiseUserId
+          meeting_id: meetingId
+          raise_hand: false
+          lowered_by: raisedByUserId
+        header:
+          timestamp: new Date().getTime()
+          name: "user_raised_hand_message"
+          version: "0.0.1"
+
+      # publish to pubsub
+      publish Meteor.config.redis.channels.toBBBApps.users, message
+    return
+
+  # meetingId: the meeting where the user is
+  # userId: the userid of the user logging out
+  # authToken: the authToken of the user
+  userLogout: (meetingId, userId, authToken) ->
+    if isAllowedTo('logoutSelf', meetingId, userId, authToken)
+      Meteor.log.info "a user is logging out from #{meetingId}:" + userId
+      requestUserLeaving meetingId, userId
+
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+
+# Only callable from server
+# Received information from BBB-Apps that a user left
+# Need to update the collection
+# params: meetingid, userid as defined in BBB-Apps
+@markUserOffline = (meetingId, userId) ->
+  # mark the user as offline. remove from the collection on meeting_end #TODO
+  Meteor.log.info "marking user [#{userId}] as offline in meeting[#{meetingId}]"
+  Meteor.Users.update({'meetingId': meetingId, 'userId': userId}, {$set:{'user.connection_status':'offline'}})
+
+
+# Corresponds to a valid action on the HTML clientside
+# After authorization, publish a user_leaving_request in redis
+# params: meetingid, userid as defined in BBB-App
+@requestUserLeaving = (meetingId, userId) ->
+  if Meteor.Users.findOne({'meetingId': meetingId, 'userId': userId})?
+    message =
+      payload:
+        meeting_id: meetingId
+        userid: userId
+      header:
+        timestamp: new Date().getTime()
+        name: "user_leaving_request"
+        version: "0.0.1"
+
+    if userId? and meetingId?
+      Meteor.log.info "sending a user_leaving_request for #{meetingId}:#{userId}"
+      publish Meteor.config.redis.channels.toBBBApps.users, message
+    else
+      Meteor.log.info "did not have enough information to send a user_leaving_request"
+
+#update a voiceUser - a helper method
+@updateVoiceUser = (meetingId, voiceUserObject) ->
+  u = Meteor.Users.findOne userId: voiceUserObject.web_userid
+  if u?
+    if voiceUserObject.talking?
+      Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.talking':voiceUserObject.talking}}) # talking
+    if voiceUserObject.joined?
+      Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.joined':voiceUserObject.joined}}) # joined
+    if voiceUserObject.locked?
+      Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.locked':voiceUserObject.locked}}) # locked
+    if voiceUserObject.muted?
+      Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.muted':voiceUserObject.muted}}) # muted
+    if voiceUserObject.listenOnly?
+      Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.listenOnly':voiceUserObject.listenOnly}}) # muted
+  else
+    Meteor.log.info "ERROR! did not find such voiceUser!"
+
+@userJoined = (meetingId, user) ->
+  userId = user.userid
+
+  u = Meteor.Users.findOne({userId:user.userid, meetingId: meetingId})
+  # the collection already contains an entry for this user because
+  # we added a dummy user on register_user_message (to save authToken)
+  if u?
+    Meteor.log.info "UPDATING USER #{user.userid}, authToken=#{u.authToken}"
+    Meteor.Users.update({userId:user.userid, meetingId: meetingId}, {$set:{
+      user:
+        userid: user.userid
+        presenter: user.presenter
+        name: user.name
+        _sort_name: user.name.toLowerCase()
+        phone_user: user.phone_user
+        raise_hand: user.raise_hand
+        has_stream: user.has_stream
+        role: user.role
+        listenOnly: user.listenOnly
+        extern_userid: user.extern_userid
+        permissions: user.permissions
+        locked: user.locked
+        time_of_joining: user.timeOfJoining
+        connection_status: "online" # TODO consider other default value
+        voiceUser:
+          web_userid: user.voiceUser.web_userid
+          callernum: user.voiceUser.callernum
+          userid: user.voiceUser.userid
+          talking: user.voiceUser.talking
+          joined: user.voiceUser.joined
+          callername: user.voiceUser.callername
+          locked: user.voiceUser.locked
+          muted: user.voiceUser.muted
+        webcam_stream: user.webcam_stream
+      }})
+
+    welcomeMessage = Meteor.config.defaultWelcomeMessage
+    .replace /%%CONFNAME%%/, Meteor.Meetings.findOne({meetingId: meetingId})?.meetingName
+    welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter
+
+    # store the welcome message in chat for easy display on the client side
+    chatId = Meteor.Chat.upsert({'message.chat_type':"SYSTEM_MESSAGE", 'message.to_userid': userId, meetingId: meetingId},
+      {$set:{
+        meetingId: meetingId
+        'message.chat_type': "SYSTEM_MESSAGE"
+        'message.message': welcomeMessage
+        'message.from_color': '0x3399FF'
+        'message.to_userid': userId
+        'message.from_userid': "SYSTEM_MESSAGE"
+        'message.from_username': ""
+        'message.from_time': user.timeOfJoining.toString()
+      }})
+    Meteor.log.info "added a system message in chat for user #{userId}"
+
+  else
+    # scenario: there are meetings running at the time when the meteor
+    # process starts. As a result we the get_users_reply message contains
+    # users for which we have not observed user_registered_message and
+    # hence we do not have the auth_token. There will be permission issues
+    # as the server collection does not have the auth_token of such users
+    # and cannot authorize their client side actions
+    Meteor.log.info "NOTE: got user_joined_message "
+    entry =
+      meetingId: meetingId
+      userId: userId
+      user:
+        userid: user.userid
+        presenter: user.presenter
+        name: user.name
+        _sort_name: user.name.toLowerCase()
+        phone_user: user.phone_user
+        raise_hand: user.raise_hand
+        has_stream: user.has_stream
+        role: user.role
+        listenOnly: user.listenOnly
+        extern_userid: user.extern_userid
+        permissions: user.permissions
+        locked: user.locked
+        time_of_joining: user.timeOfJoining
+        connection_status: "" # TODO consider other default value
+        voiceUser:
+          web_userid: user.voiceUser.web_userid
+          callernum: user.voiceUser.callernum
+          userid: user.voiceUser.userid
+          talking: user.voiceUser.talking
+          joined: user.voiceUser.joined
+          callername: user.voiceUser.callername
+          locked: user.voiceUser.locked
+          muted: user.voiceUser.muted
+        webcam_stream: user.webcam_stream
+
+    id = Meteor.Users.insert(entry)
+    Meteor.log.info "joining user id=[#{id}]:#{user.name}. Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}"
+
+@createDummyUser = (meetingId, user) ->
+  if Meteor.Users.findOne({userId:user.userid, meetingId: meetingId})?
+    Meteor.log.info "ERROR!! CAN'T REGISTER AN EXISTSING USER"
+  else
+    entry =
+      meetingId: meetingId
+      userId: user.userid
+      authToken: user.authToken
+
+    id = Meteor.Users.insert(entry)
+    Meteor.log.info "added user dummy user id=[#{id}]:#{user.name}.
+      Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}"
+
+# called on server start and on meeting end
+@clearUsersCollection = (meetingId) ->
+  if meetingId?
+    Meteor.Users.remove({meetingId: meetingId}, Meteor.log.info "cleared Users Collection (meetingId: #{meetingId}!")
+  else
+    Meteor.Users.remove({}, Meteor.log.info "cleared Users Collection (all meetings)!")
diff --git a/labs/meteor-client/app/server/publish.coffee b/labs/meteor-client/app/server/publish.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..727c870d2f771d71c09d98a2699cfb60bd74e6b2
--- /dev/null
+++ b/labs/meteor-client/app/server/publish.coffee
@@ -0,0 +1,66 @@
+# Publish only the online users that are in the particular meetingId
+# On the client side we pass the meetingId parameter
+Meteor.publish 'users', (meetingId, userid, authToken) ->
+  Meteor.log.info "attempt publishing users for #{meetingId}, #{userid}, #{authToken}"
+  u = Meteor.Users.findOne({'userId': userid, 'meetingId': meetingId})
+  if u?
+    Meteor.log.info "found it from the first time #{userid}"
+    if isAllowedTo('subscribeUsers', meetingId, userid, authToken)
+      Meteor.log.info "#{userid} was allowed to subscribe to 'users'"
+      username = u?.user?.name or "UNKNOWN"
+
+      # offline -> online
+      if u.user?.connection_status isnt 'online'
+        Meteor.call "validateAuthToken", meetingId, userid, authToken
+
+      Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "online"}})
+      Meteor.log.info "username of the subscriber: " + username + ", connection_status becomes online"
+
+      @_session.socket.on("close", Meteor.bindEnvironment(=>
+        Meteor.log.info "\na user lost connection: session.id=#{@_session.id} userId = #{userid}, username=#{username}, meeting=#{meetingId}"
+        Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "offline"}})
+        Meteor.log.info "username of the user losing connection: " + username + ", connection_status: becomes offline"
+        requestUserLeaving meetingId, userid
+        )
+      )
+
+      #publish the users which are not offline
+      Meteor.Users.find(
+        {meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}},
+        {fields:{'authToken': false}
+        })
+    else
+      Meteor.log.warn "was not authorized to subscribe to 'users'"
+
+  else #subscribing before the user was added to the collection
+    Meteor.call "validateAuthToken", meetingId, userid, authToken
+    Meteor.log.error "there was no such user #{userid}  in #{meetingId}"
+    Meteor.Users.find(
+      {meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}},
+      {fields:{'authToken': false}
+      })
+
+
+Meteor.publish 'chat', (meetingId, userid, authToken) ->
+  if isAllowedTo('subscribeChat', meetingId, userid, authToken)
+    Meteor.log.info "publishing chat for #{meetingId} #{userid} #{authToken}"
+    return Meteor.Chat.find({$or: [
+      {'message.chat_type': 'PUBLIC_CHAT', 'meetingId': meetingId},
+      {'message.from_userid': userid, 'meetingId': meetingId},
+      {'message.to_userid': userid, 'meetingId': meetingId}
+      ]})
+
+Meteor.publish 'shapes', (meetingId) ->
+  Meteor.Shapes.find({meetingId: meetingId})
+
+Meteor.publish 'slides', (meetingId) ->
+  Meteor.log.info "publishing slides for #{meetingId}"
+  Meteor.Slides.find({meetingId: meetingId})
+
+Meteor.publish 'meetings', (meetingId) ->
+  Meteor.log.info "publishing meetings for #{meetingId}"
+  Meteor.Meetings.find({meetingId: meetingId})
+
+Meteor.publish 'presentations', (meetingId) ->
+  Meteor.log.info "publishing presentations for #{meetingId}"
+  Meteor.Presentations.find({meetingId: meetingId})
diff --git a/labs/meteor-client/app/server/redispubsub.coffee b/labs/meteor-client/app/server/redispubsub.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..d983db9816d2a1467e1ba74d296c15e46a680d3f
--- /dev/null
+++ b/labs/meteor-client/app/server/redispubsub.coffee
@@ -0,0 +1,291 @@
+Meteor.methods
+  #
+  # I dont know if this is okay to be server side. We need to call it from the router, but I don't know if any harm can be caused
+  # by the client calling this
+  #
+
+  # Construct and send a message to bbb-web to validate the user
+  validateAuthToken: (meetingId, userId, authToken) ->
+    Meteor.log.info "sending a validate_auth_token with",
+      userid: userId
+      authToken: authToken
+      meetingid: meetingId
+
+    message =
+      "payload":
+        "auth_token": authToken
+        "userid": userId
+        "meeting_id": meetingId
+      "header":
+        "timestamp": new Date().getTime()
+        "reply_to": meetingId + "/" + userId
+        "name": "validate_auth_token"
+
+    if authToken? and userId? and meetingId?
+      publish Meteor.config.redis.channels.toBBBApps.meeting, message
+    else
+      Meteor.log.info "did not have enough information to send a validate_auth_token message"
+
+class Meteor.RedisPubSub
+  constructor: (callback) ->
+    Meteor.log.info "constructor RedisPubSub"
+
+    @pubClient = redis.createClient()
+    @subClient = redis.createClient()
+
+    Meteor.log.info("Subscribing message on channel: #{Meteor.config.redis.channels.fromBBBApps}")
+
+    @subClient.on "psubscribe", Meteor.bindEnvironment(@_onSubscribe)
+    @subClient.on "pmessage", Meteor.bindEnvironment(@_onMessage)
+
+    @subClient.psubscribe(Meteor.config.redis.channels.fromBBBApps)
+    callback @
+
+  _onSubscribe: (channel, count) =>
+    Meteor.log.info "Subscribed to #{channel}"
+
+    #grab data about all active meetings on the server
+    message =
+      "header":
+        "name": "get_all_meetings_request"
+      "payload": {} # I need this, otherwise bbb-apps won't recognize the message
+    publish Meteor.config.redis.channels.toBBBApps.meeting, message
+
+  _onMessage: (pattern, channel, jsonMsg) =>
+    # TODO: this has to be in a try/catch block, otherwise the server will
+    # crash if the message has a bad format
+
+    message = JSON.parse(jsonMsg)
+    correlationId = message.payload?.reply_to or message.header?.reply_to
+    meetingId = message.payload?.meeting_id
+
+    ignoredEventTypes = [
+      "keep_alive_reply"
+      "page_resized_message"
+      "presentation_page_resized_message"
+      "presentation_cursor_updated_message" # just because it's common. we handle it anyway
+    ]
+
+    if message?.header? and message?.payload?
+      unless message.header.name in ignoredEventTypes
+        Meteor.log.info "eventType=  #{message.header.name}  ",
+          message: jsonMsg
+
+      # handle voice events
+      if message.header.name in ['user_left_voice_message', 'user_joined_voice_message', 'user_voice_talking_message', 'user_voice_muted_message']
+        voiceUser = message.payload.user?.voiceUser
+        updateVoiceUser meetingId, voiceUser
+        return
+
+      # listen only
+      if message.header.name is 'user_listening_only'
+        updateVoiceUser meetingId, {'web_userid': message.payload.userid, 'listenOnly': message.payload.listen_only}
+        # most likely we don't need to ensure that the user's voiceUser's {talking, joined, muted, locked} are false by default #TODO?
+        return
+
+      if message.header.name is "get_all_meetings_reply"
+        Meteor.log.info "Let's store some data for the running meetings so that when an HTML5 client joins everything is ready!"
+        listOfMeetings = message.payload.meetings
+        for meeting in listOfMeetings
+          # we currently do not have voice_conf or duration in this message.
+          addMeetingToCollection meeting.meetingID, meeting.meetingName, meeting.recorded, meeting.voiceBridge, meeting.duration
+        return
+
+      if message.header.name is "get_users_reply" and message.payload.requester_id is "nodeJSapp"
+        unless Meteor.Meetings.findOne({MeetingId: message.payload.meeting_id})?
+          users = message.payload.users
+          for user in users
+            user.timeOfJoining = message.header.current_time # TODO this might need to be removed
+            userJoined meetingId, user
+        return
+
+      if message.header.name is "validate_auth_token_reply"
+        return
+
+      if message.header.name is "user_registered_message"
+        createDummyUser message.payload.meeting_id, message.payload.user
+        return
+
+      if message.header.name is "user_joined_message"
+        user = message.payload.user
+        user.timeOfJoining = message.header.current_time
+        userJoined meetingId, user
+        return
+
+      if message.header.name is "user_left_message"
+        userId = message.payload.user?.userid
+        if userId? and meetingId?
+          markUserOffline meetingId, userId
+        return
+
+      if message.header.name is "disconnect_user_message"
+        Meteor.log.info "a user(#{message.payload.userid}) was kicked out from meeting(#{message.payload.meeting_id})"
+        return
+
+      if message.header.name is "get_chat_history_reply" and message.payload.requester_id is "nodeJSapp"
+        unless Meteor.Meetings.findOne({MeetingId: message.payload.meeting_id})?
+          for chatMessage in message.payload.chat_history
+            addChatToCollection meetingId, chatMessage
+        return
+
+      if message.header.name is "send_public_chat_message" or message.header.name is "send_private_chat_message"
+        messageObject = message.payload.message
+        # use current_time instead of message.from_time so that the chats from Flash and HTML5 have uniform times
+        messageObject.from_time = message.header.current_time
+        addChatToCollection meetingId, messageObject
+        return
+
+      if message.header.name is "meeting_created_message"
+        meetingName = message.payload.name
+        intendedForRecording = message.payload.recorded
+        voiceConf = message.payload.voice_conf
+        duration = message.payload.duration
+        addMeetingToCollection meetingId, meetingName, intendedForRecording, voiceConf, duration
+        return
+
+      if message.header.name is "presentation_shared_message"
+        presentationId = message.payload.presentation?.id
+        # change the currently displayed presentation to presentation.current = false
+        Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},{$set: {"presentation.current": false}})
+
+        #update(if already present) entirely the presentation with the fresh data
+        removePresentationFromCollection meetingId, presentationId
+        addPresentationToCollection meetingId, message.payload.presentation
+
+        for slide in message.payload.presentation?.pages
+          addSlideToCollection meetingId, message.payload.presentation?.id, slide
+          if slide.current
+            displayThisSlide meetingId, slide.id, slide
+        return
+
+      if message.header.name is "get_presentation_info_reply" and message.payload.requester_id is "nodeJSapp"
+        for presentation in message.payload.presentations
+          addPresentationToCollection meetingId, presentation
+
+          for page in presentation.pages
+            #add the slide to the collection
+            addSlideToCollection meetingId, presentation.id, page
+
+            #request for shapes
+            whiteboardId = "#{presentation.id}/#{page.num}" # d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1
+            #Meteor.log.info "the whiteboard_id here is:" + whiteboardId
+
+            message =
+              "payload":
+                "meeting_id": meetingId
+                "requester_id": "nodeJSapp"
+                "whiteboard_id": whiteboardId
+              "header":
+                "timestamp": new Date().getTime()
+                "name": "get_whiteboard_shapes_request"
+                "version": "0.0.1"
+
+            if whiteboardId? and meetingId?
+              publish Meteor.config.redis.channels.toBBBApps.whiteboard, message
+            else
+              Meteor.log.info "did not have enough information to send a user_leaving_request"
+        return
+
+      if message.header.name is "presentation_page_changed_message"
+        newSlide = message.payload.page
+        displayThisSlide meetingId, newSlide?.id, newSlide
+        return
+
+      if message.header.name is "get_whiteboard_shapes_reply" and message.payload.requester_id is "nodeJSapp"
+        for shape in message.payload.shapes
+          whiteboardId = shape.wb_id
+          addShapeToCollection meetingId, whiteboardId, shape
+        return
+
+      if message.header.name is "send_whiteboard_shape_message"
+        shape = message.payload.shape
+        whiteboardId = shape?.wb_id
+        addShapeToCollection meetingId, whiteboardId, shape
+        return
+
+      if message.header.name is "presentation_cursor_updated_message"
+        x = message.payload.x_percent
+        y = message.payload.y_percent
+        Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},{$set: {"pointer.x": x, "pointer.y": y}})
+        return
+
+      if message.header.name is "whiteboard_cleared_message"
+        whiteboardId = message.payload.whiteboard_id
+        removeAllShapesFromSlide meetingId, whiteboardId
+        return
+
+      if message.header.name is "undo_whiteboard_request"
+        whiteboardId = message.payload.whiteboard_id
+        shapeId = message.payload.shape_id
+        removeShapeFromSlide meetingId, whiteboardId, shapeId
+        return
+
+      if message.header.name is "presenter_assigned_message"
+        newPresenterId = message.payload.new_presenter_id
+        if newPresenterId?
+          # reset the previous presenter
+          Meteor.Users.update({"user.presenter": true, meetingId: meetingId},{$set: {"user.presenter": false}})
+          # set the new presenter
+          Meteor.Users.update({"user.userid": newPresenterId, meetingId: meetingId},{$set: {"user.presenter": true}})
+        return
+
+      if message.header.name is "presentation_page_resized_message"
+        slideId = message.payload.page?.id
+        heightRatio = message.payload.page?.height_ratio
+        widthRatio = message.payload.page?.width_ratio
+        xOffset = message.payload.page?.x_offset
+        yOffset = message.payload.page?.y_offset
+        presentationId = slideId.split("/")[0]
+        Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"slide.height_ratio": heightRatio, "slide.width_ratio": widthRatio, "slide.x_offset": xOffset, "slide.y_offset": yOffset}})
+        return
+
+      if message.header.name is "user_raised_hand_message"
+        userId = message.payload.userid
+        meetingId = message.payload.meeting_id
+        if userId? and meetingId?
+          last_raised = new Date()
+          Meteor.Users.update({"user.userid": userId},{$set: {"user.raise_hand": last_raised}})
+        return
+
+      if message.header.name is "user_lowered_hand_message"
+        userId = message.payload.userid
+        meetingId = message.payload.meeting_id
+        if userId? and meetingId?
+          Meteor.Users.update({"user.userid": userId, meetingId: meetingId},{$set: {"user.raise_hand": 0}})
+        return
+
+      if message.header.name is "recording_status_changed_message"
+        intendedForRecording = message.payload.recorded
+        currentlyBeingRecorded = message.payload.recording
+        Meteor.Meetings.update({meetingId: meetingId, intendedForRecording: intendedForRecording}, {$set: {currentlyBeingRecorded: currentlyBeingRecorded}})
+        return
+
+      if message.header.name in ["meeting_ended_message", "meeting_destroyed_event",
+        "end_and_kick_all_message", "disconnect_all_users_message"]
+        if Meteor.Meetings.findOne({meetingId: meetingId})?
+          Meteor.log.info "there are #{Meteor.Users.find({meetingId: meetingId}).count()} users in the meeting"
+          for user in Meteor.Users.find({meetingId: meetingId}).fetch()
+            markUserOffline meetingId, user.userId
+            #TODO should we clear the chat messages for that meeting?!
+          unless message.header.name is "disconnect_all_users_message"
+            removeMeetingFromCollection meetingId
+        return
+
+# --------------------------------------------------------------------------------------------
+# Private methods on server
+# --------------------------------------------------------------------------------------------
+
+# message should be an object
+@publish = (channel, message) ->
+  Meteor.log.info "Publishing",
+    channel: channel
+    message: message
+
+  if Meteor.redisPubSub?
+    Meteor.redisPubSub.pubClient.publish channel, JSON.stringify(message), (err, res) ->
+      if err
+        Meteor.log.info "error",
+          error: err
+
+  else
+    Meteor.log.info "ERROR!! Meteor.redisPubSub was undefined"
diff --git a/labs/meteor-client/app/server/server.coffee b/labs/meteor-client/app/server/server.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..125492a669988db89201196ae24a0bcd2a501425
--- /dev/null
+++ b/labs/meteor-client/app/server/server.coffee
@@ -0,0 +1,14 @@
+Meteor.startup ->
+  Meteor.log.info "server start"
+
+  #remove all data
+  clearUsersCollection()
+  clearChatCollection()
+  clearMeetingsCollection()
+  clearShapesCollection()
+  clearSlidesCollection()
+  clearPresentationsCollection()
+
+  # create create a PubSub connection, start listening
+  Meteor.redisPubSub = new Meteor.RedisPubSub(->
+    Meteor.log.info "created pubsub")
diff --git a/labs/meteor-client/app/server/user_permissions.coffee b/labs/meteor-client/app/server/user_permissions.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..b7e171b9f07f30db13dd89362651195feb669bf4
--- /dev/null
+++ b/labs/meteor-client/app/server/user_permissions.coffee
@@ -0,0 +1,36 @@
+
+moderator = null
+presenter = null
+viewer =
+  # raising/lowering hand
+  raiseOwnHand : true
+  lowerOwnHand : true
+
+  # muting
+  muteSelf : true
+  unmuteSelf : true
+
+  logoutSelf : true
+
+  #subscribing
+  subscribeUsers: true
+  subscribeChat: true
+
+  #chat
+  chatPublic: true #should make this dynamically modifiable later on
+  chatPrivate: true #should make this dynamically modifiable later on
+
+@isAllowedTo = (action, meetingId, userId, authToken) ->
+  Meteor.log.info "in isAllowedTo: action-#{action}, userId=#{userId}, authToken=#{authToken}"
+
+  user = Meteor.Users.findOne({meetingId:meetingId, userId: userId})
+  if user?
+    # we check if the user is who he claims to be
+    if authToken is user.authToken
+      if user.user?.role is 'VIEWER' or user.user?.role is undefined
+        return viewer[action] or false
+    Meteor.log.error "in meetingId=#{meetingId} userId=#{userId} tried to perform #{action} without permission" +
+     "\n..while the authToken was #{user.authToken}    and the user's object is #{JSON.stringify user}"
+
+    # the current version of the HTML5 client represents only VIEWER users
+  return false
diff --git a/labs/meteor-client/app/smart.json b/labs/meteor-client/app/smart.json
new file mode 100644
index 0000000000000000000000000000000000000000..df361d73aa09c1c4eb0b0ff0ee12c5885b14c29b
--- /dev/null
+++ b/labs/meteor-client/app/smart.json
@@ -0,0 +1,7 @@
+{
+  "packages": {
+    "raphaeljs-package": {
+      "git": "https://github.com/tomconnors/raphaeljs-package.git"
+    }
+  }
+}
diff --git a/labs/meteor-client/app/smart.lock b/labs/meteor-client/app/smart.lock
new file mode 100644
index 0000000000000000000000000000000000000000..28323ba08a86ea6a4584af9e30f9038eb0c9785b
--- /dev/null
+++ b/labs/meteor-client/app/smart.lock
@@ -0,0 +1,18 @@
+{
+  "meteor": {},
+  "dependencies": {
+    "basePackages": {
+      "raphaeljs-package": {
+        "git": "https://github.com/tomconnors/raphaeljs-package.git",
+        "branch": "master"
+      }
+    },
+    "packages": {
+      "raphaeljs-package": {
+        "git": "https://github.com/tomconnors/raphaeljs-package.git",
+        "branch": "master",
+        "commit": "85eaef3664ec063e4bcb81be1226d126d4d20539"
+      }
+    }
+  }
+}
diff --git a/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js b/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js
new file mode 100755
index 0000000000000000000000000000000000000000..a766640de087f922d9fec07dd61dffb371c63e37
--- /dev/null
+++ b/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js
@@ -0,0 +1,264 @@
+var emptyUsersCollection = function() {
+  Meteor.Users.find().map(function(item) {
+    Meteor.Users.remove({ _id: item._id });
+  });
+}
+
+var renderUsersListTemplate = function() {
+  var div = document.createElement("div");
+    var data = {};
+    data.id = "users";
+    data.name = "usersList";
+    var comp = Blaze.renderWithData(Template.usersList, data); // loading data is optional
+    Blaze.insert(comp, div);
+    return div;
+}
+
+// TODO: try to use Meteor methods instead
+var removeUserFromCollection = function(id) {
+  Meteor.Users.find().map(function(item) {
+    if(item.userId == id) Meteor.Users.remove({ _id: item._id });
+  });
+}
+
+// TODO: try to start with calling the app's methods instead of modifying the collection
+xdescribe("usersList template", function () {
+  beforeEach(function () {
+    emptyUsersCollection();
+  });
+
+  it("should have no users when we start with an empty Users collection", function () {
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".userNameEntry")[0]).not.toBeDefined();
+  });
+
+  it("should not display presenter icon next to a non-presenter user", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        presenter: false
+      }
+    };
+    Meteor.Users.insert(document1);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".glyphicon-picture")[0]).not.toBeDefined();
+  });
+
+  it("should display presenter icon next to the presenter's username", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        presenter: true
+      }
+    };
+    Meteor.Users.insert(document1);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".glyphicon-picture")[0]).toBeDefined();
+  });
+
+  it("should display usernames correctly", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        name: "Maxim"
+      }
+    };
+    Meteor.Users.insert(document1);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".userNameEntry").html().trim()).toEqual("Maxim");
+  });
+
+  it("should display all the users in chat (correct number)", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        name: "Maxim"
+      }
+    };
+    var document2 = {
+      meetingId: "meeting001",
+      userId: "user002",
+      user: {
+        name: "Anton"
+      }
+    };
+    var document3 = {
+      meetingId: "meeting001",
+      userId: "user003",
+      user: {
+        name: "Danny"
+      }
+    };
+
+    Meteor.Users.insert(document1);
+    Meteor.Users.insert(document2);
+    Meteor.Users.insert(document3);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".userNameEntry").size()).toEqual(3);
+  });
+
+  it("should be able to reactively handle new and logged-out users (1 user -> 3 users -> 4 users -> 2 users -> 5 users)", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        name: "Maxim"
+      }
+    };
+    var document2 = {
+      meetingId: "meeting001",
+      userId: "user002",
+      user: {
+        name: "Anton"
+      }
+    };
+    var document3 = {
+      meetingId: "meeting001",
+      userId: "user003",
+      user: {
+        name: "Danny"
+      }
+    };
+    var document4 = {
+      meetingId: "meeting001",
+      userId: "user004",
+      user: {
+        name: "Chad"
+      }
+    };
+    var document5 = {
+      meetingId: "meeting001",
+      userId: "user005",
+      user: {
+        name: "Fardad"
+      }
+    };
+    var document6 = {
+      meetingId: "meeting001",
+      userId: "user006",
+      user: {
+        name: "Adam"
+      }
+    };
+    var document7 = {
+      meetingId: "meeting001",
+      userId: "user007",
+      user: {
+        name: "Gary"
+      }
+    };
+
+    Meteor.Users.insert(document1);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".userNameEntry").size()).toEqual(1);
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+
+    Meteor.Users.insert(document2);
+    Meteor.Users.insert(document3);
+
+    expect($(div).find(".userNameEntry").size()).toEqual(3);
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+    expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton");
+    expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny");
+
+    Meteor.Users.insert(document4);
+
+    expect($(div).find(".userNameEntry").size()).toEqual(4);
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+    expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton");
+    expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny");
+    expect($(div).find(".userNameEntry:eq(3)").html().trim()).toEqual("Chad");
+
+    removeUserFromCollection("user002");
+    removeUserFromCollection("user004");
+
+    expect($(div).find(".userNameEntry").size()).toEqual(2);
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+    expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Danny");
+
+    Meteor.Users.insert(document5);
+    Meteor.Users.insert(document6);
+    Meteor.Users.insert(document7);
+
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+    expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Danny");
+    expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Fardad");
+    expect($(div).find(".userNameEntry:eq(3)").html().trim()).toEqual("Adam");
+    expect($(div).find(".userNameEntry:eq(4)").html().trim()).toEqual("Gary");
+  });
+
+  it("should display usernames in the correct order", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        name: "Maxim"
+      }
+    };
+    var document2 = {
+      meetingId: "meeting001",
+      userId: "user002",
+      user: {
+        name: "Anton"
+      }
+    };
+    var document3 = {
+      meetingId: "meeting001",
+      userId: "user003",
+      user: {
+        name: "Danny"
+      }
+    };
+
+    Meteor.Users.insert(document1);
+    Meteor.Users.insert(document2);
+    Meteor.Users.insert(document3);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim");
+    expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton");
+    expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny");
+  });
+
+  it("should handle listen-only users properly", function () {
+    var document1 = {
+      meetingId: "meeting001",
+      userId: "user001",
+      user: {
+        name: "Maxim"
+      }
+    };
+    var document2 = {
+      meetingId: "meeting001",
+      userId: "user002",
+      user: {
+        name: "Anton",
+        listenOnly: true
+      }
+    };
+    var document3 = {
+      meetingId: "meeting001",
+      userId: "user003",
+      user: {
+        name: "Danny"
+      }
+    };
+
+    Meteor.Users.insert(document1);
+    Meteor.Users.insert(document2);
+    Meteor.Users.insert(document3);
+    var div = renderUsersListTemplate();
+
+    expect($(div).find(".glyphicon-headphones")).toBeDefined();
+  });
+});
diff --git a/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js b/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js
new file mode 100755
index 0000000000000000000000000000000000000000..db7a809486f5511e7cc854f6f79cded3ed39d974
--- /dev/null
+++ b/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js
@@ -0,0 +1,95 @@
+var emptyMeetingsCollection = function() {
+  Meteor.Meetings.find().map(function(item) {
+    Meteor.Meetings.remove({ _id: item._id });
+  });
+}
+
+var emptyPresentationsCollection = function() {
+  Meteor.Presentations.find().map(function(item) {
+    Meteor.Presentations.remove({ _id: item._id });
+  });
+}
+
+var emptySlidesCollection = function() {
+  Meteor.Slides.find().map(function(item) {
+    Meteor.Slides.remove({ _id: item._id });
+  });
+}
+
+var emptyShapesCollection = function() {
+  Meteor.Shapes.find().map(function(item) {
+    Meteor.Shapes.remove({ _id: item._id });
+  });
+}
+
+var renderWhiteboardTemplate = function(title) {
+  var div = document.createElement("div");
+    var data = {};
+    data.id = "whiteboard";
+    data.title = title;
+    data.name = "whiteboard";
+    var comp = Blaze.renderWithData(Template.whiteboard, data); // loading data is optional
+    Blaze.insert(comp, div);
+    return div;
+}
+
+xdescribe("whiteboard template", function () {
+  beforeEach(function () {
+    emptyMeetingsCollection();
+    emptyPresentationsCollection();
+    emptySlidesCollection();
+    emptyShapesCollection();
+  });
+
+  it("should contain a pencil icon inside the title entry", function () {
+    var div = renderWhiteboardTemplate("Whiteboard: default.pdf");
+
+    expect($(div).find(".title").find(".glyphicon-pencil")[0]).toBeDefined();
+  });
+
+  it("should contain the correct title", function () {
+    var div = renderWhiteboardTemplate("Whiteboard: default.pdf");
+
+    expect($(div).find(".title:eq(0)").html()).toContain("Whiteboard: default.pdf");
+  });
+
+  // TODO: finish the following
+  it("should be rendered successfully", function () {
+    var meeting1 = {
+      meetingId: "meeting001",
+      meetingName: "first meeting"
+    }
+    Meteor.Meetings.insert(meeting1);
+
+    var presentation1 = {
+      meetingId: "meeting001",
+      presentation: {
+        id: "presentation001",
+        name: "default.pdf",
+        current: true
+      }
+    }
+    Meteor.Presentations.insert(presentation1);
+
+    var slide1 = {
+      meetingId: "meeting001",
+      presentationId: "presentation001",
+      slide: {
+        id: "slide001",
+        num: 1,
+        current: true,
+        width_ratio: 100.0,
+        height_ratio: 100.0,
+        x_offset: 0.0,
+        y_offset: 0.0,
+        png_uri: "http://bigbluebutton.org/wp-content/uploads/2013/05/bbb-overview.png"
+      }
+    }
+    Meteor.Slides.insert(slide1);
+
+    var div = document.createElement("DIV");
+    Blaze.render(Template.main, div);
+
+    expect($(div).find("#whiteboard-navbar")[0]).toBeDefined();
+  });
+});
diff --git a/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js b/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js
new file mode 100755
index 0000000000000000000000000000000000000000..4f7c913db8cbc8c7238efa9b7c609386300ec725
--- /dev/null
+++ b/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js
@@ -0,0 +1,637 @@
+describe("Collections", function () {
+  beforeEach(function () {
+    MeteorStubs.install();
+  });
+
+  afterEach(function () {
+    MeteorStubs.uninstall();
+  });
+
+  //----------------------------------------------------------------------
+  // publish.coffee
+  //----------------------------------------------------------------------
+
+  it("should all be correctly handled by remove() after calling clearCollections()", function () {
+    spyOn(Meteor.Users, "remove");
+    spyOn(Meteor.Chat, "remove");
+    spyOn(Meteor.Meetings, "remove");
+    spyOn(Meteor.Shapes, "remove");
+    spyOn(Meteor.Slides, "remove");
+    spyOn(Meteor.Presentations, "remove");
+
+    clearUsersCollection();
+    clearChatCollection();
+    clearMeetingsCollection();
+    clearShapesCollection();
+    clearSlidesCollection();
+    clearPresentationsCollection();
+
+    expect(Meteor.Users.remove).toHaveBeenCalled();
+    expect(Meteor.Chat.remove).toHaveBeenCalled();
+    expect(Meteor.Meetings.remove).toHaveBeenCalled();
+    expect(Meteor.Shapes.remove).toHaveBeenCalled();
+    expect(Meteor.Slides.remove).toHaveBeenCalled();
+    expect(Meteor.Presentations.remove).toHaveBeenCalled();
+  });
+
+  //----------------------------------------------------------------------
+  // chat.coffee
+  //----------------------------------------------------------------------
+
+  it("should be handled correctly by insert() on calling addChatToCollection() in case of private chat", function () {
+    spyOn(Meteor.Users, "findOne").and.callFake(function(doc) {
+      if(doc.userId == "user001") return { userId: "user001" };
+      else if(doc.userId == "user002") return { userUd: "user002" };
+    });
+    spyOn(Meteor.Chat, "insert");
+
+    addChatToCollection("meeting001", {
+      from_time: "123",
+      from_userid: "user001",
+      to_userid: "user002",
+      chat_type: "PRIVATE_CHAT",
+      message: "Hello!",
+      to_username: "Anton",
+      from_tz_offset: "240",
+      from_color: "0x000000",
+      from_username: "Maxim",
+      from_lang: "en"
+    });
+
+    expect(Meteor.Chat.insert).toHaveBeenCalledWith({
+      meetingId: "meeting001",
+      message: {
+        chat_type: "PRIVATE_CHAT",
+        message: "Hello!",
+        to_username: "Anton",
+        from_tz_offset: "240",
+        from_color: "0x000000",
+        to_userid: "user002",//not "dbid002"
+        from_userid: "user001",//not "dbid001"
+        from_time: "123",
+        from_username: "Maxim",
+        from_lang: "en"
+      }
+    });
+  });
+
+  it("should be handled correctly by insert() on calling addChatToCollection() in case of public chat", function () {
+    spyOn(Meteor.Users, "findOne").and.callFake(function(doc) {
+      if(doc.userId == "user001") return { _id: "dbid001" };
+      else if(doc.userId == "user002") return { _id: "dbid002" };
+    });
+    spyOn(Meteor.Chat, "insert");
+
+    addChatToCollection("meeting001", {
+      from_time: "123",
+      from_userid: "user001",
+      to_userid: "public_chat_userid",
+      chat_type: "PUBLIC_CHAT",
+      message: "Hello!",
+      to_username: "public_chat_username",
+      from_tz_offset: "240",
+      from_color: "0x000000",
+      from_username: "Maxim",
+      from_lang: "en"
+    });
+
+    expect(Meteor.Chat.insert).toHaveBeenCalledWith({
+      meetingId: "meeting001",
+      message: {
+        chat_type: "PUBLIC_CHAT",
+        message: "Hello!",
+        to_username: "public_chat_username",
+        from_tz_offset: "240",
+        from_color: "0x000000",
+        to_userid: "public_chat_userid",
+        from_userid: "user001",
+        from_time: "123",
+        from_username: "Maxim",
+        from_lang: "en"
+      }
+    });
+  });
+
+  //----------------------------------------------------------------------
+  // meetings.coffee
+  //----------------------------------------------------------------------
+
+  it("should not be updated on calling addMeetingToCollection() if the meeting is already in the collection", function () {
+    spyOn(Meteor.Meetings, "findOne").and.callFake(function(doc) {
+      if(doc.meetingId == "meeting001") return { meetingId: "meeting001" };
+      else return undefined;
+    });
+    spyOn(Meteor.Meetings, "insert");
+
+    addMeetingToCollection("meeting001", "Demo Meeting", false, "12345", "0");
+
+    expect(Meteor.Meetings.insert).not.toHaveBeenCalled();
+  });
+
+  it("should be handled correctly by insert() on calling addMeetingToCollection() with a brand new meeting", function () {
+    spyOn(Meteor.Meetings, "findOne").and.returnValue(undefined);//collection is empty
+    spyOn(Meteor.Meetings, "insert");
+
+    addMeetingToCollection("meeting001", "Demo Meeting", false, "12345", "0");
+
+    expect(Meteor.Meetings.insert).toHaveBeenCalledWith({
+      meetingId: "meeting001",
+      meetingName: "Demo Meeting",
+      intendedForRecording: false,
+      currentlyBeingRecorded: false,//default value
+      voiceConf: "12345",
+      duration: "0"
+    });
+  });
+
+  it("should not be touched on calling removeMeetingFromCollection() if there is no wanted meeting in the collection", function () {
+    spyOn(Meteor.Meetings, "findOne").and.returnValue(undefined);//collection is empty
+    spyOn(Meteor.Meetings, "remove");
+
+    removeMeetingFromCollection("meeting001");
+
+    expect(Meteor.Meetings.remove).not.toHaveBeenCalled();
+  });
+
+  //TODO: emulate a find() call
+  /*it("should be correctly updated after the removeMeetingFromCollection() call", function () {
+    spyOn(Meteor.Meetings, "findOne").and.callFake(function(doc) {
+      if(doc.meetingId == "meeting001") return { _id: "id001", meetingId: "meeting001" };
+      else return undefined;
+    });
+
+    spyOn(Meteor.Meetings, "remove");
+
+    removeMeetingFromCollection("meeting001");
+
+    expect(Meteor.Meetings.remove).toHaveBeenCalled();
+  });*/
+
+  //----------------------------------------------------------------------
+  // shapes.coffee
+  //----------------------------------------------------------------------
+
+  // addShapeToCollection()
+  it('should be handled correctly by insert() on calling addShapeToCollection() with a text', function () {
+    spyOn(Meteor.Shapes, 'find').and.returnValue({
+      count: function() {
+        return 1;
+      }
+    });
+    spyOn(Meteor.Shapes, 'insert');
+
+    addShapeToCollection('meeting001', 'whiteboard001', {
+      shape_type: 'text',
+      status: 'textPublished',
+      shape: {
+        type: 'text',
+        textBoxHeight: 24.5,
+        backgroundColor: 16777215,
+        fontColor: 0,
+        status: 'textPublished',
+        dataPoints: '36.5,55.0',
+        x: 36.5,
+        textBoxWidth: 36.0,
+        whiteboardId: 'whiteboard001',
+        fontSize: 18,
+        id: 'shape001',
+        y: 55.0,
+        calcedFontSize: 3.6,
+        text: 'Hello World!',
+        background: true
+      }
+    });
+
+    expect(Meteor.Shapes.insert).toHaveBeenCalledWith({
+      meetingId: 'meeting001',
+      whiteboardId: 'whiteboard001',
+      shape: {
+        type: 'text',
+        textBoxHeight: 24.5,
+        backgroundColor: 16777215,
+        fontColor: 0,
+        status: 'textPublished',
+        dataPoints: '36.5,55.0',
+        x: 36.5,
+        textBoxWidth: 36.0,
+        whiteboardId: 'whiteboard001',
+        fontSize: 18,
+        id: 'shape001',
+        y: 55.0,
+        calcedFontSize: 3.6,
+        text: 'Hello World!',
+        background: true
+      }
+    });
+  });
+  it('should be handled correctly by insert() on calling addShapeToCollection() with a finished standard shape', function () {
+    spyOn(Meteor.Shapes, 'find').and.returnValue({
+      count: function() {
+        return 1;
+      }
+    });
+    spyOn(Meteor.Shapes, 'insert');
+
+    addShapeToCollection('meeting001', 'whiteboard001', {
+      wb_id: 'whiteboard001',
+      shape_type: 'rectangle',
+      status: 'DRAW_END',
+      id: 'shape001',
+      shape: {
+        type: 'rectangle',
+        status: 'DRAW_END',
+        points: [60.0, 17.0, 73.0, 57.5],
+        whiteboardId: 'whiteboard001',
+        id: 'shape001',
+        square: false,
+        transparency: false,
+        thickness: 10,
+        color: 0
+      }
+    });
+
+    expect(Meteor.Shapes.insert).toHaveBeenCalledWith({
+      meetingId: 'meeting001',
+      whiteboardId: 'whiteboard001',
+      shape: {
+        wb_id: 'whiteboard001',
+        shape_type: 'rectangle',
+        status: 'DRAW_END',
+        id: 'shape001',
+        shape: {
+          type: 'rectangle',
+          status: 'DRAW_END',
+          points: [60.0, 17.0, 73.0, 57.5],
+          whiteboardId: 'whiteboard001',
+          id: 'shape001',
+          square: false,
+          transparency: false,
+          thickness: 10,
+          color: 0
+        }
+      }
+    });
+  });
+  it('should be handled correctly by insert() on calling addShapeToCollection() with a pencil being used', function () {
+    spyOn(Meteor.Shapes, 'find').and.returnValue({
+      count: function() {
+        return 1;
+      }
+    });
+    spyOn(Meteor.Shapes, 'insert');
+
+    addShapeToCollection('meeting001', 'whiteboard001', {
+      wb_id: 'whiteboard001',
+      shape_type: 'pencil',
+      status: 'DRAW_START',
+      id: 'shape001',
+      shape: {
+        type: 'pencil',
+        status: 'DRAW_START',
+        points: [35.8, 63.6, 36.1, 63.4, 36.2, 63.2],
+        whiteboardId: 'whiteboard001',
+        id: 'shape001',
+        square: undefined,
+        transparency: false,
+        thickness: 10,
+        color: 0
+      }
+    });
+
+    expect(Meteor.Shapes.insert).toHaveBeenCalledWith({
+      meetingId: 'meeting001',
+      whiteboardId: 'whiteboard001',
+      shape: {
+        wb_id: 'whiteboard001',
+        shape_type: 'pencil',
+        status: 'DRAW_START',
+        id: 'shape001',
+        shape: {
+          type: 'pencil',
+          status: 'DRAW_START',
+          points: [35.8, 63.6, 36.1, 63.4, 36.2, 63.2],
+          whiteboardId: 'whiteboard001',
+          id: 'shape001',
+          square: undefined,
+          transparency: false,
+          thickness: 10,
+          color: 0
+        }
+      }
+    });
+  });
+
+  // removeAllShapesFromSlide()
+  it('should not be touched on calling removeAllShapesFromSlide() with undefined meetingId', function () {
+    spyOn(Meteor.Shapes, 'remove');
+    removeAllShapesFromSlide(undefined, 'whiteboard001');
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeAllShapesFromSlide() with undefined whiteboardId', function () {
+    spyOn(Meteor.Shapes, 'remove');
+    removeAllShapesFromSlide('meeting001', undefined);
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeAllShapesFromSlide() if there is no shapes on the whiteboard', function () {
+    spyOn(Meteor.Shapes, 'find').and.returnValue(undefined);
+    spyOn(Meteor.Shapes, 'remove');
+    removeAllShapesFromSlide('meeting001', 'whiteboard001');
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should be cleared on calling removeAllShapesFromSlide() if there are shapes on the whiteboard', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001')
+        return {
+          fetch: function() {
+            return [{shape: {id: 'shape001'}}];
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001')
+        return {
+          _id: 'doc001'
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'remove');
+    removeAllShapesFromSlide('meeting001', 'whiteboard001');
+    expect(Meteor.Shapes.remove).toHaveBeenCalledWith('doc001');
+  });
+
+  // removeShapeFromSlide()
+  it('should not be touched on calling removeShapeFromSlide() with undefined meetingId', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === undefined && doc.whiteboardId === 'whiteboard001')
+        return {
+          count: function() {
+            return 0;
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === undefined && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001')
+        return {
+          _id: 'doc001'
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'remove');
+
+    removeShapeFromSlide(undefined, 'whiteboard001', 'shape001');
+
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeShapeFromSlide() with undefined whiteboardId', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === undefined)
+        return {
+          count: function() {
+            return 0;
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === undefined && doc['shape.id'] === 'shape001')
+        return {
+          _id: 'doc001'
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'remove');
+
+    removeShapeFromSlide('meeting001', undefined, 'shape001');
+
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeShapeFromSlide() with undefined shapeId', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001')
+        return {
+          count: function() {
+            return 0;
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === undefined)
+        return {
+          _id: 'doc001'
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'remove');
+
+    removeShapeFromSlide('meeting001', 'whiteboard001', undefined);
+
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeShapeFromSlide() if there is no wanted shape on the whiteboard', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001')
+        return {
+          count: function() {
+            return 0;
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001')
+        return undefined;
+      else return {
+        _id: 'doc001'
+      };
+    });
+    spyOn(Meteor.Shapes, 'remove');
+
+    removeShapeFromSlide('meeting001', 'whiteboard001', undefined);
+
+    expect(Meteor.Shapes.remove).not.toHaveBeenCalled();
+  });
+  it('should be updated correctly on calling removeShapeFromSlide() with an existing shape', function () {
+    spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001')
+        return {
+          count: function() {
+            return 0;
+          }
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001')
+        return {
+          _id: 'doc001'
+        };
+      else return undefined;
+    });
+    spyOn(Meteor.Shapes, 'remove');
+    removeShapeFromSlide('meeting001', 'whiteboard001', 'shape001');
+    expect(Meteor.Shapes.remove).toHaveBeenCalledWith('doc001');
+  });
+
+  //----------------------------------------------------------------------
+  // presentation.coffee
+  //----------------------------------------------------------------------
+
+  it("should be handled correctly by insert() on calling addPresentationToCollection()", function (){
+    spyOn(Meteor.Presentations, "findOne").and.returnValue(undefined)
+
+    spyOn(Meteor.Presentations, "insert");
+
+    addPresentationToCollection("meeting001", {
+      id: "presentation001",
+      name: "Presentation 001",
+      current: true
+    });
+
+    expect(Meteor.Presentations.insert).toHaveBeenCalledWith({
+      meetingId: "meeting001",
+      presentation: {
+        id: "presentation001",
+        name: "Presentation 001",
+        current: true
+      },
+      pointer: {
+        x: 0.0,
+        y: 0.0
+      }
+    });
+  });
+
+  it("should be handled correctly on calling addPresentationToCollection() when presentation is already in the collection", function (){
+    spyOn(Meteor.Presentations, "findOne").and.returnValue({ _id: "dbid001" });
+
+    spyOn(Meteor.Presentations, "insert");
+
+    addPresentationToCollection("meeting001", {
+      id: "presentation001",
+      name: "Presentation 001",
+      current: true
+    });
+
+    expect(Meteor.Presentations.insert).not.toHaveBeenCalledWith({
+      meetingId: "meeting001",
+      presentation: {
+        id: "presentation001",
+        name: "Presentation 001",
+        current: true
+      },
+      pointer: {
+        x: 0.0,
+        y: 0.0
+      }
+    });
+  });
+
+  it("should be handled correctly by remove() on calling removePresentationFromCollection", function (){
+    spyOn(Meteor.Presentations, "findOne").and.returnValue({ _id: "dbid001" });
+    spyOn(Meteor.Presentations, "remove");
+
+    removePresentationFromCollection("meeting0001", "presentation001");
+
+    expect(Meteor.Presentations.remove).toHaveBeenCalled();
+  });
+
+  it("should be handled correctly by remove() on calling removePresentationFromCollection", function (){
+    spyOn(Meteor.Presentations, "findOne").and.returnValue(undefined);
+    spyOn(Meteor.Presentations, "remove");
+
+    removePresentationFromCollection("meeting0001", "presentation001");
+
+    expect(Meteor.Presentations.remove).not.toHaveBeenCalled();
+  });
+
+  //----------------------------------------------------------------------
+  // slides.coffee
+  //----------------------------------------------------------------------
+
+  // removeSlideFromCollection()
+  it('should not be touched on calling removeSlideFromCollection() with undefined meetingId', function () {
+    spyOn(Meteor.Slides, 'remove');
+    removeSlideFromCollection(undefined, 'presentation001/2');
+    expect(Meteor.Slides.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeSlideFromCollection() with undefined slideId', function () {
+    spyOn(Meteor.Slides, 'remove');
+    removeSlideFromCollection('meeting001', undefined);
+    expect(Meteor.Slides.remove).not.toHaveBeenCalled();
+  });
+  it('should not be touched on calling removeSlideFromCollection() with a slide that does not exist', function () {
+    spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'slide001')
+        return undefined;
+      else return {meetingId: 'meeting001'};
+    });
+    spyOn(Meteor.Slides, 'remove');
+    removeSlideFromCollection('meeting001', 'slide001');
+    expect(Meteor.Slides.remove).not.toHaveBeenCalled();
+  });
+  it('should be handled correctly by remove() on calling removeSlideFromCollection() with an existing slide', function () {
+    spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'slide001')
+        return {_id: 'doc001'};
+      else return undefined;
+    });
+    spyOn(Meteor.Slides, 'remove');
+    removeSlideFromCollection('meeting001', 'slide001');
+    expect(Meteor.Slides.remove).toHaveBeenCalledWith('doc001');
+  });
+
+  // addSlideToCollection()
+  it('should not be touched on calling addSlideToCollection() if the slide is already in the collection', function () {
+    spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'presentation001/2')
+        return {_id: 'doc001'};
+      else return undefined;
+    });
+    spyOn(Meteor.Slides, 'insert');
+    addSlideToCollection('meeting001', 'presentation001', {
+      id: 'presentation001/2'
+    });
+    expect(Meteor.Slides.insert).not.toHaveBeenCalled();
+  });
+  it('should be handled correctly by insert() on calling addSlideToCollection() with a brand new slide', function () {
+    spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) {
+      if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'presentation001/2')
+        return undefined;
+      else return {_id: 'doc001'};
+    });
+    spyOn(Meteor.Slides, 'insert');
+    addSlideToCollection('meeting001', 'presentation001', {
+      height_ratio: 100,
+      y_offset: 0,
+      num: 2,
+      x_offset: 0,
+      current: true,
+      png_uri: 'http://localhost/bigbluebutton/presentation/presentation001/png/2',
+      txt_uri: 'http://localhost/bigbluebutton/presentation/presentation001/textfiles/slide-2.txt',
+      id: 'presentation001/2',
+      width_ratio: 100,
+      swf_uri: 'http://localhost/bigbluebutton/presentation/presentation001/slide/2',
+      thumb_uri: 'http://localhost/bigbluebutton/presentation/presentation001/thumbnail/1',
+    });
+    expect(Meteor.Slides.insert).toHaveBeenCalledWith({
+      meetingId: 'meeting001',
+      presentationId: 'presentation001',
+        slide: {
+          height_ratio: 100,
+          y_offset: 0,
+          num: 2,
+          x_offset: 0,
+          current: true,
+          png_uri: 'http://localhost/bigbluebutton/presentation/presentation001/png/2',
+          txt_uri: 'http://localhost/bigbluebutton/presentation/presentation001/textfiles/slide-2.txt',
+          id: 'presentation001/2',
+          width_ratio: 100,
+          swf_uri: 'http://localhost/bigbluebutton/presentation/presentation001/slide/2',
+          thumb_uri: 'http://localhost/bigbluebutton/presentation/presentation001/thumbnail/1'
+        }
+    });
+  });
+});
diff --git a/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js b/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0f38a33d59c2513e6f8725e48b69997f7b642a9
--- /dev/null
+++ b/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js
@@ -0,0 +1,11 @@
+/*
+  Stub the logger
+*/
+
+Logger = {};
+Logger.prototype = {
+  constructor: Logger
+}
+Logger.info = function() {};
+Meteor.log = Logger;
+
diff --git a/labs/meteor-client/grunt-instructions.txt b/labs/meteor-client/grunt-instructions.txt
new file mode 100644
index 0000000000000000000000000000000000000000..90d57a5345088f9ecd952456e0761dc6395fa1c8
--- /dev/null
+++ b/labs/meteor-client/grunt-instructions.txt
@@ -0,0 +1,10 @@
+GRUNT
+
+Install all the dependencies (including grunt):
+$ npm install
+
+Running Grunt:
+-for the default:
+$ grunt
+
+
diff --git a/labs/meteor-client/log/.gitkeep b/labs/meteor-client/log/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/labs/meteor-client/package.json b/labs/meteor-client/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..65873d355a91fd03401c77e05639524182bf7436
--- /dev/null
+++ b/labs/meteor-client/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "BBB-HTML-CLIENT",
+  "scripts": {
+    "start": "meteor"
+  },
+  "devDependencies": {
+    "coffeelint": "^1.6.0",
+    "grunt": "^0.4.5",
+    "grunt-coffeelint": "0.0.13",
+    "grunt-contrib-coffee": "^0.11.1",
+    "grunt-contrib-jshint": "^0.10.0"
+  }
+}
+