Merge branch 'merging-html5-with-master' of https://github.com/antobinary/bigbluebutton

This commit is contained in:
Anton Georgiev 2014-08-20 18:57:55 +00:00
commit c9ad7035b1
112 changed files with 24705 additions and 707 deletions

View File

@ -14,6 +14,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;
@ -54,13 +55,18 @@ public class MeetingMessageHandler implements MessageHandler {
bbbGW.validateAuthToken(emm.meetingId, emm.userId, emm.token, emm.replyTo);
} else if (msg instanceof UserConnectedToGlobalAudio) {
UserConnectedToGlobalAudio emm = (UserConnectedToGlobalAudio) msg;
log.debug("Received UserConnectedToGlobalAudio toekn request. user id [{}]", emm.name);
log.info("Received UserConnectedToGlobalAudio token request. user id [{}]", emm.name);
bbbGW.userConnectedToGlobalAudio(emm.voiceConf, emm.userid, emm.name);
} else if (msg instanceof UserDisconnectedFromGlobalAudio) {
UserDisconnectedFromGlobalAudio emm = (UserDisconnectedFromGlobalAudio) msg;
log.debug("Received UserDisconnectedFromGlobalAudio toekn request. Meeting id [{}]", emm.name);
log.debug("Received UserDisconnectedFromGlobalAudio token request. Meeting id [{}]", emm.name);
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);

View File

@ -31,52 +31,41 @@ 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("\"", "");
//case getChatHistory
if(eventName.equalsIgnoreCase("get_chat_history")) {
String replyTo = meetingID + "/" + requesterID;
bbbGW.getChatHistory(meetingID, requesterID, replyTo);
}
else {
String chatType = messageObject.get("chat_type").toString().replace("\"", "");
String fromUserID = messageObject.get("from_userid").toString().replace("\"", "");
String fromUsername = messageObject.get("from_username").toString().replace("\"", "");
String fromColor = messageObject.get("from_color").toString().replace("\"", "");
String fromTime = messageObject.get("from_time").toString().replace("\"", "");
String fromTimezoneOffset = messageObject.get("from_tz_offset").toString().replace("\"", "");
String fromLang = messageObject.get("from_lang").toString().replace("\"", "");
String toUserID = messageObject.get("to_userid").toString().replace("\"", "");
String toUsername = messageObject.get("to_username").toString().replace("\"", "");
String chatText = messageObject.get("message").toString().replace("\"", "");
String chatType = messageObject.get("chat_type").toString().replace("\"", "");
String fromUserID = messageObject.get("from_userid").toString().replace("\"", "");
String fromUsername = messageObject.get("from_username").toString().replace("\"", "");
String fromColor = messageObject.get("from_color").toString().replace("\"", "");
String fromTime = messageObject.get("from_time").toString().replace("\"", "");
String fromTimezoneOffset = messageObject.get("from_tz_offset").toString().replace("\"", "");
String fromLang = messageObject.get("from_lang").toString().replace("\"", "");
String toUserID = messageObject.get("to_userid").toString().replace("\"", "");
String toUsername = messageObject.get("to_username").toString().replace("\"", "");
String chatText = messageObject.get("message").toString().replace("\"", "");
Map<String, String> map = new HashMap<String, String>();
map.put(ChatKeyUtil.CHAT_TYPE, chatType);
map.put(ChatKeyUtil.FROM_USERID, fromUserID);
map.put(ChatKeyUtil.FROM_USERNAME, fromUsername);
map.put(ChatKeyUtil.FROM_COLOR, fromColor);
map.put(ChatKeyUtil.FROM_TIME, fromTime);
map.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
map.put(ChatKeyUtil.FROM_LANG, fromLang);
map.put(ChatKeyUtil.TO_USERID, toUserID);
map.put(ChatKeyUtil.TO_USERNAME, toUsername);
map.put(ChatKeyUtil.MESSAGE, chatText);
Map<String, String> map = new HashMap<String, String>();
map.put(ChatKeyUtil.CHAT_TYPE, chatType);
map.put(ChatKeyUtil.FROM_USERID, fromUserID);
map.put(ChatKeyUtil.FROM_USERNAME, fromUsername);
map.put(ChatKeyUtil.FROM_COLOR, fromColor);
map.put(ChatKeyUtil.FROM_TIME, fromTime);
map.put(ChatKeyUtil.FROM_TZ_OFFSET, fromTimezoneOffset);
map.put(ChatKeyUtil.FROM_LANG, fromLang);
map.put(ChatKeyUtil.TO_USERID, toUserID);
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")) {
bbbGW.sendPublicMessage(meetingID, requesterID, map);
} else if(eventName.equalsIgnoreCase("private_chat_message_event")
|| eventName.equalsIgnoreCase("send_private_chat_message")) {
bbbGW.sendPrivateMessage(meetingID, requesterID, map);
}
if(eventName.equalsIgnoreCase("public_chat_message_event")
|| eventName.equalsIgnoreCase("send_public_chat_message")) {
bbbGW.sendPublicMessage(meetingID, requesterID, map);
} else if(eventName.equalsIgnoreCase("private_chat_message_event")
|| eventName.equalsIgnoreCase("send_private_chat_message")) {
bbbGW.sendPrivateMessage(meetingID, requesterID, map);
}
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}
@ -72,4 +74,6 @@ public class MessageFromJsonConverter {
String id = payload.get(Constants.KEEP_ALIVE_ID).getAsString();
return new KeepAliveMessage(id);
}
//private static IMessage processGetAllMeetings(JsonObject)
}

View File

@ -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";
@ -51,4 +52,7 @@ public class MessagingConstants {
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";
}

View File

@ -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("user_raised_hand_message")){
bbbInGW.userRaiseHand(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("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);
}
}
}

View File

@ -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";
}

View File

@ -0,0 +1,45 @@
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);
}
}
}
}

View File

@ -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");
@ -89,7 +94,7 @@ public class VoiceService {
bbbInGW.lockUser(meetingID, requesterID, userid, lock);
}
public void kickUSer(Map<String, Object> msg) {
public void kickUSer(Map<String, Object> msg) { //change to kickUser
String userid = (String) msg.get("userId");
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();

View File

@ -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");
}
}
}
}
}

View File

@ -12,6 +12,7 @@ public interface IBigBlueButtonInGW {
void endAllMeetings();
void createMeeting2(String meetingID, String meetingName, boolean recorded, String voiceBridge, long duration);
void destroyMeeting(String meetingID);
void getAllMeetings(String meetingID);
void lockSettings(String meetingID, Boolean locked, Map<String, Boolean> lockSettigs);

View File

@ -16,6 +16,7 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor {
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
}
@ -96,5 +97,40 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor {
}
}
}
}
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 name = meetings.get(arr(i)).head.getMeetingName()
val recorded = meetings.get(arr(i)).head.getRecordedStatus()
var info = new MeetingInfo(id, name, recorded)
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())
//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))
}
}

View File

@ -43,7 +43,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));
}
@ -231,9 +236,10 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
val thumbnail = presBaseUrl + "/thumbnail/" + i
val swfUri = presBaseUrl + "/slide/" + 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)
}

View File

@ -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
}
@ -1300,6 +1302,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)
@ -2079,4 +2087,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)
}
}

View File

@ -27,11 +27,19 @@ class MeetingActor(val meetingID: String, meetingName: String, val recorded: Boo
var recording = false;
var muted = false;
var meetingEnded = false
def getMeetingName():String = {
meetingName
}
def getRecordedStatus():Boolean = {
recorded
}
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) {
@ -41,83 +49,83 @@ class MeetingActor(val meetingID: String, meetingName: String, val recorded: Boo
}
def act() = {
loop {
react {
case "StartTimer" => handleStartTimer
case "Hello" => handleHello
case "MonitorNumberOfWebUsers" => handleMonitorNumberOfWebUsers()
case msg: ValidateAuthToken => handleValidateAuthToken(msg)
case msg: RegisterUser => handleRegisterUser(msg)
case msg: VoiceUserJoined => handleVoiceUserJoined(msg)
case msg: VoiceUserLeft => handleVoiceUserLeft(msg)
case msg: VoiceUserMuted => handleVoiceUserMuted(msg)
case msg: VoiceUserTalking => handleVoiceUserTalking(msg)
case msg: UserJoining => handleUserJoin(msg)
case msg: UserLeaving => handleUserLeft(msg)
case msg: AssignPresenter => handleAssignPresenter(msg)
case msg: GetUsers => handleGetUsers(msg)
case msg: ChangeUserStatus => handleChangeUserStatus(msg)
case msg: UserRaiseHand => handleUserRaiseHand(msg)
case msg: UserLowerHand => handleUserLowerHand(msg)
case msg: UserShareWebcam => handleUserShareWebcam(msg)
case msg: UserUnshareWebcam => handleUserunshareWebcam(msg)
case msg: MuteMeetingRequest => handleMuteMeetingRequest(msg)
case msg: MuteAllExceptPresenterRequest => handleMuteAllExceptPresenterRequest(msg)
case msg: IsMeetingMutedRequest => handleIsMeetingMutedRequest(msg)
case msg: MuteUserRequest => handleMuteUserRequest(msg)
case msg: EjectUserRequest => handleEjectUserRequest(msg)
case msg: SetLockSettings => handleSetLockSettings(msg)
case msg: InitLockSettings => handleInitLockSettings(msg)
case msg: GetChatHistoryRequest => handleGetChatHistoryRequest(msg)
case msg: SendPublicMessageRequest => handleSendPublicMessageRequest(msg)
case msg: SendPrivateMessageRequest => handleSendPrivateMessageRequest(msg)
case msg: UserConnectedToGlobalAudio => handleUserConnectedToGlobalAudio(msg)
case msg: UserDisconnectedFromGlobalAudio => handleUserDisconnectedFromGlobalAudio(msg)
case msg: GetCurrentLayoutRequest => handleGetCurrentLayoutRequest(msg)
case msg: BroadcastLayoutRequest => handleBroadcastLayoutRequest(msg)
case msg: InitializeMeeting => handleInitializeMeeting(msg)
case msg: ClearPresentation => handleClearPresentation(msg)
case msg: PresentationConversionUpdate => handlePresentationConversionUpdate(msg)
case msg: PresentationPageCountError => handlePresentationPageCountError(msg)
case msg: PresentationSlideGenerated => handlePresentationSlideGenerated(msg)
case msg: PresentationConversionCompleted => handlePresentationConversionCompleted(msg)
case msg: RemovePresentation => handleRemovePresentation(msg)
case msg: GetPresentationInfo => handleGetPresentationInfo(msg)
case msg: SendCursorUpdate => handleSendCursorUpdate(msg)
case msg: ResizeAndMoveSlide => handleResizeAndMoveSlide(msg)
case msg: GotoSlide => handleGotoSlide(msg)
case msg: SharePresentation => handleSharePresentation(msg)
case msg: GetSlideInfo => handleGetSlideInfo(msg)
case msg: PreuploadedPresentations => handlePreuploadedPresentations(msg)
case msg: PreCreatedPoll => handlePreCreatedPoll(msg)
case msg: CreatePoll => handleCreatePoll(msg)
case msg: UpdatePoll => handleUpdatePoll(msg)
case msg: DestroyPoll => handleDestroyPoll(msg)
case msg: RemovePoll => handleRemovePoll(msg)
case msg: SharePoll => handleSharePoll(msg)
case msg: StopPoll => handleStopPoll(msg)
case msg: StartPoll => handleStartPoll(msg)
case msg: ClearPoll => handleClearPoll(msg)
case msg: GetPolls => handleGetPolls(msg)
case msg: RespondToPoll => handleRespondToPoll(msg)
case msg: HidePollResult => handleHidePollResult(msg)
case msg: ShowPollResult => handleShowPollResult(msg)
case msg: SendWhiteboardAnnotationRequest => handleSendWhiteboardAnnotationRequest(msg)
case msg: GetWhiteboardShapesRequest => handleGetWhiteboardShapesRequest(msg)
case msg: ClearWhiteboardRequest => handleClearWhiteboardRequest(msg)
case msg: UndoWhiteboardRequest => handleUndoWhiteboardRequest(msg)
case msg: EnableWhiteboardRequest => handleEnableWhiteboardRequest(msg)
case msg: IsWhiteboardEnabledRequest => handleIsWhiteboardEnabledRequest(msg)
case msg: SetRecordingStatus => handleSetRecordingStatus(msg)
case msg: GetRecordingStatus => handleGetRecordingStatus(msg)
case msg: VoiceRecording => handleVoiceRecording(msg)
case msg: EndMeeting => handleEndMeeting(msg)
case StopMeetingActor => exit
case _ => // do nothing
}
}
}
loop {
react {
case "StartTimer" => handleStartTimer
case "Hello" => handleHello
case "MonitorNumberOfWebUsers" => handleMonitorNumberOfWebUsers()
case msg: ValidateAuthToken => handleValidateAuthToken(msg)
case msg: RegisterUser => handleRegisterUser(msg)
case msg: VoiceUserJoined => handleVoiceUserJoined(msg)
case msg: VoiceUserLeft => handleVoiceUserLeft(msg)
case msg: VoiceUserMuted => handleVoiceUserMuted(msg)
case msg: VoiceUserTalking => handleVoiceUserTalking(msg)
case msg: UserJoining => handleUserJoin(msg)
case msg: UserLeaving => handleUserLeft(msg)
case msg: AssignPresenter => handleAssignPresenter(msg)
case msg: GetUsers => handleGetUsers(msg)
case msg: ChangeUserStatus => handleChangeUserStatus(msg)
case msg: UserRaiseHand => handleUserRaiseHand(msg)
case msg: UserLowerHand => handleUserLowerHand(msg)
case msg: UserShareWebcam => handleUserShareWebcam(msg)
case msg: UserUnshareWebcam => handleUserunshareWebcam(msg)
case msg: MuteMeetingRequest => handleMuteMeetingRequest(msg)
case msg: MuteAllExceptPresenterRequest => handleMuteAllExceptPresenterRequest(msg)
case msg: IsMeetingMutedRequest => handleIsMeetingMutedRequest(msg)
case msg: MuteUserRequest => handleMuteUserRequest(msg)
case msg: EjectUserRequest => handleEjectUserRequest(msg)
case msg: SetLockSettings => handleSetLockSettings(msg)
case msg: InitLockSettings => handleInitLockSettings(msg)
case msg: GetChatHistoryRequest => handleGetChatHistoryRequest(msg)
case msg: SendPublicMessageRequest => handleSendPublicMessageRequest(msg)
case msg: SendPrivateMessageRequest => handleSendPrivateMessageRequest(msg)
case msg: UserConnectedToGlobalAudio => handleUserConnectedToGlobalAudio(msg)
case msg: UserDisconnectedFromGlobalAudio => handleUserDisconnectedFromGlobalAudio(msg)
case msg: GetCurrentLayoutRequest => handleGetCurrentLayoutRequest(msg)
case msg: BroadcastLayoutRequest => handleBroadcastLayoutRequest(msg)
case msg: InitializeMeeting => handleInitializeMeeting(msg)
case msg: ClearPresentation => handleClearPresentation(msg)
case msg: PresentationConversionUpdate => handlePresentationConversionUpdate(msg)
case msg: PresentationPageCountError => handlePresentationPageCountError(msg)
case msg: PresentationSlideGenerated => handlePresentationSlideGenerated(msg)
case msg: PresentationConversionCompleted => handlePresentationConversionCompleted(msg)
case msg: RemovePresentation => handleRemovePresentation(msg)
case msg: GetPresentationInfo => handleGetPresentationInfo(msg)
case msg: SendCursorUpdate => handleSendCursorUpdate(msg)
case msg: ResizeAndMoveSlide => handleResizeAndMoveSlide(msg)
case msg: GotoSlide => handleGotoSlide(msg)
case msg: SharePresentation => handleSharePresentation(msg)
case msg: GetSlideInfo => handleGetSlideInfo(msg)
case msg: PreuploadedPresentations => handlePreuploadedPresentations(msg)
case msg: PreCreatedPoll => handlePreCreatedPoll(msg)
case msg: CreatePoll => handleCreatePoll(msg)
case msg: UpdatePoll => handleUpdatePoll(msg)
case msg: DestroyPoll => handleDestroyPoll(msg)
case msg: RemovePoll => handleRemovePoll(msg)
case msg: SharePoll => handleSharePoll(msg)
case msg: StopPoll => handleStopPoll(msg)
case msg: StartPoll => handleStartPoll(msg)
case msg: ClearPoll => handleClearPoll(msg)
case msg: GetPolls => handleGetPolls(msg)
case msg: RespondToPoll => handleRespondToPoll(msg)
case msg: HidePollResult => handleHidePollResult(msg)
case msg: ShowPollResult => handleShowPollResult(msg)
case msg: SendWhiteboardAnnotationRequest => handleSendWhiteboardAnnotationRequest(msg)
case msg: GetWhiteboardShapesRequest => handleGetWhiteboardShapesRequest(msg)
case msg: ClearWhiteboardRequest => handleClearWhiteboardRequest(msg)
case msg: UndoWhiteboardRequest => handleUndoWhiteboardRequest(msg)
case msg: EnableWhiteboardRequest => handleEnableWhiteboardRequest(msg)
case msg: IsWhiteboardEnabledRequest => handleIsWhiteboardEnabledRequest(msg)
case msg: SetRecordingStatus => handleSetRecordingStatus(msg)
case msg: GetRecordingStatus => handleGetRecordingStatus(msg)
case msg: VoiceRecording => handleVoiceRecording(msg)
case msg: EndMeeting => handleEndMeeting(msg)
case StopMeetingActor => exit
case _ => // do nothing
}
}
}
def hasMeetingEnded():Boolean = {
meetingEnded
@ -139,15 +147,15 @@ class MeetingActor(val meetingID: String, meetingName: String, val recorded: Boo
def webUserJoined() {
if (users.numWebUsers > 0) {
lastWebUserLeftOn = 0
}
}
}
def startCheckingIfWeNeedToEndVoiceConf() {
if (users.numWebUsers == 0) {
lastWebUserLeftOn = timeNowInMinutes
println("*************** MonitorNumberOfWebUsers started ******************")
println("*************** MonitorNumberOfWebUsers started ******************")
scheduleEndVoiceConference()
}
}
}
def handleMonitorNumberOfWebUsers() {

View File

@ -498,3 +498,7 @@ case class IsWhiteboardEnabledRequest(
requesterID: String,
replyTo: String
) extends InMessage
case class GetAllMeetingsRequest(
meetingID: String /** Not used. Just to satisfy trait **/
) extends InMessage

View File

@ -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"
}

View File

@ -642,7 +642,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(

View File

@ -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)
startTime: Long = 0, endTime: Long = 0)
case class MeetingInfo(meetingID: String, meetingName: String, recorded: Boolean)

View File

@ -52,6 +52,7 @@ trait UsersApp {
}
}
def handleMuteAllExceptPresenterRequest(msg: MuteAllExceptPresenterRequest) {
meetingMuted = msg.mute
outGW.send(new MeetingMuted(meetingID, recorded, meetingMuted))
@ -60,7 +61,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))
@ -72,9 +73,27 @@ trait UsersApp {
def handleValidateAuthToken(msg: ValidateAuthToken) {
// println("*************** Got ValidateAuthToken message ********************" )
regUsers.get (msg.userId) match {
case Some(u) => outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, true, msg.correlationId))
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
this ! (new GetPresentationInfo(meetingID, msg.userId, replyTo))
}
case None => outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, false, msg.correlationId))
}
}
}
def handleRegisterUser(msg: RegisterUser) {
@ -113,8 +132,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)) {
@ -194,7 +228,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))

View File

@ -27,21 +27,21 @@ trait WhiteboardApp {
wbModel.addAnnotation(wbId, shape)
} else if ((WhiteboardKeyUtil.PENCIL_TYPE == shapeType)
&& (WhiteboardKeyUtil.DRAW_START_STATUS == status)) {
// println("Received pencil draw start status")
wbModel.addAnnotation(wbId, shape)
println("Received pencil draw start status")
wbModel.addAnnotation(wbId, shape)
} else if ((WhiteboardKeyUtil.DRAW_END_STATUS == status)
&& ((WhiteboardKeyUtil.RECTANGLE_TYPE == shapeType)
|| (WhiteboardKeyUtil.ELLIPSE_TYPE == shapeType)
|| (WhiteboardKeyUtil.TRIANGLE_TYPE == shapeType)
|| (WhiteboardKeyUtil.LINE_TYPE == shapeType))) {
// println("Received [" + shapeType +"] draw end status")
wbModel.addAnnotation(wbId, shape)
|| (WhiteboardKeyUtil.TRIANGLE_TYPE == shapeType)
|| (WhiteboardKeyUtil.LINE_TYPE == shapeType))) {
println("Received [" + shapeType +"] draw end status")
wbModel.addAnnotation(wbId, shape)
} else if (WhiteboardKeyUtil.TEXT_TYPE == shapeType) {
// println("Received [" + shapeType +"] modify text status")
wbModel.modifyText(wbId, shape)
} else {
// println("Received UNKNOWN whiteboard shape!!!!. status=[" + status + "], shapeType=[" + shapeType + "]")
}
println("Received [" + shapeType +"] modify text status")
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 + "]")
@ -52,20 +52,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) {

View File

@ -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)
}
}

View File

@ -103,7 +103,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)
@ -112,5 +112,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)
}
}

View File

@ -39,5 +39,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>

View File

@ -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
@ -45,12 +45,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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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/>.
<beans:bean id="voiceHandler" class="org.bigbluebutton.conference.service.voice.VoiceHandler">
</beans:bean>
<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>

View File

@ -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

View File

@ -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']

View File

@ -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()

View File

@ -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,90 +69,94 @@ class PresentationService {
if( file.isDirectory() )
presentationsList.add( file.name )
}
}
}
return presentationsList
}
def getPresentationDir = {
return presentationDir
def getPresentationDir = {
return presentationDir
}
def processUploadedPresentation = {uploadedPres ->
def processUploadedPresentation = {uploadedPres ->
// Run conversion on another thread.
new Timer().runAfter(1000)
new Timer().runAfter(1000)
{
documentConversionService.processDocument(uploadedPres)
documentConversionService.processDocument(uploadedPres)
}
}
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;
@ -161,4 +165,4 @@ class PngFilter implements FilenameFilter {
public boolean accept(File dir, String name) {
return (name.endsWith(".png"));
}
}
}

View File

@ -66,7 +66,7 @@ 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);
createPngImages(pres);
createTextFiles(pres);
createThumbnails(pres);
notifier.sendConversionCompletedMessage(pres);

View File

@ -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;

View File

@ -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();
}

2
client/bbb-html5-client/assets/css/navbar.less Normal file → Executable file
View File

@ -69,7 +69,7 @@
}
.navbar-btn-group-right {
dispaly:block;
display:block;
float:right;
}
}

View File

@ -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"

View File

@ -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.')

View File

@ -35,4 +35,8 @@ module.exports = class Controller
# @clientProxy.endMeeting()
sendingChat: (data) =>
@messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data)
@messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data)
sendingUsersMessage: (data) =>
@messageBus.sendingToRedis(config.redis.channels.toBBBApps.users, data)

View File

@ -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)
@pubSub.publishing(channel, message)

View File

@ -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 'get_presentation_info_reply'
#filter for the current=true page on the server-side
currentPage = null
numCurrentPage = null
presentations = message.payload?.presentations
if message.header?.name is 'validate_auth_token_reply'
if message.payload?.valid is "true"
for presentation in presentations
pages = presentation.pages
#TODO use the message library for these messages. Perhaps put it in Modules?!
for page in pages
if page.current is true
currentPage = page
numCurrentPage = page.num
joinMeetingMessage = {
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
"user_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": "user_joined_event"
"name": "get_whiteboard_shapes_request"
}
}
# the user joins the meeting
@publishing(config.redis.channels.toBBBApps.whiteboard, requestMessage)
@pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(joinMeetingMessage))
console.log "just published the joinMeetingMessage in RedisPubSub"
#strip off excess data, leaving only the current slide information
message.payload.currentPage = currentPage
message.payload.presentations = null
message.header.name = "presentation_page"
#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"
}
}
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
@pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(getUsersMessage))
console.log "just published the getUsersMessage in RedisPubSub"
#strip off excess data, leaving only the current slide information
message.payload.currentPage = currentPage
message.payload.presentation = null
message.header.name = "presentation_page"
#get the chat history
getChatHistory = {
"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_chat_history"
}
}
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"
@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'
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

View File

@ -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

View File

@ -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
globals.events.trigger("connection:load_users", users)
@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)
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
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) =>
console.log "socket on: all_shapes" + allShapesEventObject
globals.events.trigger("connection:all_shapes", allShapesEventObject)
#@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 "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
@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)
# 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
globals.events.trigger("connection:whiteboard_draw_event", 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: ->

22
client/bbb-html5-client/public/js/models/user.coffee Normal file → Executable file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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>

1
labs/demos/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -1,7 +1,7 @@
{
"settings": {
"IP": "http://192.168.0.203",
"IP": "http://192.168.0.232",
"PORT": "4000",
"salt": "74a91f30f165423067bf3039722e33e0"
"salt": "c7faeb82a786bd71134b61833b0ec4af"
}
}
}

9
labs/demos/lib/handlers.coffee Normal file → Executable file
View File

@ -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')
@ -19,6 +20,8 @@ login = (req, resp) ->
#calling createapi
bbbapi.create(createParams, serverAndSecret, {}, (errorOuter, responseOuter, bodyOuter) ->
#console.log JSON.stringify(response)
console.log "\n\nouterXML=" + responseOuter.body
console.log "\nerrorOuter=" + JSON.stringify errorOuter
bbbapi.join(joinParams, serverAndSecret, {}, (error, response, body) ->
if error
console.log error
@ -38,8 +41,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/meeting_id=" + meeting_id + "&user_id=" +
user_id + "&auth_token=" + auth_token
json =
resp.json({
@ -49,7 +52,7 @@ login = (req, resp) ->
failure: {
message: "Something went terribly wrong"
}
})
})
)
)
)

View File

@ -11,19 +11,19 @@ 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"
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}

2
labs/demos/views/index.html Normal file → Executable file
View File

@ -1,7 +1,7 @@
<!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/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>
</head>

6
labs/meteor-client/.gitignore vendored Executable file
View File

@ -0,0 +1,6 @@
packages
build/
npm-debug.log
../node_modules/
../node_modules/hiredis/
../node_modules/redis/

1
labs/meteor-client/.meteor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
local

View File

@ -0,0 +1,15 @@
# 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
redis
npm
less
bootstrap-3
iron-router
underscore
amplify
raphaeljs-package

View File

@ -0,0 +1 @@
0.8.1.3

View File

@ -0,0 +1,347 @@
var presenterUserID = "";
var registerListeners = function() {
console.log("Listening for events.");
BBB.listen("QueryPresentationsReplyEvent", function(bbbEvent) {
console.log("Number of presentations [" + bbbEvent.presentations.length + "]. First presentation [" + bbbEvent.presentations[0] + "].");
});
BBB.listen("OpenExternalFileUploadWindowEvent", function(bbbEvent) {
console.log("Open file upload dialog. Max file size is [" + bbbEvent.maxFileSize + "].");
});
BBB.listen("UserKickedOutEvent", function(bbbEvent) {
console.log("User has been kicked [" + bbbEvent.userID + "].");
});
BBB.listen("SwitchedLayoutEvent", function(bbbEvent) {
console.log("New Layout [" + bbbEvent.layoutID + "].");
});
BBB.listen("NewRoleEvent", function(bbbEvent) {
console.log("New Role Event [amIPresenter=" + bbbEvent.amIPresenter + ",role=" + bbbEvent.role + ",newPresenterUserID=" + bbbEvent.newPresenterUserID + "].");
});
BBB.listen("SwitchedPresenterEvent", function(bbbEvent) {
console.log("Switched Presenter [amIPresenter=" + bbbEvent.amIPresenter + ",role=" + bbbEvent.role + ",newPresenterUserID=" + bbbEvent.newPresenterUserID + "].");
presenterUserID = bbbEvent.newPresenterUserID;
if (bbbEvent.amIPresenter) {
console.log("*** I am presenter. Am I publishing webcam?");
BBB.listen("AmISharingCamQueryResponse", function(bbbEvent2) {
console.log("AmISharingCamQueryResponse [isPublishing=" + bbbEvent2.isPublishing + ",camIndex=" + bbbEvent2.camIndex + "]");
});
BBB.amISharingWebcam();
BBB.amISharingWebcam(function(bbbEvent3) {
console.log("amISharingWebcam [isPublishing=" + bbbEvent3.isPublishing
+ ",camIndex=" + bbbEvent3.camIndex
+ ",camWidth=" + bbbEvent3.camWidth
+ ",camHeight=" + bbbEvent3.camHeight
+ ",camKeyFrameInterval=" + bbbEvent3.camKeyFrameInterval
+ ",camModeFps=" + bbbEvent3.camModeFps
+ ",camQualityBandwidth=" + bbbEvent3.camQualityBandwidth
+ ",camQualityPicture=" + bbbEvent3.camQualityPicture
+ "]");
if (bbbEvent3.isPublishing) {
CAM_PREVIEW.stopPreviewCamera(bbbEvent3.avatarURL);
CAM_PREVIEW.previewCamera(bbbEvent3.camIndex, bbbEvent3.camWidth, bbbEvent3.camHeight, bbbEvent3.camKeyFrameInterval,
bbbEvent3.camModeFps, bbbEvent3.camQualityBandwidth, bbbEvent3.camQualityPicture, bbbEvent3.avatarURL);
}
});
} else {
console.log("*** I am NOT presenter. Is new presenter publishing webcam?");
BBB.listen("IsUserPublishingCamResponse", function(bbbEvent4) {
console.log("IsUserPublishingCamResponse [isUserPublishing=" + bbbEvent4.isUserPublishing
+ ",uri=" + bbbEvent4.uri
+ ",streamName=" + bbbEvent4.streamName + "]");
});
BBB.isUserSharingWebcam(bbbEvent.newPresenterUserID);
BBB.isUserSharingWebcam(bbbEvent.newPresenterUserID, function(bbbEvent5) {
console.log("isUserSharingWebcam [isUserPublishing=" + bbbEvent5.isUserPublishing
+ ",uri=" + bbbEvent5.uri
+ ",streamName=" + bbbEvent5.streamName + "]");
if (presenterUserID == bbbEvent.userID) {
CAM_VIEW.stopViewWebcamStream(bbbEvent.avatarURL);
CAM_VIEW.viewWebcamStream(bbbEvent.uri, bbbEvent.streamName, bbbEvent5.avatarURL);
}
});
CAM_PREVIEW.stopPreviewCamera(bbbEvent.avatarURL);
}
});
BBB.listen("UserLeftEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + "] has left.");
});
BBB.listen("UserJoinedEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + ", [" + bbbEvent.userName + "] has joined.");
});
BBB.listen("NewPublicChatEvent", function(bbbEvent) {
console.log("Received NewPublicChatEvent [" + bbbEvent.message + "]");
});
BBB.listen("NewPrivateChatEvent", function(bbbEvent) {
console.log("Received NewPrivateChatEvent event");
});
BBB.listen("UserJoinedVoiceEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + "] had joined the voice conference.");
});
BBB.listen("UserLeftVoiceEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + "has left the voice conference.");
});
BBB.listen("UserVoiceMutedEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + "] is muted [" + bbbEvent.muted + "]");
});
BBB.listen("UserLockedVoiceEvent", function(bbbEvent) {
console.log("User [" + bbbEvent.userID + "] is locked [" + bbbEvent.locked + "]");
});
BBB.listen("CamStreamSharedEvent", function(bbbEvent) {
console.log("User CamStreamSharedEvent [" + bbbEvent.uri + "," + bbbEvent.streamName + "]");
if (presenterUserID == bbbEvent.userID) {
CAM_VIEW.stopViewWebcamStream(bbbEvent.avatarURL);
CAM_VIEW.viewWebcamStream(bbbEvent.uri, bbbEvent.streamName, bbbEvent.avatarURL);
}
});
BBB.listen("BroadcastingCameraStartedEvent", function(bbbEvent) {
console.log("User BroadcastingCameraStartedEvent [" + bbbEvent.camIndex + "] [" + bbbEvent.camWidth + "]");
if (bbbEvent.isPresenter) {
CAM_PREVIEW.stopPreviewCamera(bbbEvent.avatarURL);
CAM_PREVIEW.previewCamera(bbbEvent.camIndex, bbbEvent.camWidth, bbbEvent.camHeight, bbbEvent.camKeyFrameInterval,
bbbEvent.camModeFps, bbbEvent.camQualityBandwidth, bbbEvent.camQualityPicture, bbbEvent.avatarURL);
}
});
BBB.listen("BroadcastingCameraStoppedEvent", function(bbbEvent) {
console.log("User BroadcastingCameraStoppedEvent ]");
CAM_PREVIEW.stopPreviewCamera(bbbEvent.avatarURL);
});
console.log("Listen Presentation Updates");
BBB.listen("OfficeDocConversionSuccessEvent", function(bbbEvent) {
console.log("Successfully converted Office document. : " + JSON.stringify(bbbEvent));
});
BBB.listen("OfficeDocConversionFailedEvent", function(bbbEvent) {
console.log("Failed to convert Office document. : " + JSON.stringify(bbbEvent));
});
BBB.listen("SupportedDocEvent", function(bbbEvent) {
console.log("Uploaded presentation file type is supported. : " + JSON.stringify(bbbEvent));
});
BBB.listen("UnsupportedDocEvent", function(bbbEvent) {
console.log("Uploaded presentation file type is unsupported. : " + JSON.stringify(bbbEvent));
});
BBB.listen("PageCountFailedEvent", function(bbbEvent) {
console.log("Failed to determine number of pages for the uploaded presentation. : " + JSON.stringify(bbbEvent));
});
BBB.listen("ThumbnailsUpdateEvent", function(bbbEvent) {
console.log("Generating thumbnails for uploaded presentation. : " + JSON.stringify(bbbEvent));
});
BBB.listen("PageCountExceededEvent", function(bbbEvent) {
console.log("Uploaded presentation had exceeded max number of pages. : " + JSON.stringify(bbbEvent));
});
BBB.listen("ConversionSuccessEvent", function(bbbEvent) {
console.log("Successfully converted uploaded presentation. : " + JSON.stringify(bbbEvent));
});
BBB.listen("ConversionProgressEvent", function(bbbEvent) {
console.log("Progress update on conversion process. : " + JSON.stringify(bbbEvent));
});
}
var leaveVoiceConference2 = function () {
BBB.leaveVoiceConference();
}
var joinVoiceConference2 = function () {
BBB.joinVoiceConference();
}
var amIPresenterAsync = function() {
BBB.listen("AmIPresenterQueryResponse", function(bbbEvent) {
console.log("Received AmIPresenterQueryResponse event [" + bbbEvent.amIPresenter + "]");
});
BBB.amIPresenter();
}
var amIPresenterSync = function() {
BBB.amIPresenter(function(amIPresenter) {
console.log("Am I Presenter = " + amIPresenter);
});
}
var getMyUserInfoAsynch = function() {
BBB.listen("GetMyUserInfoResponse", function(bbbEvent) {
console.log("User info response [myUserID=" + bbbEvent.myUserID
+ ",myUsername=" + bbbEvent.myUsername + ",myAvatarURL=" + bbbEvent.myAvatarURL
+ ",myRole=" + bbbEvent.myRole + ",amIPresenter=" + bbbEvent.amIPresenter
+ ",dialNumber=" + bbbEvent.dialNumber + ",voiceBridge=" + bbbEvent.voiceBridge + "].");
for(var key in bbbEvent.customdata){
console.log(key + " " + bbbEvent.customdata[key]);
}
});
BBB.getMyUserInfo();
}
var getMyUserInfoSynch = function() {
BBB.getMyUserInfo(function(userInfo) {
console.log("User info callback [myUserID=" + userInfo.myUserID
+ ",myUsername=" + userInfo.myUsername + ",myAvatarURL=" + userInfo.myAvatarURL
+ ",myRole=" + userInfo.myRole + ",amIPresenter=" + userInfo.amIPresenter
+ ",dialNumber=" + userInfo.dialNumber + ",voiceBridge=" + userInfo.voiceBridge + "].");
for(var key in userInfo.customdata){
console.log(key + " " + userInfo.customdata[key]);
}
});
}
var getMyRoleAsynch = function() {
BBB.listen("GetMyRoleResponse", function(bbbEvent) {
console.log("Received GetMyRoleResponse event [" + bbbEvent.myRole + "]");
});
BBB.getMyRole();
}
var getMyRoleSynch = function() {
BBB.getMyRole(function(myRole) {
console.log("My role = " + myRole);
});
}
var getMyUserID = function() {
BBB.getMyUserID(function(userID) {
console.log("My user ID = [" + userID + "]");
});
}
var getMeetingID = function() {
BBB.getMeetingID(function(meetingID) {
console.log("Meeting ID = [" + meetingID + "]");
});
}
var raiseHand = function(raiseHand) {
BBB.raiseHand(raiseHand);
}
var muteMe = function() {
BBB.muteMe();
}
var unmuteMe = function() {
BBB.unmuteMe();
}
var muteAll = function() {
BBB.muteAll();
}
var unmuteAll = function() {
BBB.unmuteAll();
}
var switchLayout = function(newLayout) {
BBB.switchLayout(newLayout);
}
var lockLayout = function(lock) {
BBB.lockLayout(lock);
}
var queryListOfPresentations = function() {
BBB.queryListOfPresentations();
}
var displayPresentation = function(presentationID) {
BBB.displayPresentation(presentationID);
}
var deletePresentation = function(presentationID) {
BBB.deletePresentation(presentationID);
}
var sendPublicChat = function () {
var message = "Hello from the Javascript API";
BBB.sendPublicChatMessage('0x7A7A7A', "en", message);
}
var sendPrivateChat = function () {
var message = "ECHO: " + bbbEvent.message;
BBB.sendPrivateChatMessage(bbbEvent.fromColor, bbbEvent.fromLang, message, bbbEvent.fromUserID);
}
var webcamViewStandaloneAppReady = function() {
console.log("WebcamViewStandalone App is ready.");
BBB.getPresenterUserID(function(puid) {
if (puid == "") {
console.log("There is no presenter in the meeting");
} else {
console.log("The presenter user id is [" + puid + "]");
// Is presenter sharing webcam? If so, get the webcam stream and display.
}
});
}
var webcamPreviewStandaloneAppReady = function() {
console.log("WebcamPreviewStandalone App is ready.");
BBB.getPresenterUserID(function(puid) {
if (puid == "") {
console.log("There is no presenter in the meeting");
} else {
console.log("The presenter user id is [" + puid + "]");
}
});
// Am I presenter? If so, am I publishing my camera? If so, display my camera.
}
var uploadPresentation = function() {
console.log("uploadPresentation");
BBB.getInternalMeetingID(function(meetingID) {
var formData = new FormData($('form')[0]);
formData.append("presentation_name", document.getElementById('fileUpload').value.split(/(\\|\/)/g).pop());
formData.append("conference", meetingID);
formData.append("room", meetingID);
$.ajax({
url: '/bigbluebutton/presentation/upload', //server script to process data
type: 'POST',
xhr: function() { // custom xhr
myXhr = $.ajaxSettings.xhr();
if(myXhr.upload){ // check if upload property exists
myXhr.upload.addEventListener('progress',progressHandlingFunction, false); // for handling the progress of the upload
}
return myXhr;
},
//Ajax events
success: completeHandler,
error: errorHandler,
// Form data
data: formData,
//Options to tell JQuery not to process data or worry about content-type
cache: false,
contentType: false,
processData: false
});
});
}
function progressHandlingFunction(e){
if(e.lengthComputable){
console.log("progress: loaded " + e.loaded + " total:" + e.total);
}
}
function completeHandler(e){
$('form')[0].reset();
console.log("you file has been uploaded!");
}
function errorHandler(e){
console.log("There was an error uploading your file.");
}

View File

@ -0,0 +1,117 @@
var bbbAudioConference;
var currentSession;
// Hang Up webrtc call
function webrtc_hangup(callback) {
console.log("Terminating current session");
currentSession.terminate(); // allows calling multiple times
callback();
}
// Call
function webrtc_call(username, voiceBridge, server, callback) {
var sayswho = navigator.sayswho,
browser = sayswho[0],
version = +(sayswho[1].split('.')[0]);
console.log("Browser: " + browser + ", version: " + version);
if ( !( (browser == "Chrome" && version >= 28) || (browser == "Firefox" && version >= 26) ) ) {
callback({'status': 'browserError', message: "Browser version not supported"});
return;
}
server = server || window.document.location.host.split(':')[0]
console.log("user " + username + " calling to " + voiceBridge);
var configuration = {
uri: 'sip:' + escape(username) + '@' + server,
// password: freeswitchPassword,
// ws_servers: 'wss://' + server + ':7443',
ws_servers: 'ws://' + server + ':5066',
display_name: username,
// authorization_user: freeswitchUser,
register: false,
// register_expires: null,
// no_answer_timeout: null,
trace_sip: true,
stun_servers: "stun:74.125.134.127:19302",
// turn_servers: null,
// use_preloaded_route: null,
// connection_recovery_min_interval: null,
// connection_recovery_max_interval: null,
// hack_via_tcp: null,
// hack_ip_in_contact: null
};
bbbAudioConference = new JsSIP.UA(configuration);
bbbAudioConference.on('newRTCSession', function(e) {
console.log("New Webrtc session created!");
currentSession = e.data.session;
});
bbbAudioConference.start();
// Make an audio/video call:
// HTML5 <video> elements in which local and remote video will be shown
var selfView = document.getElementById('local-media');
var remoteView = document.getElementById('remote-media');
console.log("Registering callbacks to desired call events..");
var eventHandlers = {
'progress': function(e){
console.log('call is in progress: ' + e.data);
callback({'status':'progress', 'message': e.data});
},
'failed': function(e){
console.log('call failed with cause: '+ e.data.cause);
callback({'status':'failed', 'cause': e.data.cause});
},
'ended': function(e){
console.log('call ended with cause: '+ e.data.cause);
callback({'status':'ended', 'cause': e.data.cause});
},
'started': function(e){
var rtcSession = e.sender;
var localStream = false;
var remoteStream = false;
console.log('BigBlueButton call started');
// Attach local stream to selfView
if (rtcSession.getLocalStreams().length > 0) {
console.log("Got local stream");
localStream = true;
}
// Attach remote stream to remoteView
if (rtcSession.getRemoteStreams().length > 0) {
console.log("Got remote stream");
remoteView.src = window.URL.createObjectURL(rtcSession.getRemoteStreams()[0]);
remoteStream = true;
}
callback({'status':'started', 'localStream': localStream, 'remoteStream': remoteStream});
}
};
console.log("Setting options.. ");
var options = {
'eventHandlers': eventHandlers,
'mediaConstraints': {'audio': true, 'video': false}
};
console.log("Calling to " + voiceBridge + "....");
bbbAudioConference.call('sip:' + voiceBridge + '@' + server, options);
}
// http://stackoverflow.com/questions/5916900/detect-version-of-browser
navigator.sayswho= (function(){
var ua= navigator.userAgent,
N= navigator.appName,
tem,
M= ua.match(/(opera|chrome|safari|firefox|msie|trident)\/?\s*([\d\.]+)/i) || [];
M= M[2]? [M[1], M[2]]:[N, navigator.appVersion, '-?'];
if(M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
return M;
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,214 @@
# retrieve account for selected user
@getCurrentUserFromSession = ->
Meteor.Users.findOne("userId": getInSession("userId"))
@getInSession = (k) -> SessionAmplify.get k
@getMeetingName = ->
meetName = getInSession("meetingName") # check if we actually have one in the session
if meetName? then meetName # great return it, no database query
else # we need it from the database
meet = Meteor.Meetings.findOne({})
if meet?.meetingName
setInSession "meetingName", meet?.meetingName # store in session for fast access next time
meet?.meetingName
else null
# Finds the names of all people the current user is in a private conversation with
# Removes yourself and duplicates if they exist
@getPrivateChatees = ->
me = getInSession("userId")
users = Meteor.Users.find().fetch()
people = Meteor.Chat.find({$or: [{'message.from_userid': me, 'message.chat_type': 'PRIVATE_CHAT'},{'message.to_userid': me, 'message.chat_type': 'PRIVATE_CHAT'}] }).fetch()
formattedUsers = null
formattedUsers = (u for u in users when (do ->
return false if u.userId is me
found = false
for chatter in people
if u.userId is chatter.message.to_userid or u.userId is chatter.message.from_userid
found = true
found
)
)
if formattedUsers? then formattedUsers else []
@getTime = -> # returns epoch in ms
(new Date).valueOf()
@getUsersName = ->
name = getInSession("userName") # check if we actually have one in the session
if name? then name # great return it, no database query
else # we need it from the database
user = Meteor.Users.findOne({'userId': getInSession("userId")})
if user?.user?.name
setInSession "userName", user.user.name # store in session for fast access next time
user.user.name
else null
Handlebars.registerHelper 'equals', (a, b) -> # equals operator was dropped in Meteor's migration from Handlebars to Spacebars
a is b
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", ->
window.getMeetingName()
Handlebars.registerHelper "getShapesForSlide", ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
# try to reuse the lines above
Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id})
# retrieves all users in the meeting
Handlebars.registerHelper "getUsersInMeeting", ->
Meteor.Users.find({})
Handlebars.registerHelper "isCurrentUser", (id) ->
id is getInSession("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 "isUserSharingAudio", (u) ->
if u?
user = Meteor.Users.findOne({userId:u.userid})
user.user.voiceUser?.joined
else return false
Handlebars.registerHelper "isUserSharingVideo", (u) ->
u.webcam_stream.length isnt 0
Handlebars.registerHelper "isUserTalking", (u) ->
if u?
user = Meteor.Users.findOne({userId:u.userid})
user.user.voiceUser?.talking
else return false
Handlebars.registerHelper "isUserMuted", (u) ->
if u?
user = Meteor.Users.findOne({userId:u.userid})
user.user.voiceUser?.muted
else return false
Handlebars.registerHelper "messageFontSize", ->
style: "font-size: #{getInSession("messageFontSize")}px;"
Handlebars.registerHelper "pointerLocation", ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
currentPresentation.pointer
Handlebars.registerHelper "setInSession", (k, v) -> SessionAmplify.set k, v
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>"
# Creates a 'tab' object for each person in chat with
# adds public and options tabs to the menu
@makeTabs = ->
privTabs = getPrivateChatees().map (u, index) ->
newObj = {
userId: u.userId
name: u.user.name
class: "privateChatTab"
}
tabs = [
{userId: "PUBLIC_CHAT", name: "Public", class: "publicChatTab"},
{userId: "OPTIONS", name: "Options", class: "optionsChatTab"}
].concat privTabs
@setInSession = (k, v) -> SessionAmplify.set k, v
Meteor.methods
sendMeetingInfoToClient: (meetingId, userId) ->
setInSession("userId", userId)
setInSession("meetingId", meetingId)
setInSession("currentChatId", meetingId)
setInSession("meetingName", null)
setInSession("bbbServerVersion", "0.90")
setInSession("userName", null)
@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"
@toggleMic = (event) ->
if getInSession "isSharingAudio" # only allow muting/unmuting if they are in the call
u = Meteor.Users.findOne({userId:getInSession("userId")})
if u?
# format: meetingId, userId, requesterId, mutedBoolean
# TODO: insert the requesterId - the user who requested the muting of userId (might be a moderator)
Meteor.call('publishMuteRequest', u.meetingId, u.userId, u.userId, not u.user.voiceUser.muted)
@toggleNavbar = ->
setInSession "display_navbar", !getInSession "display_navbar"
# toggle state of session variable
@toggleUsersList = ->
setInSession "display_usersList", !getInSession "display_usersList"
@toggleVoiceCall = (event) ->
if getInSession "isSharingAudio"
hangupCallback = ->
console.log "left voice conference"
# sometimes we can hangup before the message that the user stopped talking is received so lets set it manually, otherwise they might leave the audio call but still be registered as talking
Meteor.call("userStopAudio", getInSession("meetingId"),getInSession("userId"))
setInSession "isSharingAudio", false # update to no longer sharing
webrtc_hangup hangupCallback # sign out of call
else
# create voice call params
username = "#{getInSession("userId")}-bbbID-#{getUsersName()}"
# voiceBridge = "70827"
voiceBridge = "70827"
server = null
joinCallback = (message) ->
console.log JSON.stringify message
Meteor.call("userShareAudio", getInSession("meetingId"),getInSession("userId"))
console.log "joined audio call"
console.log Meteor.Users.findOne(userId:getInSession("userId"))
setInSession "isSharingAudio", true
webrtc_call(username, voiceBridge, server, joinCallback) # make the call
return false
@toggleWhiteBoard = ->
setInSession "display_whiteboard", !getInSession "display_whiteboard"
# Starts the entire logout procedure.
# meeting: the meeting the user is in
# the user's userId
@userLogout = (meeting, user) ->
Meteor.call("userLogout", meeting, user)
# Clear the local user session and redirect them away
setInSession("userId", null)
setInSession("meetingId", null)
setInSession("currentChatId", null)
setInSession("meetingName", null)
setInSession("bbbServerVersion", null)
setInSession("userName", null)
setInSession "display_navbar", false # needed to hide navbar when the layout template renders
Router.go('logout') # navigate to logout

View File

@ -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;
}
})();

View File

@ -0,0 +1,72 @@
Template.footer.helpers
getFooterString: ->
# info = Meteor.call('getServerInfo')
year = "YEAR" #info.getBuildYear()
month = "MONTH" #info.getBuildMonth()
day = "DAY" #info.getBuildDay()
version = "VERSION_XXXX" #info.getBuildVersion()
copyrightYear = (new Date()).getFullYear()
link = "<a href='http://bigbluebutton.org/' target='_blank'>http://bigbluebutton.org</a>"
foot = "(c) #{copyrightYear} BigBlueButton Inc. [build #{version}-#{year}-#{month}-#{day}] - For more information visit #{link}"
Template.header.events
"click .usersListIcon": (event) ->
toggleUsersList()
"click .chatBarIcon": (event) ->
toggleChatbar()
"click .videoFeedIcon": (event) ->
toggleCam @
"click .audioFeedIcon": (event) ->
toggleVoiceCall @
"click .muteIcon": (event) ->
toggleMic @
"click .signOutIcon": (event) ->
userLogout getInSession("meetingId"), getInSession("userId"), true
"click .hideNavbarIcon": (event) ->
toggleNavbar()
"click .settingsIcon": (event) ->
alert "settings"
"click .raiseHand": (event) ->
Meteor.call('userRaiseHand', getInSession("meetingId"), @id)
"click .lowerHand": (event) ->
# loweredBy = @id # TODO! this must be the userid of the person lowering the hand - instructor/student
loweredBy = getInSession("userId")
Meteor.call('userLowerHand', getInSession("meetingId"), @id, loweredBy)
"click .whiteboardIcon": (event) ->
toggleWhiteBoard()
Template.recordingStatus.rendered = ->
$('button[rel=tooltip]').tooltip()
Template.makeButton.rendered = ->
$('button[rel=tooltip]').tooltip()
# Gets called last in main template, just an easy place to print stuff out
Handlebars.registerHelper "doFinalStuff", ->
console.log "-----Doing Final Stuff-----"
# These settings can just be stored locally in session, created at start up
Meteor.startup ->
@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
)
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 "joinedAt", getTime()
setInSession "isSharingAudio", false
setInSession "inChatWith", 'PUBLIC_CHAT'
setInSession "messageFontSize", 12

View File

@ -0,0 +1,107 @@
<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">
{{#with getCurrentUser}}
<div class="navbarUserButtons navbarSection">
<!-- display/hide users list toggle -->
{{#if getInSession "display_usersList"}}
{{> makeButton id=userId btn_class="navbarIconToggleActive usersListIcon navbarButton" i_class="user" _id=_id rel="tooltip" data_placement="bottom" title="Hide List of Users"}}
{{else}}
{{> makeButton id=userId btn_class="usersListIcon navbarButton" i_class="user" _id=_id 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" i_class="pencil" _id=_id rel="tooltip" data_placement="bottom" title="Hide Whiteboard"}}
{{else}}
{{> makeButton id=userId btn_class="whiteboardIcon navbarButton" i_class="pencil" _id=_id rel="tooltip" data_placement="bottom" title="Show Whiteboard"}}
{{/if}}
<!-- display/hide chat bar toggle -->
{{#if getInSession "display_chatbar"}}
{{> makeButton id=userId btn_class="navbarIconToggleActive chatBarIcon navbarButton" i_class="comment" _id=_id rel="tooltip" data_placement="bottom" title="Hide Message Pane"}}
{{else}}
{{> makeButton id=userId btn_class="chatBarIcon navbarButton" i_class="comment" _id=_id rel="tooltip" data_placement="bottom" title="Show Message Pane"}}
{{/if}}
<!-- display/hide webcam streams toggle -->
{{#if isUserSharingVideo user}}
{{> makeButton id=userId btn_class="navbarIconToggleActive videoFeedIcon navbarButton" i_class="stop" sharingVideo=true _id=_id rel="tooltip" data_placement="bottom" title="Hide Webcams"}}
{{else}}
{{> makeButton id=userId btn_class="videoFeedIcon navbarButton" i_class="facetime-video" sharingVideo=false _id=_id rel="tooltip" data_placement="bottom" title="Show Webcams"}}
{{/if}}
<!-- Join/hang up audio call -->
{{#if isUserSharingAudio user}}
{{> makeButton id=userId btn_class="navbarIconToggleActive audioFeedIcon navbarButton" i_class="volume-off" sharingAudio=true _id=_id rel="tooltip" data_placement="bottom" title="Leave Audio Call"}}
{{else}}
{{> makeButton id=userId btn_class="audioFeedIcon navbarButton" i_class="headphones" sharingAudio=false _id=_id rel="tooltip" data_placement="bottom" title="Join Audio Call"}}
{{/if}}
<!-- Mute/unmute user -->
{{#if isUserSharingAudio user}}
<!-- Should use much more noticeable icons than just bootstraps volume-up & volume-down to differentiate talking but it is good for now -->
{{#if isUserMuted user}}
{{> makeButton id=userid btn_class="muteIcon navbarButton" i_class="volume-off" sharingAudio=true _id=_id rel="tooltip" data_placement="bottom" title="Unute"}}
{{else}}
{{#if isUserTalking user}}
{{> makeButton id=userid btn_class="navbarIconToggleActive muteIcon navbarButton" i_class="volume-up" sharingAudio=true _id=_id rel="tooltip" data_placement="bottom" title="Mute"}}
{{else}}
{{> makeButton id=userId btn_class="navbarIconToggleActive muteIcon navbarButton" i_class="volume-down" sharingAudio=true _id=_id rel="tooltip" data_placement="bottom" title="Mute"}}
{{/if}}
{{/if}}
{{/if}}
{{#if user.raise_hand}}
{{> makeButton id=userId btn_class="lowerHand navbarIconToggleActive navbarButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Lower your hand"}}
{{else}}
{{> makeButton id=userId btn_class="raiseHand navbarButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Raise your hand"}}
{{/if}}
{{> recordingStatus}}
</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 id="userId" btn_class="signOutIcon navbarButton" i_class="log-out" rel="tooltip" data_placement="bottom" title="Logout"}}
{{> makeButton id="" btn_class="hideNavbarIcon navbarButton" i_class="chevron-up" rel="tooltip" data_placement="bottom" title="Hide Navbar"}}
</div>
{{/with}}
</div>
{{else}}
<div id="navbar" class="myNavbarMinimized navbar-default navbar-static-top" role="navigation">
<!-- User wants to hide navbar. The button for bringing the navbar back needs to still be available. Perhaps it should appear/disappear in the future on hover? Something to add later. -->
{{> makeButton btn_class="hideNavbarIcon navbarMinimizedButton" i_class="chevron-down" rel="tooltip" data_placement="bottom" title="Display Navbar"}}
</div>
{{/if}}
</template>
<template name="main">
<head><title>BigBlueButton Meteor</title></head>
<body>
<div id="main" class="mainContainer row-fluid">
<div>{{> header}}</div>
{{> usersList id="users" name="usersList"}}
{{> whiteboard id="whiteboard" title="Whiteboard" name="whiteboard"}}
{{> chatbar id="chat" title="Chat" name="chatbar"}}
<audio id="remote-media" autoplay="autoplay"></audio>
{{> footer}}
{{doFinalStuff}}
</div>
</body>
</template>
<template name="recordingStatus">
<!-- Recording status of the meeting -->
{{#if meetingIsRecording}}
<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 Being Recorded"><span class="glyphicon glyphicon-record"></span></button>
{{/if}}
</template>

View File

@ -0,0 +1,93 @@
.chat{
list-style: none;
margin: 0px;
padding: 0px;
}
#chat {
background-color: #f5f5f5;
border: 1px solid #ccc;
float: left;
min-height:500px;
min-width:300px;
overflow: hidden;
width: 25%;
}
.chat li{
border-bottom: 1px dotted #B3A9A9;
margin: 0px;
padding-bottom: 5px;
padding-left: 15px;
padding-right: 15px;
padding-top: 5px;
word-wrap:break-word;
}
.chat li:nth-child(even) {
background-color: rgb(245,245,245);
}
#chatbar-contents{
background-color: #fff;
height:90%;
padding-left: 0px;
padding-right: 0px;
width:100%;
}
#chatbody{
height:80%;
overflow-y: scroll;
padding-left: 0px;
padding-right: 0px;
}
.chatGreeting {
color: blue;
margin-top:5px;
}
.chat-input-wrapper {
margin-left:20px;
}
#chat-options-bar {
border-bottom:1px solid #ccc;
position:relative;
width:100%;
}
#chat-user-list {
padding:5px;
}
#newMessageInput {
height:40px;
width: 80%;
margin-top:6px;
padding:5px;
}
.optionsBar{
padding-top:15px;
padding-left: 15px;
height:100%;
}
.panel-footer{
padding-top:0px;
position:relative;
bottom:0px;
}
.privateChatTab{}
.publicChatTab{}
.private-chat-user-box {
border: solid 1px grey;
padding: 5px;
width: 90%;
height:60%;
}
.private-chat-user-entry {}
.private-chat-user-list {
font-size:12px;
}
.private-chat-user-list :hover {
background: #0099FF;
font-size:14px;
font-style:italic;
}
#sendMessageButton{
height:40px;
margin-left: 10px;
background-color: #D7D7D7
}
.tab{}

View File

@ -0,0 +1,123 @@
.audioFeedIcon{}
body {
background: #eee;
bottom:0;
color: #666666;
height:100%;
left:0;
position:absolute;
right:0;
top:0;
}
.btn{
background-color:#F5F5F5;
}
.chatBarIcon{}
.component{
background: #fff;
border: 1px solid #ccc;
float: left;
height:90%;
}
.gradientBar{
background: -webkit-linear-gradient(rgb(255,255,255), rgb(182,181,181)); /* For Safari 5.1 to 6.0 */
background: -o-linear-gradient( rgb(255,255,255), rgb(182,181,181)); /* For Opera 11.1 to 12.0 */
background: -moz-linear-gradient( rgb(255,255,255), rgb(182,181,181)); /* For Firefox 3.6 to 15 */
background: linear-gradient( rgb(255,255,255), rgb(182,181,181)); /* Standard syntax (must be last) */
}
.hideNavbarIcon{}
.lowerHand{}
.mainContainer{
height:100%;
padding-top:60px;
}
.myFooter{
color:black;
font-size: 12px;
min-height: 0px;
max-height: 30px;
}
.myNavbar{
margin-bottom:0.5%;
min-width:900px;
padding-top:5px;
}
.myNavbarMinimized{
background: #eee;
height:20px;
margin-bottom:0.2%;
margin-top:0px;
min-width:900px;
padding-top:0px;
text-align:right;
}
.navbarButton {
height:40px;
margin-left: 2px;
margin-right: 2px;
width:40px;
}
.navbarIconToggleActive{
border-bottom: 4px solid lightblue;
}
.navbarMinimizedButton{
height:20px;
margin-bottom:0px;
margin-left: 2px;
margin-right: 20px;
margin-top:0px;
width:40px;
}
.navbarSection{
float:left;
min-width:300px;
width:33%;
}
.navbarSettingsButtons{
text-align: right;
}
.navbarTitle{
font-size: 16px;
font-weight:bold;
padding-top:15px;
text-align:center;
}
.navbarUserButtons{
padding-left:0.5%;
}
.raiseHand {}
.recordingStatus{
background:none!important;
border:none;
padding:0!important;
margin-left: 10px;
}
.recordingStatusTrue{
color:green;
}
.recordingStatusFalse{
color:maroon;
}
.ScrollableWindow {
height: 100%;
overflow-y: scroll;
padding-right:5px;
}
.settingsIcon{}
.signOutIcon{}
.tab{
height:40px;
}
.title {
border-bottom: 1px solid #d7d7d7;
color: #666;
font-size:14px;
font-weight: bold;
line-height: 2em;
margin:0;
padding-bottom: 5px;
padding-left: 10px;
padding-top: 5px;
}
.usersListIcon{}
.videoFeedIcon{}

View File

@ -0,0 +1,25 @@
#users {
margin-left: 0.5%;
min-width:230px;
padding-bottom: 10px;
width: 12%;
}
#user-contents {
padding-bottom: 10px;
height:95%;
}
.userNameEntry{
height:20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 90px;
}
.user-entry tr:nth-child(even) {background-color: rgb(245,245,245);}
.user-entry tr:hover {font-weight: bold;}
.userListSettingIcon {}
.userNameContainer{
border-right: 1px solid #ccc;
padding-right:0px;
}

View File

@ -0,0 +1,11 @@
#whiteboard {
margin-left: 0.5%;
margin-right: 0.5%;
padding: 5px;
width:61%;
}
#whiteboard-paper {
background-color:grey;
width: 100%;
}

View File

@ -0,0 +1,197 @@
@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"
dest = Meteor.Users.findOne("userId": chattingWith)
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": getUsersName()
"from_tz_offset": "240"
"to_username": if chattingWith is "PUBLIC_CHAT" then "public_chat_username" else dest.user.name
"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
$('#newMessageInput').val '' # Clear message box
Template.chatInput.events
'click #sendMessageButton': (event) ->
sendMessage()
'keypress #newMessageInput': (event) -> # user pressed a button inside the chatbox
if event.which is 13 # Check for pressing enter to submit message
sendMessage()
Template.chatInput.rendered = ->
$('input[rel=tooltip]').tooltip()
$('button[rel=tooltip]').tooltip()
Template.chatbar.helpers
getChatGreeting: ->
greeting = "Welcome to #{getMeetingName()}!\n\n
For help on using BigBlueButton see these (short) <a href='http://www.bigbluebutton.org/videos/' target='_blank'>tutorial videos</a>.\n\n
To join the audio bridge click the headset icon (upper-left hand corner). Use a headset to avoid causing background noise for others.\n\n\n
This server is running BigBlueButton #{getInSession 'bbbServerVersion'}."
# 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: () ->
friend = chattingWith = getInSession('inChatWith') # the recipient(s) of the messages
after = before = greeting = []
if chattingWith is 'PUBLIC_CHAT' # find all public messages
before = Meteor.Chat.find({'message.chat_type': chattingWith, 'message.from_time': {$lt: String(getInSession("joinedAt"))}}).fetch()
after = Meteor.Chat.find({'message.chat_type': chattingWith, 'message.from_time': {$gt: String(getInSession("joinedAt"))}}).fetch()
greeting = [
'message':
'message': Template.chatbar.getChatGreeting(),
'from_username': 'System',
'from_time': getTime()
'from_color': '0x3399FF' # A nice blue in hex
]
else
me = getInSession("userId")
after = Meteor.Chat.find({ # find all messages between current user and recipient
'message.chat_type': 'PRIVATE_CHAT',
$or: [{'message.from_userid': me, 'message.to_userid': friend},{'message.from_userid': friend, 'message.to_userid': me}]
}).fetch()
messages = (before.concat greeting).concat after
getCombinedMessagesForChat: ->
msgs = Template.chatbar.getFormattedMessagesForChat()
prev_time = msgs[0].message.from_time
prev_userid = msgs[0].message.from_userid
for i in [0...msgs.length]
if i != 0
if prev_userid is msgs[i].message.from_userid
msgs[i].message.from_username = ''
if Template.message.toClockTime(msgs[i].message.from_time) is Template.message.toClockTime(prev_time)
prev_time = msgs[i].message.from_time
msgs[i].message.from_time = null
else
prev_time = msgs[i].message.from_time
else
prev_time = msgs[i].message.from_time
prev_userid = msgs[i].message.from_userid
msgs
Template.message.rendered = -> # When a message has been added and finished rendering, scroll to the bottom of the chat
$('#chatbody').scrollTop($('#chatbody')[0].scrollHeight)
Template.optionsBar.events
'click .private-chat-user-entry': (event) -> # clicked a user's name to begin private chat
setInSession 'display_chatPane', true
setInSession "inChatWith", @userId
me = getInSession("userId")
if Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT', $or: [{'message.from_userid': me, 'message.to_userid': @userId},{'message.from_userid': @userId, 'message.to_userid': me}]}).fetch().length is 0
messageForServer =
"message": "#{getUsersName()} has joined private chat with #{@user.name}."
"chat_type": "PRIVATE_CHAT"
"from_userid": me
"from_username": getUsersName()
"from_tz_offset": "240"
"to_username": @user.name
"to_userid": @userId
"from_lang": "en"
"from_time": getTime()
"from_color": "0"
Meteor.call "sendChatMessagetoServer", getInSession("meetingId"), messageForServer
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
Meteor.call("deletePrivateChatMessages", getInSession("userId"), @userId)
return false # stops propogation/prevents default
'click .optionsChatTab': (event) ->
setInSession 'display_chatPane', false
'click .privateChatTab': (event) ->
setInSession 'display_chatPane', true
console.log ".private"
'click .publicChatTab': (event) ->
setInSession 'display_chatPane', true
'click .tab': (event) ->
setInSession "inChatWith", @userId
Template.tabButtons.helpers
getChatbarTabs: ->
tabs = makeTabs()
makeTabButton: -> # create tab button for private chat or other such as options
button = '<li '
button += 'class="'
button += 'active ' if getInSession("inChatWith") is @userId
button += "tab #{@class}\"><a href=\"#\" data-toggle=\"tab\">#{@name}"
button += '&nbsp;<button class="close closeTab" type="button" >×</button>' if @class is 'privateChatTab'
button += '</a></li>'
button
Template.message.helpers
activateBreakLines: (str) ->
res = str
res = res.replace /\n/gim, '<br/>'
res = res.replace /\r/gim, '<br/>'
getHexColor: (c) ->
if parseInt(c).toString(16).length is 4
"#00#{parseInt(c).toString(16)}"
else
"##{parseInt(c).toString(16)}"
# make links received from Flash client clickable in HTML
toClickable: (str) ->
res = str
# res = str.replace /&lt;a href='event:/gim, "<a target='_blank' href='"
# res = res.replace /&lt;a&gt;/gim, '</a>'
# res = res.replace /&lt;u&gt;/gim, '<u>'
# res = res.replace /&lt;\/u&gt;/gim, '</u>'
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
sanitizeAndFormat: (str) ->
res = str
# First, replace replace all tags with the ascii equivalent
res = res.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
res = Template.message.toClickable(res)
res = Template.message.activateBreakLines(res)

View File

@ -0,0 +1,107 @@
<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}}</h3>
<div style="overflow-y: scroll; height:40px">{{>tabButtons}}</div>
{{#if getInSession "display_chatPane"}}
<div id="chatbody">
<ul class="chat">
{{#each getCombinedMessagesForChat}}
<li {{messageFontSize}}>{{> message}}</li>
{{/each}}
</ul>
</div>
<div class="panel-footer">{{> chatInput}}</div>
{{else}}
{{> optionsBar}}
{{/if}}
</div>
</div>
</template>
<template name="chatInput">
<div class="chat-input-wrapper">
<input type="text" id="newMessageInput" placeholder="Write a message..." rel="tooltip" data-placement="top" title="Write a new message" />
<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>
<!-- Displays and styles an individual message in the chat -->
<template name="message">
<table style="width:100%">
<tr>
<td>
{{#if message.from_username}}
<div class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{message.from_username}}">
<strong>{{message.from_username}}</strong>
</div>
{{/if}}
</td>
<td style="text-align:right">
{{#if message.from_time}}<small class="pull-right">{{toClockTime message.from_time}} <span class="glyphicon glyphicon-time"></span></small>{{/if}}
</td>
</tr>
</table>
<div style="color:{{getHexColor message.from_color}}">{{{sanitizeAndFormat message.message}}}</div> <!-- Messages must be safely filtered and stripped -->
</template>
<!-- Displays the list of options available -->
<template name="optionsBar">
<div class="optionsBar">
<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"}}
<table class="table table-hover">
<tbody class="private-chat-user-list">
{{#each getUsersInMeeting}}
<tr class="private-chat-user-entry">
{{#unless isCurrentUser userId}}
{{user.name}}
{{/unless}}
</tr>
{{/each}}
</tbody>
</table>
{{/contentFor}}
{{/Layout}}
</div>
<br/>
{{> chatOptions}}
</div>
</template>
<template name="optionsFontSize">
<div class="dropdown" style="float:left">
<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" style="height:80px; overflow-y:scroll; right:0px;">
<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="tabButtonContainer" class="nav nav-tabs">
{{#each getChatbarTabs}}
{{{makeTabButton}}}
{{/each}}
</ul>
</template>

View File

@ -0,0 +1,3 @@
<template name="layout">
{{> yield}}
</template>

View File

@ -0,0 +1,10 @@
<template name="logout">
<head>
<title>BigBlueButton</title>
</head>
<body>
<div id="main" class="row-fluid">
<h2>You have been logged out.</h2>
</div>
</body>
</template>

View File

@ -0,0 +1,21 @@
<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>
<template name="moduleTable">
{{> yield}}
<table class="{{tableClass}}">
<tbody class="{{tbodyClass}}">
{{> yield region="Contents"}}
</tbody>
</table>
</template>

View File

@ -0,0 +1,41 @@
<template name="displayUserIcons">
<td>{{#if isUserSharingVideo user}}
<span class="userListSettingIcon glyphicon glyphicon-facetime-video"></span>
{{/if}}</td>
<!-- Should use much more noticeable icons than just bootstrap's volume-up & volume-down to differentiate talking but it is good for now -->
<td>{{#if isUserSharingAudio user}}
{{#if isUserMuted user}}
<span class="userListSettingIcon glyphicon glyphicon-volume-off"></span>
{{else}}
{{#if isUserTalking user}}
<span class="userListSettingIcon glyphicon glyphicon-volume-up"></span>
{{else}}
<span class="userListSettingIcon glyphicon glyphicon-volume-down"></span>
{{/if}}
{{/if}}
{{/if}}</td>
<td>{{#if user.presenter}}<span class="userListSettingIcon glyphicon glyphicon-picture"></span>{{/if}}</td>
<td>{{#if user.raise_hand}}<span class="userListSettingIcon glyphicon glyphicon-hand-up"></span>{{/if}}</td>
</template>
<template name="usernameEntry"> <!-- A template now because more information be added/styled -->
<td class="userNameContainer">
{{#if isCurrentUser userId}}
<p class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}} (you)">
<strong>{{user.name}} (you)</strong>
</p>
{{else}}
<p class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}}">
{{user.name}}
</p>
{{/if}}
</td>
</template>
<template name="userItem">
{{> usernameEntry}}
{{> displayUserIcons}}
</template>

View File

@ -0,0 +1,3 @@
Template.usersList.helpers
getMeetingSize: -> # Retreieve the number of users in the chat, or "error" string
Meteor.Users.find().count()

View File

@ -0,0 +1,18 @@
<template name="usersList">
<div id="{{id}}" {{visibility name}} class="component">
<h3 class="title gradientBar"><span class="glyphicon glyphicon-user"></span> Participants: {{getMeetingSize}} Users</h3>
<div id="user-contents">
{{#Layout template="scrollWindow" id="publicUserScrollWindow"}}
{{#contentFor region="scrollContents"}}
{{#Layout template="moduleTable" tableClass="table table-hover" tbodyClass="user-entry"}}
{{#contentFor region="Contents"}}
{{#each getUsersInMeeting}}
<tr>{{>userItem}}</tr>
{{/each}}
{{/contentFor}}
{{/Layout}}
{{/contentFor}}
{{/Layout}}
</div>
</div>
</template>

View File

@ -0,0 +1,66 @@
Template.slide.rendered = ->
height = $('#whiteboard').height()
console.log "height = #{height}"
$('#whiteboard-paper').height((height-$("#whiteboard-navbar").height()-10)+'px')
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
if currentSlide?.slide?.png_uri?
Template.slide.createWhiteboardPaper (wpm) ->
Template.slide.displaySlide wpm
Template.slide.helpers
createWhiteboardPaper: (callback) ->
Template.slide.whiteboardPaperModel = new WhiteboardPaperModel('whiteboard-paper')
callback(Template.slide.whiteboardPaperModel)
displaySlide: (wpm) ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
wpm.create()
wpm._displayPage(currentSlide?.slide?.png_uri)
updatePointerLocation: (pointer) ->
wpm = Template.slide.whiteboardPaperModel
wpm?.moveCursor(pointer.x, pointer.y)
manuallyDisplayShapes: ->
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
wpm = Template.slide.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"
for num in [0..3] # 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)
#### 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"
for num in [0..3] # the coordinates must be in the range 0 to 1
shapeInfo.points[num] = shapeInfo.points[num] / 100
wpm = Template.slide.whiteboardPaperModel
wpm.makeShape(shapeType, shapeInfo)
wpm.updateShape(shapeType, shapeInfo)
Template.shape.destroyed = ->
wpm = Template.slide.whiteboardPaperModel
wpm.clearShapes()
Template.slide.displaySlide(wpm)
Template.slide.manuallyDisplayShapes()

View File

@ -0,0 +1,9 @@
<template name="slide">
{{#each getShapesForSlide}}
{{> shape}}
{{/each}}
{{updatePointerLocation pointerLocation}}
</template>
<template name="shape">
</template>

View File

@ -0,0 +1,16 @@
Template.whiteboard.rendered = ->
$(window).resize( ->
height = $('#whiteboard').height()
console.log "height = #{height}"
$('#whiteboard-paper').height((height-$("#whiteboard-navbar").height()-10)+'px')
# $('#svggroup').html('')
Template.slide.whiteboardPaperModel._updateContainerDimensions()
wpm = Template.slide.whiteboardPaperModel
wpm.clearShapes()
wpm = Template.slide.whiteboardPaperModel
Template.slide.displaySlide(wpm)
Template.slide.manuallyDisplayShapes()
);

View File

@ -0,0 +1,10 @@
<template name="whiteboard">
<div id="{{id}}" {{visibility name}} class="component">
<h3 id="whiteboard-navbar" class="title gradientBar"><span class="glyphicon glyphicon-pencil"></span> {{title}}</h3>
<div id="whiteboard-paper">
{{#each getCurrentSlide}}
{{> slide}}
{{/each}}
</div>
</div>
</template>

View File

@ -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?

View File

@ -0,0 +1,42 @@
# 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()
# @param {string,int} stroke stroke color, can be a number (a hex converted to int) or a
# string (e.g. "#ffff00")
# @param {string,ing} thickness thickness as a number or string (e.g. "2" or "2px")
strokeAndThickness: (stroke, thickness) ->
stroke ?= "0"
thickness ?= "1"
r =
stroke: if stroke.toString().match(/\#.*/) then stroke else colourToHex(stroke)
"stroke-width": if thickness.toString().match(/.*px$/) then thickness else "#{thickness}px"
r
# Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff)
colourToHex = (value) ->
hex = value.toString(16)
hex = "0" + hex while hex.length < 6
"##{hex}"

View File

@ -0,0 +1,40 @@
# 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)

View File

@ -0,0 +1,150 @@
# 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 Meteor.call("strokeAndThickness", color, 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 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
#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
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

View File

@ -0,0 +1,206 @@
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"]
# @lineX = null
# @lineY = null
# 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 Meteor.call("strokeAndThickness", color, thickness)
@obj.attr({"stroke-linejoin": "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?
# if adding points from the pencil
if _.isBoolean(info.adding)
add = info.adding
pathPercent = "L" + x1 + " " + y1
@definition.data[0] += pathPercent
x1 = x1 * @gw + @xOffset
y1 = y1 * @gh + @yOffset
# if adding to the line
if add
path = @obj.attrs.path + "L" + x1 + " " + y1
@obj.attr path: path
# if simply updating the last portion (for drawing a straight line)
else
@obj.attrs.path.pop()
path = @obj.attrs.path.join(" ")
path = path + "L" + x1 + " " + y1
@obj.attr path: path
# adding lines from the line tool
else
path = @_buildPath(x1, y1, x2, y2)
@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(x1, y1, x2, y2)
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: (x1, y1, x2, y2) ->
"M#{x1} #{y1}L#{x2} #{y2}"
# 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

View File

@ -0,0 +1,828 @@
# "Paper" which is the Raphael term for the entire SVG object on the webpage.
# This class deals with this SVG component only.
class @WhiteboardPaperModel
# Container must be a DOM element
constructor: (@container) ->
# a WhiteboardCursorModel
@cursor = null
# all slides in the presentation indexed by url
@slides = {}
# the slide being shown
@currentSlide = null
@fitToPage = true
@panX = null
@panY = null
# a raphaeljs set with all the shapes in the current slide
@currentShapes = 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)
@currentShapesDefinitions = []
# pointers to the current shapes being drawn
@currentLine = null
@currentRect = null
@currentEllipse = null
@currentTriangle = null
@currentText = null
@zoomLevel = 1
@shiftPressed = false
@currentPathCount = 0
# $container = $('#whiteboard-paper')
# @containerWidth = $container.innerWidth()
# @containerHeight = $container.innerHeight()
@_updateContainerDimensions()
# $(window).on "resize.whiteboard_paper", _.bind(@_onWindowResize, @)
# $(document).on "keydown.whiteboard_paper", _.bind(@_onKeyDown, @)
# $(document).on "keyup.whiteboard_paper", _.bind(@_onKeyUp, @)
# Bind to the event triggered when the client connects to the server
# if globals.connection.isConnected()
# @_registerEvents()
# else
# globals.events.on "connection:connected", =>
# @_registerEvents()
# Override the close() to unbind events.
unbindEvents: ->
$(window).off "resize.whiteboard_paper"
$(document).off "keydown.whiteboard_paper"
$(document).off "keyup.whiteboard_paper"
# TODO: other events are being used in the code and should be off() here
# 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()
console.log "h: #{h}"
console.log "w: #{w}"
# @raphaelObj ?= ScaleRaphael(@container, "900", "500")
@raphaelObj ?= ScaleRaphael(@container, w, h)
# $container = $('#whiteboard-contents')
@raphaelObj ?= ScaleRaphael(@container, $container.innerHeight(), $container.innerWidth())
@raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
@cursor = new WhiteboardCursorModel(@raphaelObj)
@cursor.draw()
#@cursor.on "cursor:mousewheel", _.bind(@_zoomSlide, @)
if @slides
@rebuild()
else
@slides = {} # if previously loaded
unless navigator.userAgent.indexOf("Firefox") is -1
@raphaelObj.renderfix()
# initializing border around slide to cover up areas which shouldnt show
@borders = {}
for border in ['left', 'right', 'top', 'bottom']
@borders[border] = @raphaelObj.rect(0, 0, 0, 0)
@borders[border].attr("fill", "#ababab")
@borders[border].attr( {stroke:"#ababab"} )
@raphaelObj
# Re-add the images to the paper that are found
# in the slides array (an object of urls and dimensions).
rebuild: ->
@currentSlide = null
for url of @slides
if @slides.hasOwnProperty(url)
@addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight()
# A wrapper around ScaleRaphael's `changeSize()` method, more details at:
# http://www.shapevent.com/scaleraphael/
# Also makes sure that the images are redraw in the canvas so they are actually resized.
changeSize: (windowWidth, windowHeight, center=true, clipping=false) ->
if @raphaelObj?
@raphaelObj.changeSize(windowWidth, windowHeight, center, clipping)
# TODO: we can scale the slides and drawings instead of re-adding them, but the logic
# will change quite a bit
# slides
slidesTmp = _.clone(@slides)
urlTmp = @currentSlide
@removeAllImagesFromPaper()
@slides = slidesTmp
@rebuild()
@showImageFromPaper(urlTmp?.url)
# drawings
tmp = _.clone(@currentShapesDefinitions)
@clearShapes()
@drawListOfShapes(tmp)
# 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()
if @fitToPage
# 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
# TODO: temporary solution
url = @_slideUrl(url)
sw = width / max
sh = height / max
cx = (@containerWidth / 2) - (width / 2)
cy = (@containerHeight / 2) - (height / 2)
img = @raphaelObj.image(url, cx, cy, width, height)
originalWidth = width
originalHeight = height
else
# fit to width
alert "no fit"
# assume it will fit width ways
sw = width / wr
sh = height / wr
wr = width / @containerWidth
originalWidth = sw
originalHeight = sh
sw = width / wr
sh = height / wr
img = @raphaelObj.image(url, cx = 0, cy = 0, sw, sh)
# 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, originalWidth, originalHeight, sw, sh, cx, cy)
unless @currentSlide?
img.toBack()
@currentSlide = @slides[url]
else if @currentSlide.url is url
img.toBack()
else
img.hide()
$(@container).on "mousemove", _.bind(@_onMouseMove, @)
$(@container).on "mousewheel", _.bind(@_zoomSlide, @)
# TODO $(img.node).bind "mousewheel", zoomSlide
#@trigger('paper:image:added', img)
# TODO: other places might also required an update in these dimensions
@_updateContainerDimensions()
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()) # TODO do we need this?
@slides = {}
@currentSlide = null
# Shows an image from the paper.
# The url must be in the slides array.
# @param {string} url the url of the image (must be in slides array)
showImageFromPaper: (url) ->
# TODO: temporary solution
url = @_slideUrl(url)
if not @currentSlide? or (@slides[url]? and @currentSlide.url isnt url)
@_hideImageFromPaper(@currentSlide.url) if @currentSlide?
next = @_getImageFromPaper(url)
if next
next.show()
next.toFront()
@currentShapes.forEach (element) ->
element.toFront()
@cursor.toFront()
@currentSlide = @slides[url]
# Updates the paper from the server values.
# @param {number} cx_ the x-offset value as a percentage of the original width
# @param {number} cy_ the y-offset value as a percentage of the original height
# @param {number} sw_ the slide width value as a percentage of the original width
# @param {number} sh_ the slide height value as a percentage of the original height
# TODO: not working as it should
updatePaperFromServer: (cx_, cy_, sw_, sh_) ->
# # if updating the slide size (zooming!)
# [slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
# if sw_ and sh_
# @raphaelObj.setViewBox cx_ * slideWidth, cy_ * slideHeight, sw_ * slideWidth, sh_ * slideHeight,
# sw = slideWidth / sw_
# sh = slideHeight / sh_
# # just panning, so use old slide size values
# else
# [sw, sh] = @_currentSlideDimensions()
# @raphaelObj.setViewBox cx_ * slideWidth, cy_ * slideHeight, @raphaelObj._viewBox[2], @raphaelObj._viewBox[3]
# # update corners
# cx = cx_ * sw
# cy = cy_ * sh
# # update position of svg object in the window
# sx = (@containerWidth - slideWidth) / 2
# sy = (@containerHeight - slideHeight) / 2
# sy = 0 if sy < 0
# @raphaelObj.canvas.style.left = sx + "px"
# @raphaelObj.canvas.style.top = sy + "px"
# @raphaelObj.setSize slideWidth - 2, slideHeight - 2
# # update zoom level and cursor position
# z = @raphaelObj._viewBox[2] / slideWidth
# @zoomLevel = z
# dcr = 1
# @cursor.setRadius(dcr * z)
# # force the slice attribute despite Raphael changing it
# @raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice"
# 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 "path", "line"
@cursor.undrag()
@currentLine = @_createTool(tool)
@cursor.drag(@currentLine.dragOnMove, @currentLine.dragOnStart, @currentLine.dragOnEnd)
when "rect"
@cursor.undrag()
@currentRect = @_createTool(tool)
@cursor.drag(@currentRect.dragOnMove, @currentRect.dragOnStart, @currentRect.dragOnEnd)
# TODO: the shapes below are still in the old format
# when "panzoom"
# @cursor.undrag()
# @cursor.drag _.bind(@_panDragging, @),
# _.bind(@_panGo, @), _.bind(@_panStop, @)
# when "ellipse"
# @cursor.undrag()
# @cursor.drag _.bind(@_ellipseDragging, @),
# _.bind(@_ellipseDragStart, @), _.bind(@_ellipseDragStop, @)
# when "text"
# @cursor.undrag()
# @cursor.drag _.bind(@_rectDragging, @),
# _.bind(@_textStart, @), _.bind(@_textStop, @)
else
console.log "ERROR: Cannot set invalid tool:", tool
# Sets the fit to page.
# @param {boolean} value If true fit to page. If false fit to width.
# TODO: not really working as it should be
setFitToPage: (value) ->
@fitToPage = value
# TODO: we can scale the slides and drawings instead of re-adding them, but the logic
# will change quite a bit
temp = @slides
@removeAllImagesFromPaper()
@slides = temp
# re-add all the images as they should fit differently
@rebuild()
# set to default zoom level
#globals.connection.emitPaperUpdate 0, 0, 1, 1
# get the shapes to reprocess
#globals.connection.emitAllShapes()
# Socket response - Update zoom variables and viewbox
# @param {number} d the delta value from the scroll event
# @return {undefined}
setZoom: (d) ->
step = 0.05 # step size
if d < 0
@zoomLevel += step # zooming out
else
@zoomLevel -= step # zooming in
[sw, sh] = @_currentSlideDimensions()
[cx, cy] = @_currentSlideOffsets()
x = cx / sw
y = cy / sh
# cannot zoom out further than 100%
z = (if @zoomLevel > 1 then 1 else @zoomLevel)
# cannot zoom in further than 400% (1/4)
z = (if z < 0.25 then 0.25 else z)
# cannot zoom to make corner less than (x,y) = (0,0)
x = (if x < 0 then 0 else x)
y = (if y < 0 then 0 else y)
# cannot view more than the bottom corners
zz = 1 - z
x = (if x > zz then zz else x)
y = (if y > zz then zz else y)
#globals.connection.emitPaperUpdate x, y, z, z # send update to all clients
stopPanning: ->
# nothing to do
# Draws an array of shapes to the paper.
# @param {array} shapes the array of shapes to draw
drawListOfShapes: (shapes) ->
@currentShapesDefinitions = shapes
@currentShapes = @raphaelObj.set()
for shape in shapes
shapeType = shape?.shape?.shape_type
dataBlock = shape?.shape?.shape
data = if _.isString(dataBlock) then JSON.parse(dataBlock) else dataBlock
tool = @_createTool(shapeType)
if tool?
@currentShapes.push tool.draw.apply(tool, data)
else
console.log "shape not recognized at drawListOfShapes", shape
# 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?
@currentShapes.forEach (element) ->
element.remove()
@currentShapes = []
@currentShapesDefinitions = []
# 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) ->
switch shape
when "line"
@currentLine.update(data)
when "rectangle"
@currentRect.update(data)
when "ellipse"
@currentEllipse.update(data)
when "triangle"
@currentTriangle.update(data)
when "text"
#@currentText.update.apply(@currentText, data)
@currentText.update(data)
else
console.log "shape not recognized at updateShape", shape
# Make a shape `shape` with the data in `data`.
makeShape: (shape, data) ->
tool = null
switch shape
when "path", "line"
@currentLine = @_createTool(shape)
toolModel = @currentLine
tool = @currentLine.make(data)
when "rectangle"
@currentRect = @_createTool(shape)
toolModel = @currentRect
tool = @currentRect.make(data)
when "ellipse"
@currentEllipse = @_createTool(shape)
toolModel = @currentEllipse
tool = @currentEllipse.make(data)
when "triangle"
@currentTriangle = @_createTool(shape)
toolModel = @currentTriangle
tool = @currentTriangle.make(data)
when "text"
@currentText = @_createTool(shape)
toolModel = @currentText
#tool = @currentText.make.apply(@currentText, data)
tool = @currentText.make(data)
else
console.log "shape not recognized at makeShape", shape
if tool?
@currentShapes ?= @raphaelObj.set()
@currentShapes.push(tool)
@currentShapesDefinitions.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 )
# Update the slide to move and zoom
# @param {number} xOffset the x value of offset
# @param {number} yOffset the y value of offset
# @param {number} widthRatio the ratio of the previous width
# @param {number} heightRatio the ratio of the previous height
moveAndZoom: (xOffset, yOffset, widthRatio, heightRatio) ->
@globalxOffset = xOffset
@globalyOffset = yOffset
@globalwidthRatio = widthRatio
@globalheightRatio = heightRatio
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
#console.log("xOffset: " + xOffset + ", yOffset: " + yOffset);
#console.log("@containerWidth: " + @containerWidth + " @containerHeight: " + @containerHeight);
#console.log("slideWidth: " + slideWidth + " slideHeight: " + slideHeight);
baseWidth = (@containerWidth - slideWidth) / 2
baseHeight = (@containerHeight - slideHeight) / 2
#get the actual size of the slide, depending on the limiting factor (container width or container height)
actualWidth = @currentSlide.displayWidth
actualHeight = @currentSlide.displayHeight
#console.log("actualWidth:" + actualWidth + " actualHeight: " + actualHeight)
#calculate parameters to pass
newXPos = baseWidth - 2* xOffset * actualWidth / 100
newyPos = baseHeight - 2* yOffset * actualHeight / 100
newWidth = actualWidth / 100 * widthRatio
newHeight = actualHeight / 100 * heightRatio
@viewBoxXpos = newXPos
@viewBoxYPos = newyPos
@viewBoxWidth = newWidth
@viewBoxHeight = newHeight
#console.log("newXPos: " + newXPos + " newyPos: " + newyPos + " newWidth: " + newWidth + " newHeight: " + newHeight)
#set parameters to raphael viewbox
@raphaelObj.setViewBox(newXPos , newyPos, newWidth , newHeight , true)
# update the rectangle elements which create the border when page is zoomed
@borders.left.attr( {width:newXPos, height: @containerHeight} )
@borders.right.attr(
x: newXPos + newWidth
y: 0
width:newXPos
height:@containerHeight
)
@borders.top.attr(
width: @containerWidth
height: newyPos
)
@borders.bottom.attr(
y: newyPos + newHeight
width: @containerWidth
height: @containerHeight
)
# borders should appear infront of every other element (i.e. shapes)
for _, border of @borders
border.toFront()
#update cursor to appear the same size even when page is zoomed in
@cursor.setRadius( 3 * widthRatio / 100 )
# Registers listeners for events in the gloval event bus
_registerEvents: ->
# globals.events.on "connection:whiteboardDrawPen", (startingData) =>
# type = startingData.payload.shape_type
# color = startingData.payload.data.line.color
# thickness = startingData.payload.data.line.weight
# points = startingData.shape.points
# if type is "line"
# for i in [0..points.length - 1]
# if i is 0
# #make these compatible with a line
# console.log "points[i]: " + points[i]
# lineObject = {
# shape: {
# type: "line",
# coordinate: {
# firstX : points[i].x/100,
# firstY : points[i].y/100
# },
# color: startingData.payload.data.line.color,
# thickness : startingData.payload.data.line.weight
# }
# adding : false #tell the line object that we are NOT adding points but creating a new line
# }
# console.log "lineObject: " + lineObject
# @makeShape type, lineObject
# else
# console.log "points[i]: "+ points[i]
# lineObject = {
# shape: {
# type: "line",
# coordinate: {
# firstX : points[i].x/100,
# firstY : points[i].y/100
# },
# color: startingData.payload.data.line.color,
# thickness : startingData.payload.data.line.weight
# }
# adding : true #tell the line object that we ARE adding points and NOT creating a new line
# }
# console.log "lineObject: " + lineObject
# @updateShape type, lineObject
# globals.events.on "connection:move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) =>
# @moveAndZoom(xOffset, yOffset, widthRatio, heightRatio)
# globals.events.on "connection:changeslide", (url) =>
# @showImageFromPaper(url)
# globals.events.on "connection:viewBox", (xperc, yperc, wperc, hperc) =>
# xperc = parseFloat(xperc, 10)
# yperc = parseFloat(yperc, 10)
# wperc = parseFloat(wperc, 10)
# hperc = parseFloat(hperc, 10)
# @updatePaperFromServer(xperc, yperc, wperc, hperc)
# globals.events.on "connection:fitToPage", (value) =>
# @setFitToPage(value)
# globals.events.on "connection:zoom", (delta) =>
# @setZoom(delta)
# globals.events.on "connection:paper", (cx, cy, sw, sh) =>
# @updatePaperFromServer(cx, cy, sw, sh)
# globals.events.on "connection:panStop", =>
# @stopPanning()
# globals.events.on "connection:toolChanged", (tool) =>
# @setCurrentTool(tool)
# globals.events.on "connection:textDone", =>
# @textDone()
# globals.events.on "connection:uploadStatus", (message, fade) =>
# globals.events.trigger("whiteboard:paper:uploadStatus", message, fade)
# 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
# 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
# Hides an image from the paper given the URL.
# The url must be in the slides array.
# @param {string} url the url of the image (must be in slides array)
_hideImageFromPaper: (url) ->
img = @_getImageFromPaper(url)
img.hide() if img?
# Update zoom variables on all clients
# @param {Event} e the event that occurs when scrolling
# @param {number} delta the speed/direction at which the scroll occurred
_zoomSlide: (e, delta) ->
#globals.connection.emitZoom delta
# Called when the cursor is moved over the presentation.
# Sends cursor moving event to server.
# @param {Event} e the mouse event
# @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: this should only be done if the user is the presenter
_onMouseMove: (e, x, y) ->
[sw, sh] = @_currentSlideDimensions()
xLocal = (e.pageX - @containerOffsetLeft) / sw
yLocal = (e.pageY - @containerOffsetTop) / sh
#globals.connection.emitMoveCursor xLocal, yLocal
# When the user is dragging the cursor (click + move)
# @param {number} dx the difference between the x value from panGo and now
# @param {number} dy the difference between the y value from panGo and now
_panDragging: (dx, dy) ->
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
sx = (@containerWidth - slideWidth) / 2
sy = (@containerHeight - slideHeight) / 2
[sw, sh] = @_currentSlideDimensions()
# ensuring that we cannot pan outside of the boundaries
x = (@panX - dx)
# cannot pan past the left edge of the page
x = (if x < 0 then 0 else x)
y = (@panY - dy)
# cannot pan past the top of the page
y = (if y < 0 then 0 else y)
if @fitToPage
x2 = slideWidth + x
else
x2 = @containerWidth + x
# cannot pan past the width
x = (if x2 > sw then sw - (@containerWidth - sx * 2) else x)
if @fitToPage
y2 = slideHeight + y
else
# height of image could be greater (or less) than the box it fits in
y2 = @containerHeight + y
# cannot pan below the height
y = (if y2 > sh then sh - (@containerHeight - sy * 2) else y)
#globals.connection.emitPaperUpdate x / sw, y / sh, null, null
# When panning starts
# @param {number} x the x value of the cursor
# @param {number} y the y value of the cursor
_panGo: (x, y) ->
[cx, cy] = @_currentSlideOffsets()
@panX = cx
@panY = cy
# When panning finishes
# @param {Event} e the mouse event
_panStop: (e) ->
@stopPanning()
# Called when the application window is resized.
_onWindowResize: ->
@_updateContainerDimensions()
console.log "_onWindowResize"
#TODO: temporary hacked away fix so that the buttons resize correctly when the window resizes
$("#users-btn").click();
$("#users-btn").click();
#TODO: maybe find solution besides these global values..no conflicts however
[slideWidth, slideHeight] = @_currentSlideOriginalDimensions()
#console.log("xOffset: " + xOffset + ", yOffset: " + yOffset);
#console.log("@containerWidth: " + @containerWidth + " @containerHeight: " + @containerHeight);
#console.log("slideWidth: " + slideWidth + " slideHeight: " + slideHeight);
baseWidth = (@containerWidth - slideWidth) / 2
baseHeight = (@containerHeight - slideHeight) / 2
#get the actual size of the slide, depending on the limiting factor (container width or container height)
if(@containerWidth - slideWidth < @containerHeight - slideHeight)
actualHeight = @containerWidth * slideHeight / slideWidth
actualWidth = @containerWidth
else
actualWidth = @containerHeight * slideWidth / slideHeight
actualHeight = @containerHeight
#console.log("actualWidth:" + actualWidth + " actualHeight: " + actualHeight)
#calculate parameters to pass
newXPos = baseWidth
newyPos = baseHeight
newWidth = actualWidth
newHeight = actualHeight
#now the zooming will still be correct when the window is resized
#and hopefully when rotated on a mobile device
if @globalxOffset? && @globalyOffset? && @globalwidthRatio? && @globalheightRatio?
console.log "has zoomed in"
@moveAndZoom(@globalxOffset, @globalyOffset, @globalwidthRatio, @globalheightRatio)
else
obj =
globalxOffset : @globalxOffset
globalyOffset : @globalyOffset
globalwidthRatio : @globalwidthRatio
globalheightRatio : @globalheightRatio
console.log obj
console.log "not zoomed"
@raphaelObj.setViewBox(newXPos, newyPos, newWidth, newHeight,true)
# when pressing down on a key at anytime
_onKeyDown: (event) ->
unless event
keyCode = window.event.keyCode
else
keyCode = event.keyCode
switch keyCode
when 16 # shift key
@shiftPressed = true
# when releasing any key at any time
_onKeyUp: ->
unless event
keyCode = window.event.keyCode
else
keyCode = event.keyCode
switch keyCode
when 16 # shift key
@shiftPressed = false
_currentSlideDimensions: ->
if @currentSlide? then @currentSlide.getDimensions() else [0, 0]
_currentSlideOriginalDimensions: ->
if @currentSlide? then @currentSlide.getOriginalDimensions() else [0, 0]
_currentSlideOffsets: ->
if @currentSlide? then @currentSlide.getOffsets() else [0, 0]
# Wrapper method to create a tool for the whiteboard
_createTool: (type) ->
switch type
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 did not match the expected format"
#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()
# get dimensions for available whiteboard space
# get where to start from the left -> either the end of the user's list or the left edge of the screen
# if getInSession "display_usersList" then xBegin = $("#userListContainer").width()
# else xBegin = 0
# # get where to start from the right -> either the beginning of the chat bar or the right edge of the screen
# if getInSession "display_chatbar" then xEnd = $("#chat").position().left
# else xEnd = $( document ).width();
# # find the height to start the top of the image at
# if getInSession "display_navbar" then yBegin = $("#navbar").height()
# else yBegin = 0
# yEnd = $( document ).height();
# # TODO: add some form of padding to the left, right, top, and bottom boundaries
# #
# boardWidth = xEnd - xBegin
# boardHeight = yEnd - yBegin
boardWidth = @containerWidth
boardHeight = @containerHeight
currentPresentation = Meteor.Presentations.findOne({"presentation.current": true})
presentationId = currentPresentation?.presentation?.id
currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true})
# 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}"
# @addImageToPaper(data, imageWidth, imageHeight) # TODO the dimensions should be modified
@addImageToPaper(data, imageWidth, imageHeight)

View File

@ -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 Meteor.call("strokeAndThickness",color, 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
[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

View File

@ -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]

View File

@ -0,0 +1,250 @@
# 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 a text:" + JSON.stringify startingData
x = startingData.x
y = startingData.y
width = startingData.textBoxWidth
height = startingData.textBoxHeight
colour = 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
colour = Meteor.call("strokeAndThickness",colour, false)
@obj = @paper.text(x/100, y/100, "")
@obj.attr
fill: Meteor.call("strokeAndThickness",colour, false)
"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 = startingData.fontColor
fontSize = startingData.fontSize
calcFontSize = startingData.calcedFontSize
myText = startingData.text
svgNS = "http://www.w3.org/2000/svg"
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: "#000" #Meteor.call("strokeAndThickness",colour, false)
"font-size": calcFontSize
cell = @obj.node
while cell? and cell.hasChildNodes()
cell.removeChild(cell.firstChild)
#extract and add line breaks for start
dashArray = new Array()
dashFound = true
indexPos = 0
cumulY = 0
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
lastLineBreak = 0
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?

View File

@ -0,0 +1,105 @@
# A triangle in the whiteboard
class @WhiteboardTriangleModel extends WhiteboardToolModel
constructor: (@paper) ->
console.log "Whiteboard - Creating rectangle"
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 Meteor.call("strokeAndThickness", color, 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) ->
console.log "Whiteboard - updating triangle points"
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

View File

@ -0,0 +1,22 @@
Meteor.methods
addChatToCollection: (meetingId, messageObject) ->
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)
console.log "added chat id=[#{id}]:#{messageObject.message}. Chat.size is now
#{Meteor.Chat.find({meetingId: meetingId}).count()}"
sendChatMessagetoServer: (meetingId, messageObject) ->
Meteor.call "publishChatMessage", meetingId, messageObject

View File

@ -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")

View File

@ -0,0 +1,19 @@
Meteor.methods
addMeetingToCollection: (meetingId, name, recorded) ->
console.log "trying to add to Meetings:#{meetingId}|#{name} Meetings.size before:#{Meteor.Meetings.find().count()}"
#check if the meeting is already in the collection
unless Meteor.Meetings.findOne({meetingId: meetingId})?
id = Meteor.Meetings.insert(meetingId: meetingId, meetingName: name, recorded: recorded)
console.log "added meeting _id=[#{id}]:meetingId=[#{meetingId}]:name=[#{name}].
Meetings.size is now #{Meteor.Meetings.find().count()}"
removeMeetingFromCollection: (meetingId) ->
if Meteor.Meetings.findOne({meetingId: meetingId})?
if Meteor.Users.find({meetingId: meetingId}).count() isnt 0
console.log "\n!!!!!removing a meeting which has active users in it!!!!\n"
id = Meteor.Meetings.findOne({meetingId: meetingId})
if id?
Meteor.Meetings.remove(id._id)
console.log "removed from Meetings:#{meetingId} now there are only
#{Meteor.Meetings.find().count()} meetings running"

View File

@ -0,0 +1,26 @@
Meteor.methods
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)
console.log "added presentation 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)
console.log "----removed presentation[" + presentationId + "] from " + meetingId

View File

@ -0,0 +1,77 @@
Meteor.methods
addShapeToCollection: (meetingId, whiteboardId, shapeObject) ->
console.log "shapeObject=" + JSON.stringify shapeObject
if shapeObject?.shape_type is "text" and shapeObject.status is "textPublished"
console.log "we are dealing with a text shape"
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
id = Meteor.Shapes.insert(entry)
numShapesOnSlide = Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch().length
console.log "added textShape id =[#{id}]:#{shapeObject.id} in #{meetingId} || now there are #{numShapesOnSlide} shapes on the slide"
else
if shapeObject?.status is "DRAW_END" #the mouse button was released - the drawing is complete
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)
numShapesOnSlide = Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch().length
console.log "added shape id =[#{id}]:#{shapeObject.id} in #{meetingId} || now there are #{numShapesOnSlide} shapes on the slide"
removeAllShapesFromSlide: (meetingId, whiteboardId) ->
console.log "removeAllShapesFromSlide__" + whiteboardId
if meetingId? and whiteboardId? and Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId})?
shapesOnSlide = Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch()
console.log "number of shapes:" + shapesOnSlide.length
for s in shapesOnSlide
console.log "shape=" + s.shape.id
id = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": s.shape.id})
if id?
Meteor.Shapes.remove(id._id)
console.log "----removed shape[" + s.shape.id + "] from " + whiteboardId
console.log "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)
console.log "----removed shape[" + shapeId + "] from " + whiteboardId
console.log "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).count()

Some files were not shown because too many files have changed in this diff Show More