diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java index 664c16e4414b9863e47e7fde5e75df33aed906b5..d4768231bd0baaa2f0886648e9fe3873e9a16d3a 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java @@ -13,6 +13,7 @@ import org.bigbluebutton.conference.service.messaging.RegisterUserMessage; import org.bigbluebutton.conference.service.messaging.UserConnectedToGlobalAudio; import org.bigbluebutton.conference.service.messaging.UserDisconnectedFromGlobalAudio; import org.bigbluebutton.conference.service.messaging.ValidateAuthTokenMessage; +import org.bigbluebutton.conference.service.messaging.GetAllMeetingsRequest; import org.bigbluebutton.conference.service.messaging.redis.MessageHandler; import org.bigbluebutton.core.api.IBigBlueButtonInGW; import org.red5.logging.Red5LoggerFactory; @@ -86,6 +87,11 @@ public class MeetingMessageHandler implements MessageHandler { log.info("User disconnected from global audio: data={}", logStr); bbbGW.userDisconnectedFromGlobalAudio(emm.voiceConf, emm.userid, emm.name); } + else if (msg instanceof GetAllMeetingsRequest) { + GetAllMeetingsRequest emm = (GetAllMeetingsRequest) msg; + log.info("Received GetAllMeetingsRequest"); + bbbGW.getAllMeetings("no_need_of_a_meeting_id"); + } } } else if (channel.equalsIgnoreCase(MessagingConstants.TO_SYSTEM_CHANNEL)) { IMessage msg = MessageFromJsonConverter.convert(message); diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java index 9e0cbb17743202ecb53a844663be46884b18bfe3..bd256d180f0711a1a3c5bd5d9599fd032ebc4a13 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/chat/ChatMessageListener.java @@ -31,11 +31,8 @@ public class ChatMessageListener implements MessageHandler{ String eventName = headerObject.get("name").toString(); eventName = eventName.replace("\"", ""); - if (eventName.equalsIgnoreCase("public_chat_message_event") || - eventName.equalsIgnoreCase("send_public_chat_message") || //identical - eventName.equalsIgnoreCase("private_chat_message_event") || - eventName.equalsIgnoreCase("send_private_chat_message") ||//identical - eventName.equalsIgnoreCase("get_chat_history")){ + if (eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST) || + eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)){ String meetingID = payloadObject.get("meeting_id").toString().replace("\"", ""); String requesterID = payloadObject.get("requester_id").toString().replace("\"", ""); @@ -67,13 +64,11 @@ public class ChatMessageListener implements MessageHandler{ map.put(ChatKeyUtil.TO_USERNAME, toUsername); map.put(ChatKeyUtil.MESSAGE, chatText); - //public message - if(eventName.equalsIgnoreCase("public_chat_message_event") - || eventName.equalsIgnoreCase("send_public_chat_message")) { + if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PUBLIC_CHAT_MESSAGE_REQUEST)) { bbbGW.sendPublicMessage(meetingID, requesterID, map); - } else if(eventName.equalsIgnoreCase("private_chat_message_event") - || eventName.equalsIgnoreCase("send_private_chat_message")) { - bbbGW.sendPrivateMessage(meetingID, requesterID, map); + } + else if(eventName.equalsIgnoreCase(MessagingConstants.SEND_PRIVATE_CHAT_MESSAGE_REQUEST)) { + bbbGW.sendPrivateMessage(meetingID, requesterID, map); } } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..baa1cd4099f32188401382a6095455b6cac21925 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/GetAllMeetingsRequest.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.conference.service.messaging; + +public class GetAllMeetingsRequest implements IMessage { + public static final String GET_ALL_MEETINGS_REQUEST_EVENT = "get_all_meetings_request"; + public static final String VERSION = "0.0.1"; + + public final String meetingId; + + public GetAllMeetingsRequest(String meetingId) { + this.meetingId = meetingId; + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java index ed14ac7d20d09df6867a441034f078953095b02f..70ac6181cee81c8bdc79ec4a8f9c35caab0751b8 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessageFromJsonConverter.java @@ -33,6 +33,8 @@ public class MessageFromJsonConverter { return UserConnectedToGlobalAudio.fromJson(message); case UserDisconnectedFromGlobalAudio.USER_DISCONNECTED_FROM_GLOBAL_AUDIO: return UserDisconnectedFromGlobalAudio.fromJson(message); + case GetAllMeetingsRequest.GET_ALL_MEETINGS_REQUEST_EVENT: + return new GetAllMeetingsRequest("the_string_is_not_used_anywhere"); } } } @@ -76,4 +78,6 @@ public class MessageFromJsonConverter { String id = payload.get(Constants.KEEP_ALIVE_ID).getAsString(); return new KeepAliveMessage(id); } + + //private static IMessage processGetAllMeetings(JsonObject) } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java index 3dfb3a9e69b3f78572c1c29ce1a47ca8c81f8dcd..7e6a0258b97a2f1103c2c9c2dd79d074354b58fa 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java @@ -37,8 +37,9 @@ public class MessagingConstants { public static final String TO_PRESENTATION_CHANNEL = TO_BBB_APPS_CHANNEL + ":presentation"; public static final String TO_POLLING_CHANNEL = TO_BBB_APPS_CHANNEL + ":polling"; public static final String TO_USERS_CHANNEL = TO_BBB_APPS_CHANNEL + ":users"; - public static final String TO_CHAT_CHANNEL = TO_BBB_APPS_CHANNEL + ":chat"; - + public static final String TO_CHAT_CHANNEL = TO_BBB_APPS_CHANNEL + ":chat"; + public static final String TO_VOICE_CHANNEL = TO_BBB_APPS_CHANNEL + ":voice"; + public static final String TO_WHITEBOARD_CHANNEL = TO_BBB_APPS_CHANNEL + ":whiteboard"; public static final String DESTROY_MEETING_REQUEST_EVENT = "DestroyMeetingRequestEvent"; public static final String CREATE_MEETING_REQUEST_EVENT = "CreateMeetingRequestEvent"; @@ -48,7 +49,11 @@ public class MessagingConstants { public static final String MEETING_DESTROYED_EVENT = "meeting_destroyed_event"; public static final String USER_JOINED_EVENT = "UserJoinedEvent"; public static final String USER_LEFT_EVENT = "UserLeftEvent"; + public static final String USER_LEFT_VOICE_REQUEST = "user_left_voice_request"; public static final String USER_STATUS_CHANGE_EVENT = "UserStatusChangeEvent"; public static final String SEND_POLLS_EVENT = "SendPollsEvent"; public static final String RECORD_STATUS_EVENT = "RecordStatusEvent"; + public static final String SEND_PUBLIC_CHAT_MESSAGE_REQUEST = "send_public_chat_message_request"; + public static final String SEND_PRIVATE_CHAT_MESSAGE_REQUEST = "send_private_chat_message_request"; + public static final String MUTE_USER_REQUEST = "mute_user_request"; } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java index b10a9f835ed00d34a31a35e26fdd81a8645ef7d1..3f8c2a45513ab6313f23bec01ca377c8115cd80b 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java @@ -4,6 +4,7 @@ package org.bigbluebutton.conference.service.participants; import org.bigbluebutton.conference.service.messaging.MessagingConstants; import org.bigbluebutton.conference.service.messaging.redis.MessageHandler; +//import org.bigbluebutton.core.api.*; import org.bigbluebutton.core.api.IBigBlueButtonInGW; import com.google.gson.JsonParser; @@ -20,7 +21,6 @@ public class ParticipantsListener implements MessageHandler{ @Override public void handleMessage(String pattern, String channel, String message) { if (channel.equalsIgnoreCase(MessagingConstants.TO_USERS_CHANNEL)) { - System.out.println("AntonChannel=(participants)" + channel); JsonParser parser = new JsonParser(); JsonObject obj = (JsonObject) parser.parse(message); @@ -29,46 +29,22 @@ public class ParticipantsListener implements MessageHandler{ String eventName = headerObject.get("name").toString().replace("\"", ""); - if(eventName.equalsIgnoreCase("register_user_request") || - eventName.equalsIgnoreCase("user_left_event") || - eventName.equalsIgnoreCase("user_joined_event") || - eventName.equalsIgnoreCase("get_users_request") || - eventName.equalsIgnoreCase("raise_user_hand_request")){ + if(eventName.equalsIgnoreCase("user_leaving_request") || + eventName.equalsIgnoreCase("user_raised_hand_message") || + eventName.equalsIgnoreCase("user_lowered_hand_message")){ String roomName = payloadObject.get("meeting_id").toString().replace("\"", ""); + String userID = payloadObject.get("userid").toString().replace("\"", ""); - if(eventName.equalsIgnoreCase("register_user_request")){ - String userID = payloadObject.get("user_id").toString().replace("\"", ""); - String username = payloadObject.get("name").toString().replace("\"", ""); - String role = payloadObject.get("role").toString().replace("\"", ""); - String externUserID = payloadObject.get("external_user_id").toString().replace("\"", ""); - - } - else if(eventName.equalsIgnoreCase("user_left_event")){ - String userID = payloadObject.get("user_id").toString().replace("\"", ""); - + if(eventName.equalsIgnoreCase("user_leaving_request")){ bbbInGW.userLeft(roomName, userID); } - else if(eventName.equalsIgnoreCase("user_joined_event")){ - String userID = payloadObject.get("user_id").toString().replace("\"", ""); - - bbbInGW.userJoin(roomName, userID); - } - else if(eventName.equalsIgnoreCase("get_users_request")){ - String requesterID = payloadObject.get("requester_id").toString().replace("\"", ""); - bbbInGW.getUsers(roomName, requesterID); + else if(eventName.equalsIgnoreCase("user_raised_hand_message")){ + bbbInGW.userRaiseHand(roomName, userID); } - else if(eventName.equalsIgnoreCase("raise_user_hand_request")){ - String userID = payloadObject.get("user_id").toString().replace("\"", ""); - boolean raise = Boolean.parseBoolean(payloadObject.get("raise").toString().replace("\"", "")); - - if(raise){ - bbbInGW.userRaiseHand(roomName, userID); - } - else { - String requesterID = payloadObject.get("requester_id").toString().replace("\"", ""); - bbbInGW.lowerHand(roomName, userID, requesterID); - } + else if(eventName.equalsIgnoreCase("user_lowered_hand_message")){ + String requesterID = payloadObject.get("lowered_by").toString().replace("\"", ""); + bbbInGW.lowerHand(roomName, userID, requesterID); } } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..9bc1fd75bc5df2452d4fefd0a8924cbe4d9dcc4b --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceKeyUtil.java @@ -0,0 +1,6 @@ +package org.bigbluebutton.conference.service.voice; + +public class VoiceKeyUtil { + public static final String MUTE = "mute"; + public static final String USERID = "userId"; +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ab7062c8b6b955377ba1c36ff35a8bfee8874372 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceMessageListener.java @@ -0,0 +1,53 @@ +package org.bigbluebutton.conference.service.voice; + +import org.bigbluebutton.conference.service.messaging.MessagingConstants; +import org.bigbluebutton.conference.service.messaging.redis.MessageHandler; +import com.google.gson.JsonParser; +import com.google.gson.JsonObject; + +import java.util.Map; +import java.util.HashMap; + +import org.bigbluebutton.core.api.IBigBlueButtonInGW; + +public class VoiceMessageListener implements MessageHandler{ + + private IBigBlueButtonInGW bbbGW; + + public void setBigBlueButtonInGW(IBigBlueButtonInGW bbbGW) { + this.bbbGW = bbbGW; + } + + @Override + public void handleMessage(String pattern, String channel, String message) { + if (channel.equalsIgnoreCase(MessagingConstants.TO_VOICE_CHANNEL)) { + + JsonParser parser = new JsonParser(); + JsonObject obj = (JsonObject) parser.parse(message); + JsonObject headerObject = (JsonObject) obj.get("header"); + JsonObject payloadObject = (JsonObject) obj.get("payload"); + + String eventName = headerObject.get("name").toString().replace("\"", ""); + + if (eventName.equalsIgnoreCase(MessagingConstants.MUTE_USER_REQUEST)){ + + String meetingID = payloadObject.get("meeting_id").toString().replace("\"", ""); + String requesterID = payloadObject.get("requester_id").toString().replace("\"", ""); + String userID = payloadObject.get("userid").toString().replace("\"", ""); + String muteString = payloadObject.get(VoiceKeyUtil.MUTE).toString().replace("\"", ""); + Boolean mute = Boolean.valueOf(muteString); + + System.out.println("handling mute_user_request"); + bbbGW.muteUser(meetingID, requesterID, userID, mute); + } + else if (eventName.equalsIgnoreCase(MessagingConstants.USER_LEFT_VOICE_REQUEST)){ + + String meetingID = payloadObject.get("meeting_id").toString().replace("\"", ""); + String userID = payloadObject.get("userid").toString().replace("\"", ""); + + System.out.println("handling user_left_voice_request"); + bbbGW.voiceUserLeft(meetingID, userID); + } + } + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java index 7e70ff62b593efe07180890d95c642622f6c1d10..b7b1c5966cda5acf2cbe20609078b745a0e0f0d0 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/voice/VoiceService.java @@ -17,11 +17,15 @@ * */ package org.bigbluebutton.conference.service.voice; - import org.slf4j.Logger; import org.red5.server.api.Red5; import org.bigbluebutton.conference.BigBlueButtonSession; import org.bigbluebutton.conference.Constants; import org.bigbluebutton.core.api.IBigBlueButtonInGW; +import org.slf4j.Logger; +import org.red5.server.api.Red5; +import org.bigbluebutton.conference.BigBlueButtonSession; +import org.bigbluebutton.conference.Constants; +import org.bigbluebutton.core.api.IBigBlueButtonInGW; import org.red5.logging.Red5LoggerFactory; import java.util.List; import java.util.Map; - + public class VoiceService { private static Logger log = Red5LoggerFactory.getLogger( VoiceService.class, "bigbluebutton" ); @@ -70,16 +74,17 @@ public class VoiceService { String requesterID = getBbbSession().getInternalUserID(); bbbInGW.isMeetingMuted(meetingID, requesterID); } - + + // not sure if this is used public void muteUnmuteUser(Map<String, Object> msg) { - Boolean mute = (Boolean) msg.get("mute"); - String userid = (String) msg.get("userId"); + Boolean mute = (Boolean) msg.get(VoiceKeyUtil.MUTE); + String userid = (String) msg.get(VoiceKeyUtil.USERID); String meetingID = Red5.getConnectionLocal().getScope().getName(); - String requesterID = getBbbSession().getInternalUserID(); + String requesterID = getBbbSession().getInternalUserID(); bbbInGW.muteUser(meetingID, requesterID, userid, mute); } - + public void lockMuteUser(Map<String, Object> msg) { Boolean lock = (Boolean) msg.get("lock"); String userid = (String) msg.get("userId"); diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java new file mode 100644 index 0000000000000000000000000000000000000000..47bc7b68c60f50e5301787ac2ac46fa8150a62b3 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/whiteboard/WhiteboardListener.java @@ -0,0 +1,52 @@ + +package org.bigbluebutton.conference.service.whiteboard; + + +import org.bigbluebutton.conference.service.messaging.MessagingConstants; +import org.bigbluebutton.conference.service.messaging.redis.MessageHandler; + +import org.bigbluebutton.core.api.IBigBlueButtonInGW; +import com.google.gson.JsonParser; +import com.google.gson.JsonObject; + +public class WhiteboardListener implements MessageHandler{ + + private IBigBlueButtonInGW bbbInGW; + + public void setBigBlueButtonInGW(IBigBlueButtonInGW bbbInGW) { + this.bbbInGW = bbbInGW; + } + + @Override + public void handleMessage(String pattern, String channel, String message) { + if (channel.equalsIgnoreCase(MessagingConstants.TO_WHITEBOARD_CHANNEL)) { + System.out.println("AntonChannel=(whiteboard)" + channel); + + JsonParser parser = new JsonParser(); + JsonObject obj = (JsonObject) parser.parse(message); + JsonObject headerObject = (JsonObject) obj.get("header"); + JsonObject payloadObject = (JsonObject) obj.get("payload"); + + String eventName = headerObject.get("name").toString().replace("\"", ""); + + if(eventName.equalsIgnoreCase("get_whiteboard_shapes_request")){ + //more cases to follow + + String roomName = payloadObject.get("meeting_id").toString().replace("\"", ""); + + if(eventName.equalsIgnoreCase("get_whiteboard_shapes_request")){ + String requesterID = payloadObject.get("requester_id").toString().replace("\"", ""); + if(payloadObject.get("whiteboard_id") != null){ + String whiteboardID = payloadObject.get("whiteboard_id").toString().replace("\"", ""); + System.out.println("\n FOUND A whiteboardID:" + whiteboardID + "\n"); + bbbInGW.requestWhiteboardAnnotationHistory(roomName, requesterID, whiteboardID, requesterID); + } + else { + System.out.println("\n DID NOT FIND A whiteboardID \n"); + } + System.out.println("\n\n\n user<" + requesterID + "> requested the shapes.\n\n"); + } + } + } + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java index 6e8fb6c695130bd5ab971394395e15ecdf83b862..b436e1b4444be87ca275d2571c60929c46df0379 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java @@ -14,6 +14,7 @@ public interface IBigBlueButtonInGW { String voiceBridge, long duration, boolean autoStartRecording, boolean allowStartStopRecording); void destroyMeeting(String meetingID); + void getAllMeetings(String meetingID); void lockSettings(String meetingID, Boolean locked, Map<String, Boolean> lockSettigs); diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala index 6810a1aec3fd587eae87f8b2f4473e580454501c..e71dafc4fad340f07842cb836bc3d23d77fadd30 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonActor.scala @@ -17,6 +17,7 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor with LogHelper case msg: CreateMeeting => handleCreateMeeting(msg) case msg: DestroyMeeting => handleDestroyMeeting(msg) case msg: KeepAliveMessage => handleKeepAliveMessage(msg) + case msg: GetAllMeetingsRequest => handleGetAllMeetingsRequest(msg) case msg: InMessage => handleMeetingMessage(msg) case _ => // do nothing } @@ -100,5 +101,45 @@ class BigBlueButtonActor(outGW: MessageOutGateway) extends Actor with LogHelper } } } - + + private def handleGetAllMeetingsRequest(msg: GetAllMeetingsRequest) { + var len = meetings.keys.size + println("meetings.size=" + meetings.size) + println("len_=" + len) + + val set = meetings.keySet + val arr : Array[String] = new Array[String](len) + set.copyToArray(arr) + val resultArray : Array[MeetingInfo] = new Array[MeetingInfo](len) + + for(i <- 0 until arr.length) { + val id = arr(i) + val duration = meetings.get(arr(i)).head.getDuration() + val name = meetings.get(arr(i)).head.getMeetingName() + val recorded = meetings.get(arr(i)).head.getRecordedStatus() + val voiceBridge = meetings.get(arr(i)).head.getVoiceBridgeNumber() + + var info = new MeetingInfo(id, name, recorded, voiceBridge, duration) + resultArray(i) = info + + //remove later + println("for a meeting:" + id) + println("Meeting Name = " + meetings.get(id).head.getMeetingName()) + println("isRecorded = " + meetings.get(id).head.getRecordedStatus()) + println("voiceBridge = " + voiceBridge) + println("duration = " + duration) + + //send the users + this ! (new GetUsers(id, "nodeJSapp")) + + //send the presentation + this ! (new GetPresentationInfo(id, "nodeJSapp", "nodeJSapp")) + + //send chat history + this ! (new GetChatHistoryRequest(id, "nodeJSapp", "nodeJSapp")) + } + + outGW.send(new GetAllMeetingsReply(resultArray)) + } + } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala index 514e795a10b7397db40e323a29465b54c0830bd7..b4473b9fc5f907ca23ad17a726341bd26e9a4107 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala @@ -30,7 +30,12 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen // println("******************** DESTROY MEETING [" + meetingID + "] ***************************** ") bbbGW.accept(new DestroyMeeting(meetingID)) } - + + def getAllMeetings(meetingID: String) { + println("******************** GET ALL MEETINGS ***************************** ") + bbbGW.accept(new GetAllMeetingsRequest("meetingId")) + } + def isAliveAudit(aliveId:String) { bbbGW.acceptKeepAlive(new KeepAliveMessage(aliveId)); } @@ -221,10 +226,12 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen val current = if (i == 1) true else false val thumbnail = presBaseUrl + "/thumbnail/" + i val swfUri = presBaseUrl + "/slide/" + i - val txtUri = presBaseUrl + "/textfiles/" + i - + + val txtUri = presBaseUrl + "/textfiles/slide-" + i + ".txt" + val pngUri = presBaseUrl + "/png/" + i + val p = new Page(id=id, num=num, thumbUri=thumbnail, swfUri=swfUri, - txtUri=txtUri, pngUri=thumbnail, + txtUri=txtUri, pngUri=pngUri, current=current) pages += (p.id -> p) } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala index 3635a8ad9479bcea19199a0eec5ca1d89381dc81..96e4e1146389145800aaf3351d233dadb91a1e5d 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala @@ -98,6 +98,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor { case msg: UndoWhiteboardRequest => handleUndoWhiteboardRequest(msg) case msg: EnableWhiteboardRequest => handleEnableWhiteboardRequest(msg) case msg: IsWhiteboardEnabledRequest => handleIsWhiteboardEnabledRequest(msg) + case msg: GetAllMeetingsRequest => handleGetAllMeetingsRequest(msg) //OUT MESSAGES case msg: MeetingCreated => handleMeetingCreated(msg) @@ -177,6 +178,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor { case msg: UndoWhiteboardEvent => handleUndoWhiteboardEvent(msg) case msg: WhiteboardEnabledEvent => handleWhiteboardEnabledEvent(msg) case msg: IsWhiteboardEnabledReply => handleIsWhiteboardEnabledReply(msg) + case msg: GetAllMeetingsReply => handleGetAllMeetingsReply(msg) case _ => // do nothing } @@ -1376,6 +1378,12 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor { dispatcher.dispatch(buildJson(header, payload)) } + private def handleGetAllMeetingsRequest(msg: GetAllMeetingsRequest) { + println("***** DISPATCHING GET ALL MEETINGS REQUEST *****************") + } + + + // OUT MESSAGES private def handleMeetingCreated(msg: MeetingCreated) { val json = MeetingMessageToJsonConverter.meetingCreatedToJson(msg) @@ -2196,4 +2204,9 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor { val json = WhiteboardMessageToJsonConverter.isWhiteboardEnabledReplyToJson(msg) dispatcher.dispatch(json) } + private def handleGetAllMeetingsReply(msg: GetAllMeetingsReply) { + val json = MeetingMessageToJsonConverter.getAllMeetingsReplyToJson(msg) + println("***** DISPATCHING GET ALL MEETINGS REPLY OUTMSG *****************") + dispatcher.dispatch(json) + } } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala index 7021f5959e52d1e73525fa315f3f007b8339a8ed..ec0003c803b7a91757a5662687f37a4f42ff9491 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala @@ -30,11 +30,27 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee var recording = false; var muted = false; var meetingEnded = false + + def getDuration():Long = { + duration + } + + def getMeetingName():String = { + meetingName + } + + def getRecordedStatus():Boolean = { + recorded + } + + def getVoiceBridgeNumber():String = { + voiceBridge + } val TIMER_INTERVAL = 30000 var hasLastWebUserLeft = false var lastWebUserLeftOn:Long = 0 - + class TimerActor(val timeout: Long, val who: Actor, val reply: String) extends Actor { def act { reactWithin(timeout) { @@ -121,7 +137,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee case _ => // do nothing } } - } + } def hasMeetingEnded():Boolean = { meetingEnded @@ -143,7 +159,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee def webUserJoined() { if (users.numWebUsers > 0) { lastWebUserLeftOn = 0 - } + } } def startRecordingIfAutoStart() { @@ -168,7 +184,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee lastWebUserLeftOn = timeNowInMinutes logger.debug("MonitorNumberOfWebUsers started for meeting [" + meetingID + "]") scheduleEndVoiceConference() - } + } } def handleMonitorNumberOfWebUsers() { diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala index a8d2c80e56ff9d095bac4460906a83377c5171ff..c4fd79ea05d2abf01d8abecff7d293b1e8268521 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala @@ -503,3 +503,7 @@ case class IsWhiteboardEnabledRequest( requesterID: String, replyTo: String ) extends InMessage + +case class GetAllMeetingsRequest( + meetingID: String /** Not used. Just to satisfy trait **/ + ) extends InMessage diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala index 6d02f3113a7a651af8ca591dd8c4a25a95f415e4..6443025d1f30d105650988144a6faf45109e197b 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala @@ -78,6 +78,7 @@ object MessageNames { val UNDO_WHITEBOARD = "undo_whiteboard_request" val ENABLE_WHITEBOARD = "enable_whiteboard_request" val IS_WHITEBOARD_ENABLED = "is_whiteboard_enabled_request" + val GET_ALL_MEETINGS_REQUEST = "get_all_meetings_request" // OUT MESSAGES val MEETING_CREATED = "meeting_created_message" @@ -158,5 +159,5 @@ object MessageNames { val MEETING_DESTROYED_EVENT = "meeting_destroyed_event" val KEEP_ALIVE_REPLY = "keep_alive_reply" val USER_LISTEN_ONLY = "user_listening_only" - + val GET_ALL_MEETINGS_REPLY = "get_all_meetings_reply" } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala index 553042c1249949ae55702c489ff9aaa5da504a7e..388d467738dcb047fba294ef5a3dc42ba09fc95f 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala @@ -647,7 +647,11 @@ case class IsWhiteboardEnabledReply( replyTo: String, version:String = Versions.V_0_0_1 ) extends IOutMessage - + +case class GetAllMeetingsReply( + meetings: Array[MeetingInfo], + version:String = Versions.V_0_0_1 +) extends IOutMessage // Value Objects case class MeetingVO( diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala index fc95079998d64d0f9c8630bb156cc325d1b82590..e9eb5ad35217a0acaee01b365711d96e47749315 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala @@ -5,7 +5,7 @@ object Role extends Enumeration { val MODERATOR = Value("MODERATOR") val VIEWER = Value("VIEWER") } - + case class Presenter( presenterID: String, presenterName: String, @@ -102,4 +102,6 @@ case class VoiceConfig(telVoice: String, webVoice: String, dialNumber: String) case class MeetingPasswords(moderatorPass: String, viewerPass: String) case class MeetingDuration(duration: Int = 0, createdTime: Long = 0, - startTime: Long = 0, endTime: Long = 0) \ No newline at end of file + startTime: Long = 0, endTime: Long = 0) + +case class MeetingInfo(meetingID: String, meetingName: String, recorded: Boolean, voiceBridge: String, duration: Long) diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala index 18572be961aeb33cc6e412b3c3305e6fc643a559..7e34b97810e895add249e9c9a4f969f87ebef230 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala @@ -53,6 +53,7 @@ trait UsersApp { } } + def handleMuteAllExceptPresenterRequest(msg: MuteAllExceptPresenterRequest) { meetingMuted = msg.mute outGW.send(new MeetingMuted(meetingID, recorded, meetingMuted)) @@ -61,7 +62,7 @@ trait UsersApp { outGW.send(new MuteVoiceUser(meetingID, recorded, msg.requesterID, u.userID, msg.mute)) } } - + def handleMuteMeetingRequest(msg: MuteMeetingRequest) { meetingMuted = msg.mute outGW.send(new MeetingMuted(meetingID, recorded, meetingMuted)) @@ -73,15 +74,31 @@ trait UsersApp { def handleValidateAuthToken(msg: ValidateAuthToken) { // println("*************** Got ValidateAuthToken message ********************" ) regUsers.get (msg.userId) match { - case Some(u) => { - logger.info("ValidateToken success: mid=[" + meetingID + "] uid=[" + msg.userId + "]") + case Some(u) => + { + val replyTo = meetingID + '/' + msg.userId + + //send the reply outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, true, msg.correlationId)) + + //send the list of users in the meeting + outGW.send(new GetUsersReply(meetingID, msg.userId, users.getUsers)) + + //send chat history + this ! (new GetChatHistoryRequest(meetingID, msg.userId, replyTo)) + + //join the user + handleUserJoin(new UserJoining(meetingID, msg.userId)) + + //send the presentation + logger.info("ValidateToken success: mid=[" + meetingID + "] uid=[" + msg.userId + "]") + this ! (new GetPresentationInfo(meetingID, msg.userId, replyTo)) } case None => { logger.info("ValidateToken failed: mid=[" + meetingID + "] uid=[" + msg.userId + "]") outGW.send(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, false, msg.correlationId)) } - } + } } def handleRegisterUser(msg: RegisterUser) { @@ -129,8 +146,23 @@ trait UsersApp { case None => // do nothing } } - - + + def handleLockUser(msg: LockUser) { + + } + + def handleLockAllUsers(msg: LockAllUsers) { + + } + + def handleGetLockSettings(msg: GetLockSettings) { + + } + + def handleIsMeetingLocked(msg: IsMeetingLocked) { + + } + def handleSetLockSettings(msg: SetLockSettings) { // println("*************** Received new lock settings ********************") if (!permissionsEqual(msg.settings)) { @@ -228,7 +260,7 @@ trait UsersApp { outGW.send(new UserUnsharedWebcam(meetingID, recorded, uvo.userID, stream)) } } - + def handleChangeUserStatus(msg: ChangeUserStatus):Unit = { if (users.hasUser(msg.userID)) { outGW.send(new UserStatusChange(meetingID, recorded, msg.userID, msg.status, msg.value)) diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala index bbbe0629367cf7e86057db6a3e487b8609a6bef6..d0d8a82027ef98db8ff61035915661d3f63ba895 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/whiteboard/WhiteboardApp.scala @@ -43,8 +43,7 @@ trait WhiteboardApp { wbModel.modifyText(wbId, shape) } else { // println("Received UNKNOWN whiteboard shape!!!!. status=[" + status + "], shapeType=[" + shapeType + "]") - } - + } wbModel.getWhiteboard(wbId) foreach {wb => // println("WhiteboardApp::handleSendWhiteboardAnnotationRequest - num shapes [" + wb.shapes.length + "]") outGW.send(new SendWhiteboardAnnotationEvent(meetingID, recorded, @@ -60,20 +59,20 @@ trait WhiteboardApp { } def handleGetWhiteboardShapesRequest(msg: GetWhiteboardShapesRequest) { -// println("WB: Received page history [" + msg.whiteboardId + "]") + //println("WB: Received page history [" + msg.whiteboardId + "]") wbModel.history(msg.whiteboardId) foreach {wb => outGW.send(new GetWhiteboardShapesReply(meetingID, recorded, - msg.requesterID, wb.id, wb.shapes.toArray, msg.replyTo)) + msg.requesterID, wb.id, wb.shapes.toArray, msg.replyTo)) } - } - + } + def handleClearWhiteboardRequest(msg: ClearWhiteboardRequest) { -// println("WB: Received clear whiteboard") + //println("WB: Received clear whiteboard") wbModel.clearWhiteboard(msg.whiteboardId) wbModel.getWhiteboard(msg.whiteboardId) foreach {wb => outGW.send(new ClearWhiteboardEvent(meetingID, recorded, - msg.requesterID, wb.id)) - } + msg.requesterID, wb.id)) + } } def handleUndoWhiteboardRequest(msg: UndoWhiteboardRequest) { diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala index ad82351546e0eb416be322205c788a82f30a380f..79973fb14798a06323fd7b1014153d8760980ad7 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingEventRedisPublisher.scala @@ -23,7 +23,8 @@ class MeetingEventRedisPublisher(service: MessageSender) extends OutMessageListe case msg: MeetingDestroyed => handleMeetingDestroyed(msg) case msg: KeepAliveMessageReply => handleKeepAliveMessageReply(msg) case msg: StartRecording => handleStartRecording(msg) - case msg: StopRecording => handleStopRecording(msg) + case msg: StopRecording => handleStopRecording(msg) + case msg: GetAllMeetingsReply => handleGetAllMeetingsReply(msg) case _ => //println("Unhandled message in MeetingEventRedisPublisher") } } @@ -84,5 +85,10 @@ class MeetingEventRedisPublisher(service: MessageSender) extends OutMessageListe private def handleMeetingHasEnded(msg: MeetingHasEnded) { val json = MeetingMessageToJsonConverter.meetingHasEndedToJson(msg) service.send(MessagingConstants.FROM_MEETING_CHANNEL, json) - } + } + + private def handleGetAllMeetingsReply(msg: GetAllMeetingsReply) { + val json = MeetingMessageToJsonConverter.getAllMeetingsReplyToJson(msg) + service.send(MessagingConstants.FROM_MEETING_CHANNEL, json) + } } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala index 915c9e7e9696a6faf718e9e886d78136d7581a19..6215c01c0aee4575c9872d9dd660cda6e42d6cdc 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/meeting/MeetingMessageToJsonConverter.scala @@ -108,7 +108,7 @@ object MeetingMessageToJsonConverter { Util.buildJson(header, payload) } - def stopRecordingToJson(msg: StopRecording):String = { + def stopRecordingToJson(msg: StopRecording):String = { val payload = new java.util.HashMap[String, Any]() payload.put(Constants.MEETING_ID, msg.meetingID) payload.put(Constants.RECORDED, msg.recorded) @@ -117,5 +117,12 @@ object MeetingMessageToJsonConverter { val header = Util.buildHeader(MessageNames.STOP_RECORDING, msg.version, None) Util.buildJson(header, payload) } - + + def getAllMeetingsReplyToJson(msg: GetAllMeetingsReply):String = { + val payload = new java.util.HashMap[String, Any]() + payload.put("meetings", msg.meetings) + + val header = Util.buildHeader(MessageNames.GET_ALL_MEETINGS_REPLY, msg.version, None) + Util.buildJson(header, payload) + } } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml index 8a173fba0c8dcf2c1e77ae1b317d6578f677f16e..02237809f7ff0a1005bf67122bbec543b40b0ee2 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-chat.xml @@ -34,5 +34,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="chat.service" class="org.bigbluebutton.conference.service.chat.ChatService"> <property name="chatApplication"> <ref local="chatApplication"/></property> </bean> - + + <bean id="chatMessageListener" class="org.bigbluebutton.conference.service.chat.ChatMessageListener"> + <property name="bigBlueButtonInGW" ref="bbbInGW" /> + </bean> </beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml index 2201a5ff352b40c8c089cf8f1730c1e182c0e40b..1438b14c19f1952f01c7a0a77208298f12a512f3 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-presentation.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -42,12 +42,4 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="presentationMessageListener" class="org.bigbluebutton.conference.service.presentation.PresentationMessageListener"> <property name="conversionUpdatesProcessor" ref="conversionUpdatesProcessor" /> </bean> - - <bean id="chatMessageListener" class="org.bigbluebutton.conference.service.chat.ChatMessageListener"> - <property name="bigBlueButtonInGW" ref="bbbInGW" /> - </bean> - - <bean id="participantsListener" class="org.bigbluebutton.conference.service.participants.ParticipantsListener"> - <property name="bigBlueButtonInGW" ref="bbbInGW" /> - </bean> </beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml index 0e8e62847c4a65bf38619038eab3dc94fd32baac..a3eb65037368452f389712d141ead8e2d7f7b184 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-users.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -26,7 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd "> - + <bean id="participantsHandler" class="org.bigbluebutton.conference.service.participants.ParticipantsHandler"> <property name="participantsApplication"> <ref local="participantsApplication"/></property> </bean> @@ -38,5 +38,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <bean id="participants.service" class="org.bigbluebutton.conference.service.participants.ParticipantsService"> <property name="participantsApplication"> <ref local="participantsApplication"/></property> </bean> - + + <bean id="participantsListener" class="org.bigbluebutton.conference.service.participants.ParticipantsListener"> + <property name="bigBlueButtonInGW" ref="bbbInGW" /> + </bean> </beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml index 8c426036f8c4b7c83c1019d775dc6b2e0c43ce04..adcfb71c3ad2909bf40a9aea324b2709d185b4ca 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-whiteboard.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -26,13 +26,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd "> - + <bean id="whiteboardApplication" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardApplication"> - <property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property> + <property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property> </bean> - + <bean id="whiteboard.service" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardService"> <property name="whiteboardApplication"> <ref local="whiteboardApplication"/></property> </bean> - + + <bean id="whiteboardListener" class="org.bigbluebutton.conference.service.whiteboard.WhiteboardListener"> + <property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property> + </bean> + </beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml index 47504b0d9f4ff10a03a032a9867ac3089b7a34bc..5563af25782d8361a0fea270e2be2824eb2e296f 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-redis-messaging.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -26,35 +26,36 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd "> - - + <bean id="redisMessageSender" class="org.bigbluebutton.conference.service.messaging.redis.MessageSender" init-method="start" destroy-method="stop"> - <property name="redisPool"> <ref bean="redisPool"/></property> - </bean> + <property name="redisPool"> <ref bean="redisPool"/></property> + </bean> <bean id="redisMessageReceiver" class="org.bigbluebutton.conference.service.messaging.redis.MessageReceiver" init-method="start" destroy-method="stop"> - <property name="redisPool"> <ref bean="redisPool"/></property> - <property name="messageHandler"> <ref local="redisMessageHandler"/> </property> - </bean> + <property name="redisPool"> <ref bean="redisPool"/></property> + <property name="messageHandler"> <ref local="redisMessageHandler"/> </property> + </bean> <bean id="redisMessageHandler" class="org.bigbluebutton.conference.service.messaging.redis.ReceivedMessageHandler" init-method="start" destroy-method="stop"> <property name="messageDistributor"><ref bean="redisMessageDistributor" /></property> </bean> - - <bean id="redisMessageDistributor" class="org.bigbluebutton.conference.service.messaging.redis.MessageDistributor"> - <property name="messageHandler"> <ref local="redisMessageHandler"/> </property> - <property name="messageListeners"> - <set> - <ref bean="presentationMessageListener" /> - <ref bean="chatMessageListener" /> - <ref bean="meetingMessageHandler" /> - <ref bean="pollMessageHandler" /> - <ref bean="participantsListener" /> - </set> - </property> - </bean> - + + <bean id="redisMessageDistributor" class="org.bigbluebutton.conference.service.messaging.redis.MessageDistributor"> + <property name="messageHandler"> <ref local="redisMessageHandler"/> </property> + <property name="messageListeners"> + <set> + <ref bean="presentationMessageListener" /> + <ref bean="chatMessageListener" /> + <ref bean="meetingMessageHandler" /> + <ref bean="pollMessageHandler" /> + <ref bean="participantsListener" /> + <ref bean="voiceMessageListener" /> + <ref bean="whiteboardListener" /> + </set> + </property> + </bean> + </beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml index bf2d885820935bd01ea9828280f5eb15b0115b05..308802613c3e833a32a2624035623069d66754bf 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-voice-app.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -23,13 +23,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:beans="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> - + <beans:bean id="voiceEventRecorder" class="org.bigbluebutton.webconference.voice.VoiceEventRecorder"> <beans:property name="recorderApplication" ref="recorderApplication"/> </beans:bean> - + <beans:bean id="voice.service" class="org.bigbluebutton.conference.service.voice.VoiceService"> <beans:property name="bigBlueButtonInGW" ref="bbbInGW"/> </beans:bean> - + + <beans:bean id="voiceMessageListener" class="org.bigbluebutton.conference.service.voice.VoiceMessageListener"> + <beans:property name="bigBlueButtonInGW" ref="bbbInGW" /> + </beans:bean> + </beans:beans> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml index 5e3f8ffa658400e806ddaf7d651c5f49e1e8b574..d1fb2b56b58f5f991f20e7080d66bdd10c00c329 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml @@ -3,7 +3,7 @@ BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). This program is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software diff --git a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy index 2dad58221d670c5ba3f70a81d632408d8837ce49..c6175a35e4afe881e76666616dc65619df0789b3 100755 --- a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy +++ b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy @@ -24,6 +24,14 @@ class UrlMappings { "/presentation/$conference/$room/$presentation_name/thumbnail/$id"(controller:"presentation") { action = [GET:'showThumbnail'] } + + "/presentation/$conference/$room/$presentation_name/pngs"(controller:"presentation") { + action = [GET:'numberOfPngs'] + } + + "/presentation/$conference/$room/$presentation_name/png/$id"(controller:"presentation") { + action = [GET:'showPngImage'] + } "/presentation/$conference/$room/$presentation_name/textfiles"(controller:"presentation") { action = [GET:'numberOfTextfiles'] diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 5b29897b398b82448bef950a4bfcd76e6a55640d..041af441a751b5b087f3fd5cd05b05c0494d677e 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -68,6 +68,11 @@ maxNumPages=200 # Maximum swf file size for load to the client (default 500000). MAX_SWF_FILE_SIZE=500000 +#---------------------------------------------------- +# Additional conversion of the presentation slides to PNG +# to be used in the HTML5 client +pngImagesRequired=false + # Default number of digits for voice conference users joining through the PSTN. defaultNumDigitsForTelVoice=5 diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index 48b13ebb91e886533f21902f6fad1b4eaea30c80..14501b7d88acd994020da998dbfdb8dc2c4414d6 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -96,6 +96,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <property name="maxSwfFileSize" value="${MAX_SWF_FILE_SIZE}"/> <property name="maxConversionTime" value="${maxConversionTime}"/> <property name="swfSlidesGenerationProgressNotifier" ref="swfSlidesGenerationProgressNotifier"/> + <property name="pngImagesRequired" value="${pngImagesRequired}"/> </bean> <bean id="imageToSwfSlidesGenerationService" class="org.bigbluebutton.presentation.imp.ImageToSwfSlidesGenerationService"> diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 2b44056aa68c8c32574d1b09e57f91e53259cc9d..99319f9756a272441047c6aeb6c9511463e39f7c 100644 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -303,6 +303,8 @@ class ApiController { boolean redirectImm = parseBoolean(params.redirectImmediately) String internalUserID = RandomStringUtils.randomAlphanumeric(12).toLowerCase() + + String authToken = RandomStringUtils.randomAlphanumeric(12).toLowerCase() String externUserID = params.userID if (StringUtils.isEmpty(externUserID)) { @@ -346,6 +348,7 @@ class ApiController { } UserSession us = new UserSession(); + us.authToken = authToken; us.internalUserId = internalUserID us.conferencename = meeting.getName() us.meetingID = meeting.getInternalId() @@ -382,7 +385,7 @@ class ApiController { meetingService.addUserSession(session['user-token'], us); // Register user into the meeting. - meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.internalUserId /* authToken for now */) + meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.authToken) log.info("Session user token for " + us.fullname + " [" + session['user-token'] + "]") session.setMaxInactiveInterval(SESSION_TIMEOUT); @@ -421,7 +424,7 @@ class ApiController { message("You have joined successfully.") meeting_id(us.meetingID) user_id(us.internalUserId) - auth_token(us.internalUserId) + auth_token(us.authToken) } } } @@ -1353,6 +1356,7 @@ class ApiController { externMeetingID = us.externMeetingID externUserID = us.externUserID internalUserID = us.internalUserId + authToken = us.authToken role = us.role conference = us.conference room = us.room diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy index ccd4229e5d3d1e0e5ac86f3a71839093e0404fe3..d82459beb84841c0eea149c2e34c6a80fbf54bfc 100644 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy @@ -297,7 +297,28 @@ class PresentationController { } } } - + + def numberOfPngs = { + def filename = params.presentation_name + def f = confInfo() + def numPngs = presentationService.numberOfPngs(f.conference, f.room, filename) + withFormat { + xml { + render(contentType:"text/xml") { + conference(id:f.conference, room:f.room) { + presentation(name:filename) { + pngs(count:numPngs) { + for (def i=0;i<numPngs;i++) { + png(name:"pngs/${i}") + } + } + } + } + } + } + } + } + def numberOfTextfiles = { def filename = params.presentation_name def f = confInfo() diff --git a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy index 422b8afa9a7eb99d11e8da8f75afba931028c125..2b1f6b0cc22f96f6db24bf59f1a0532b23897d8c 100644 --- a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy +++ b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy @@ -1,45 +1,45 @@ -/** -* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -* -* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). -* -* This program is free software; you can redistribute it and/or modify it under the -* terms of the GNU Lesser General Public License as published by the Free Software -* Foundation; either version 3.0 of the License, or (at your option) any later -* version. -* -* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY -* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License along -* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. -* -*/ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). +* +* This program is free software; you can redistribute it and/or modify it under the +* terms of the GNU Lesser General Public License as published by the Free Software +* Foundation; either version 3.0 of the License, or (at your option) any later +* version. +* +* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License along +* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. +* +*/ package org.bigbluebutton.web.services import java.util.concurrent.*; -import java.lang.InterruptedException -import org.bigbluebutton.presentation.DocumentConversionService -import org.bigbluebutton.presentation.UploadedPresentation +import java.lang.InterruptedException +import org.bigbluebutton.presentation.DocumentConversionService +import org.bigbluebutton.presentation.UploadedPresentation class PresentationService { - static transactional = false - DocumentConversionService documentConversionService - def presentationDir - def testConferenceMock - def testRoomMock - def testPresentationName - def testUploadedPresentation - def defaultUploadedPresentation - def presentationBaseUrl - + static transactional = false + DocumentConversionService documentConversionService + def presentationDir + def testConferenceMock + def testRoomMock + def testPresentationName + def testUploadedPresentation + def defaultUploadedPresentation + def presentationBaseUrl + def deletePresentation = {conf, room, filename -> def directory = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename) - deleteDirectory(directory) + deleteDirectory(directory) } - + def deleteDirectory = {directory -> log.debug "delete = ${directory}" /** @@ -47,7 +47,7 @@ class PresentationService { * We need to delete files inside a directory before a * directory can be deleted. **/ - File[] files = directory.listFiles(); + File[] files = directory.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]) @@ -56,9 +56,9 @@ class PresentationService { } } // Now that the directory is empty. Delete it. - directory.delete() + directory.delete() } - + def listPresentations = {conf, room -> def presentationsList = [] def directory = roomDirectory(conf, room) @@ -69,95 +69,99 @@ class PresentationService { if( file.isDirectory() ) presentationsList.add( file.name ) } - } + } return presentationsList } - - def getPresentationDir = { - return presentationDir + + def getPresentationDir = { + return presentationDir } - - def processUploadedPresentation = {uploadedPres -> - // Run conversion on another thread. - Timer t = new Timer(uploadedPres.getName(), false) - - t.runAfter(1000) { - try { - documentConversionService.processDocument(uploadedPres) - } finally { - t.cancel() - } - } - } - + + def processUploadedPresentation = {uploadedPres -> + // Run conversion on another thread. + Timer t = new Timer(uploadedPres.getName(), false) + + t.runAfter(1000) { + try { + documentConversionService.processDocument(uploadedPres) + } finally { + t.cancel() + } + } + } + def showSlide(String conf, String room, String presentationName, String id) { new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "slide-${id}.swf") - } - - def showPngImage(String conf, String room, String presentationName, String id) { - new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + id) } - + + def showPngImage(String conf, String room, String presentationName, String id) { + new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + "slide${id}.png") + } + def showPresentation = {conf, room, filename -> new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename + File.separatorChar + "slides.swf") } - - def showThumbnail = {conf, room, presentationName, thumb -> + + def showThumbnail = {conf, room, presentationName, thumb -> println "Show thumbnails request for $presentationName $thumb" def thumbFile = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "thumbnails" + File.separatorChar + "thumb-${thumb}.png" log.debug "showing $thumbFile" - + new File(thumbFile) - } - - def showTextfile = {conf, room, presentationName, textfile -> - println "Show textfiles request for $presentationName $textfile" - def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + - "textfiles" + File.separatorChar + "slide-${textfile}.txt" - log.debug "showing $txt" - - new File(txt) } - + + def showTextfile = {conf, room, presentationName, textfile -> + println "Show textfiles request for $presentationName $textfile" + def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + + "textfiles" + File.separatorChar + "slide-${textfile}.txt" + log.debug "showing $txt" + + new File(txt) + } + def numberOfThumbnails = {conf, room, name -> def thumbDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "thumbnails") thumbDir.listFiles().length - } - - def numberOfTextfiles = {conf, room, name -> - log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles" - def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles") - textfilesDir.listFiles().length } - - def roomDirectory = {conf, room -> - return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room) - } - - def testConversionProcess() { - File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName) - - if (presDir.exists()) { - File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation) - if (pres.exists()) { - UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName); - uploadedPres.setUploadedFile(pres); - // Run conversion on another thread. - new Timer().runAfter(1000) - { - documentConversionService.processDocument(uploadedPres) - } - } else { - log.error "${pres.absolutePath} does NOT exist" - } - } else { - log.error "${presDir.absolutePath} does NOT exist." - } - - } - -} + + def numberOfPngs = {conf, room, name -> + def PngsDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "pngs") + PngsDir.listFiles().length + } + + def numberOfTextfiles = {conf, room, name -> + log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles" + def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles") + textfilesDir.listFiles().length + } + + def roomDirectory = {conf, room -> + return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room) + } + + def testConversionProcess() { + File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName) + + if (presDir.exists()) { + File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation) + if (pres.exists()) { + UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName); + uploadedPres.setUploadedFile(pres); + // Run conversion on another thread. + new Timer().runAfter(1000) + { + documentConversionService.processDocument(uploadedPres) + } + } else { + log.error "${pres.absolutePath} does NOT exist" + } + } else { + log.error "${presDir.absolutePath} does NOT exist." + } + + } +} /*** Helper classes **/ import java.io.FilenameFilter; @@ -166,4 +170,4 @@ class PngFilter implements FilenameFilter { public boolean accept(File dir, String name) { return (name.endsWith(".png")); } -} +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java index 480b835bd3ab7fabd8fe97882af5faa3af0ed6d1..57abb2bc9424ce702b4c88a0e470784dc2d78c06 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java @@ -20,6 +20,7 @@ package org.bigbluebutton.api.domain; public class UserSession { + public String authToken = null; public String internalUserId = null; public String conferencename = null; public String meetingID = null; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java index 13f34180f378242f58dfde7c75f58b0fd2d0166c..219028f5b22198284be638e53146487bc4fd8427 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PdfToSwfSlidesGenerationService.java @@ -1,7 +1,7 @@ /** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ * -* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +* Copyright (c) 2015 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -59,6 +59,7 @@ public class PdfToSwfSlidesGenerationService { private long MAX_CONVERSION_TIME = 5*60*1000; private String BLANK_SLIDE; private int MAX_SWF_FILE_SIZE; + private boolean pngImagesRequired; public void generateSlides(UploadedPresentation pres) { log.debug("Generating slides"); @@ -66,9 +67,14 @@ public class PdfToSwfSlidesGenerationService { log.info("Determined number of pages. MeetingId=[" + pres.getMeetingId() + "], presId=[" + pres.getId() + "], name=[" + pres.getName() + "], numPages=[" + pres.getNumberOfPages() + "]"); if (pres.getNumberOfPages() > 0) { convertPdfToSwf(pres); -// createPngImages(pres); createTextFiles(pres); createThumbnails(pres); + + // only create PNG images if the configuration requires it + if (pngImagesRequired) { + createPngImages(pres); + } + notifier.sendConversionCompletedMessage(pres); } } @@ -207,6 +213,10 @@ public class PdfToSwfSlidesGenerationService { public void setMaxSwfFileSize(int size) { this.MAX_SWF_FILE_SIZE = size; } + + public void setPngImagesRequired(boolean png) { + this.pngImagesRequired = png; + } public void setThumbnailCreator(ThumbnailCreator thumbnailCreator) { this.thumbnailCreator = thumbnailCreator; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java index 4750593bc2f6b51243bb6ae73c6c33c88dff5d32..e36a22639deb412c39d7c76aa030f326f08848e7 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/PngImageCreatorImp.java @@ -66,7 +66,8 @@ public class PngImageCreatorImp implements PngImageCreator { for(int i=1; i<=pres.getNumberOfPages(); i++){ File tmp = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "tmp" + File.separatorChar + "slide" + i + ".pdf"); File destpng = new File(imagePresentationDir.getAbsolutePath() + File.separatorChar + "slide" + i + ".png"); - COMMAND = IMAGEMAGICK_DIR + "/convert -density 300x300 -quality 90 +dither -depth 8 -colors 256 " + File.separatorChar + tmp.getAbsolutePath() + " " + destpng.getAbsolutePath(); + COMMAND = IMAGEMAGICK_DIR + "/convert -density 300x300 -quality 90 +dither -depth 8 -colors 256 " + File.separatorChar + tmp.getAbsolutePath() + " " + destpng.getAbsolutePath(); + done = new ExternalProcessExecutor().exec(COMMAND, 60000); if(!done){ break; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java index 5ddf09db959250a153ed013f1a9169585666676e..23978e7e3bd1f304dbae425546eb682a66e54abd 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/ThumbnailCreatorImp.java @@ -1,7 +1,7 @@ /** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -* -* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). +* +* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software @@ -69,7 +69,6 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { String source = pres.getUploadedFile().getAbsolutePath(); String dest; String COMMAND = ""; - if(SupportedFileTypes.isImageFile(pres.getFileType())){ dest = thumbsDir.getAbsolutePath() + File.separator + TEMP_THUMB_NAME + ".png"; COMMAND = IMAGEMAGICK_DIR + "/convert -thumbnail 150x150 " + source + " " + dest; @@ -77,22 +76,22 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { dest = thumbsDir.getAbsolutePath() + File.separator + "thumb-"; COMMAND = IMAGEMAGICK_DIR + "/gs -q -sDEVICE=pngalpha -dBATCH -dNOPAUSE -dNOPROMPT -dDOINTERPOLATE -dPDFFitPage -r16 -sOutputFile=" + dest +"%d.png " + source; } - + boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); - + if (done) { return true; - } else { - log.warn("Failed to create thumbnails: " + COMMAND); + } else { + log.warn("Failed to create thumbnails: " + COMMAND); } - return false; + return false; } - + private File determineThumbnailDirectory(File presentationFile) { return new File(presentationFile.getParent() + File.separatorChar + "thumbnails"); } - + private void renameThumbnails(File dir) { /* * If more than 1 file, filename like 'temp-thumb-X.png' else filename is 'temp-thumb.png' @@ -137,17 +136,17 @@ public class ThumbnailCreatorImp implements ThumbnailCreator { } } } - + private void copyBlankThumbnail(File thumb) { try { FileUtils.copyFile(new File(BLANK_THUMBNAIL), thumb); } catch (IOException e) { log.error("IOException while copying blank thumbnail."); - } + } } - - private void cleanDirectory(File directory) { - File[] files = directory.listFiles(); + + private void cleanDirectory(File directory) { + File[] files = directory.listFiles(); for (int i = 0; i < files.length; i++) { files[i].delete(); } diff --git a/client/bbb-html5-client/assets/css/navbar.less b/client/bbb-html5-client/assets/css/navbar.less old mode 100644 new mode 100755 index 0f9b2e557fc4f44cbc4126f4e23ced0183bd2572..ecf578d29c64b5231aba2aa6bedd20f5c0a3a3e8 --- a/client/bbb-html5-client/assets/css/navbar.less +++ b/client/bbb-html5-client/assets/css/navbar.less @@ -69,7 +69,7 @@ } .navbar-btn-group-right { - dispaly:block; + display:block; float:right; } } diff --git a/client/bbb-html5-client/config.coffee b/client/bbb-html5-client/config.coffee index 416d181cfc4c5ae4847b3eb04adbddc850985be6..210cdfbec64eceff5dcd46f9ac1afe40f79f295f 100755 --- a/client/bbb-html5-client/config.coffee +++ b/client/bbb-html5-client/config.coffee @@ -33,6 +33,7 @@ config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*" config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat" config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting" config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users" +config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard" config.redis.internalChannels = {} config.redis.internalChannels.receive = "html5-receive" config.redis.internalChannels.reply = "html5-reply" diff --git a/client/bbb-html5-client/lib/clientproxy.coffee b/client/bbb-html5-client/lib/clientproxy.coffee index 21045477237921a05cb6415de17609aab029a816..46d253486376d9049711d5a69f31ff9b7c82cbdb 100755 --- a/client/bbb-html5-client/lib/clientproxy.coffee +++ b/client/bbb-html5-client/lib/clientproxy.coffee @@ -20,7 +20,7 @@ module.exports = class ClientProxy @io.sockets.on 'connection', (socket) => log.debug({ client: socket.id }, "Client has connected.") socket.on 'message', (jsonMsg) => - log.debug({ message: jsonMsg }, "Received message") # TODO to check whether only 'message' works or 'djhkwa' too + log.debug({ message: jsonMsg }, "Received message") @_handleMessage(socket, jsonMsg) socket.on 'disconnect', => @_handleClientDisconnected socket @@ -49,8 +49,10 @@ module.exports = class ClientProxy callback?() _handleClientDisconnected: (socket) -> - if socket.userId? - log.info("User [#{socket.userId}] has disconnected.") + console.log "\ntrying to disconnect" + + #if socket.userId? + # log.info("User [#{socket.userId}] has disconnected.") _handleMessage: (socket, message) -> if message.header?.name? @@ -64,6 +66,8 @@ module.exports = class ClientProxy @_handleLoginMessage socket, message when 'send_public_chat_message' @controller.sendingChat message + when 'user_leaving_request' + @controller.sendingUsersMessage message else log.error({ message: message }, 'Unknown message name.') diff --git a/client/bbb-html5-client/lib/controller.coffee b/client/bbb-html5-client/lib/controller.coffee index f8b1327ca59b0c058495a74f76470fad22efbcc0..5d1c93c616affcd51a21ffc38b9548d93e54abef 100755 --- a/client/bbb-html5-client/lib/controller.coffee +++ b/client/bbb-html5-client/lib/controller.coffee @@ -35,4 +35,8 @@ module.exports = class Controller # @clientProxy.endMeeting() sendingChat: (data) => - @messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data) \ No newline at end of file + @messageBus.sendingToRedis(config.redis.channels.toBBBApps.chat, data) + + + sendingUsersMessage: (data) => + @messageBus.sendingToRedis(config.redis.channels.toBBBApps.users, data) diff --git a/client/bbb-html5-client/lib/messagebus.coffee b/client/bbb-html5-client/lib/messagebus.coffee index 20174d840465520335224660f027ec8606ab4b5a..e6723ba89799f275215e35e0f71fbdb7df8178ff 100755 --- a/client/bbb-html5-client/lib/messagebus.coffee +++ b/client/bbb-html5-client/lib/messagebus.coffee @@ -1,8 +1,8 @@ -postal = require('postal') crypto = require 'crypto' +postal = require 'postal' config = require '../config' -log = require './bbblogger' +log = require './bbblogger' moduleDeps = ["RedisPubSub"] @@ -40,4 +40,4 @@ module.exports = class MessageBus data: data sendingToRedis: (channel, message) => - @pubSub.publishing(channel, message) \ No newline at end of file + @pubSub.publishing(channel, message) diff --git a/client/bbb-html5-client/lib/redispubsub.coffee b/client/bbb-html5-client/lib/redispubsub.coffee index aa05f96534474825277c27cd5bc86c0eb79173d7..c8e007f391772c5eb274c0e4b33654d1a53eb3d0 100644 --- a/client/bbb-html5-client/lib/redispubsub.coffee +++ b/client/bbb-html5-client/lib/redispubsub.coffee @@ -58,7 +58,7 @@ module.exports = class RedisPubSub # put the entry in the hash so we can match the response later @pendingRequests[correlationId] = entry message.header.reply_to = correlationId - console.log("\n\nmessage=" + JSON.stringify(message) + "\n\n") + console.log "\n Waiting for a reply on:" + JSON.stringify(message) log.info({ message: message, channel: config.redis.channels.toBBBApps.meeting}, "Publishing a message") @pubClient.publish(config.redis.channels.toBBBApps.meeting, JSON.stringify(message)) @@ -73,16 +73,16 @@ module.exports = class RedisPubSub # TODO: this has to be in a try/catch block, otherwise the server will # crash if the message has a bad format message = JSON.parse(jsonMsg) + correlationId = message.payload?.reply_to or message.header?.reply_to - unless message.header?.name is "keep_alive_reply" #temporarily stop logging the keep_alive_reply message + unless message.header?.name is "keep_alive_reply" + console.log "\nchannel=" + channel + console.log "correlationId=" + correlationId if correlationId? + console.log "eventType=" + message.header?.name + "\n" log.debug({ pattern: pattern, channel: channel, message: message}, "Received a message from redis") - #console.log "=="+JSON.stringify message # retrieve the request entry - #correlationId = message.header?.reply_to - correlationId = message.payload?.reply_to or message.header?.reply_to - console.log "\ncorrelation_id=" + correlationId if correlationId? and @pendingRequests?[correlationId]? entry = @pendingRequests[correlationId] # make sure the message in the timeout isn't triggered by clearing it @@ -94,76 +94,63 @@ module.exports = class RedisPubSub topic: entry.replyTo.topic data: message else - #sendToController(message) - - if message.header?.name is 'validate_auth_token_reply' - if message.payload?.valid is "true" - - #TODO use the message library for these messages. Perhaps put it in Modules?! - - joinMeetingMessage = { - "payload": { - "meeting_id": message.payload.meeting_id - "user_id": message.payload.userid - }, - "header": { - "timestamp": new Date().getTime() - "reply_to": message.payload.meeting_id + "/" + message.payload.userid - "name": "user_joined_event" - } - } - # the user joins the meeting - - @pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(joinMeetingMessage)) - console.log "just published the joinMeetingMessage in RedisPubSub" - - #get the list of users in the meeting - getUsersMessage = { - "payload": { - "meeting_id": message.payload.meeting_id - "requester_id": message.payload.userid - }, - "header": { - "timestamp": new Date().getTime() - "reply_to": message.payload.meeting_id + "/" + message.payload.userid - "name": "get_users_request" - } - } - - @pubClient.publish(config.redis.channels.toBBBApps.users, JSON.stringify(getUsersMessage)) - console.log "just published the getUsersMessage in RedisPubSub" - - #get the chat history - getChatHistory = { + if message.header?.name is 'get_presentation_info_reply' + #filter for the current=true page on the server-side + currentPage = null + numCurrentPage = null + presentations = message.payload?.presentations + + for presentation in presentations + pages = presentation.pages + + for page in pages + if page.current is true + currentPage = page + numCurrentPage = page.num + + console.log "\n\n\n\n the message is: " + JSON.stringify message + console.log "\n" + message.payload?.presentations[0]?.id + "/" + numCurrentPage + "\n\n" + #request the whiteboard information + requestMessage = { "payload": { - "meeting_id": message.payload.meeting_id - "requester_id": message.payload.userid + "meeting_id": message.payload?.meeting_id + "requester_id": message.payload?.requester_id + "whiteboard_id": message.payload?.presentations[0]?.id + "/" + numCurrentPage #not sure if always [0] }, "header": { "timestamp": new Date().getTime() - "reply_to": message.payload.meeting_id + "/" + message.payload.userid - "name": "get_chat_history" + "name": "get_whiteboard_shapes_request" } } - - @pubClient.publish(config.redis.channels.toBBBApps.chat, JSON.stringify(getChatHistory)) - console.log "just published the getChatHistory in RedisPubSub" - - - else if message.header?.name is 'get_users_reply' - console.log 'got a reply from bbb-apps for get users' + @publishing(config.redis.channels.toBBBApps.whiteboard, requestMessage) + + #strip off excess data, leaving only the current slide information + message.payload.currentPage = currentPage + message.payload.presentations = null + message.header.name = "presentation_page" + + else if message.header?.name is 'presentation_shared_message' + currentPage = null + presentation = message.payload?.presentation + for page in presentation.pages + if page.current is true + currentPage = page + + #strip off excess data, leaving only the current slide information + message.payload.currentPage = currentPage + message.payload.presentation = null + message.header.name = "presentation_page" + + else if message.header?.name is 'presentation_page_changed_message' + message.payload.currentPage = message.payload?.page + message.payload?.page = null + message.header.name = "presentation_page" + + console.log " Sending to Controller (In):" + message.header?.name sendToController(message) - else if message.header?.name is 'get_chat_history_reply' - console.log 'got a reply from bbb-apps for chat history' - sendToController(message) - - else if message.header?.name is 'send_public_chat_message' - console.log "just got a public chat message :" + JSON.stringify message - sendToController (message) - publishing: (channel, message) => - console.log '\n Publishing\n' + console.log "Publishing #{message.header?.name}" @pubClient.publish(channel, JSON.stringify(message)) sendToController = (message) -> @@ -171,4 +158,3 @@ sendToController = (message) -> channel: config.redis.internalChannels.receive topic: "broadcast" data: message - diff --git a/client/bbb-html5-client/public/js/collections/users.coffee b/client/bbb-html5-client/public/js/collections/users.coffee index 581d0a389f64df6684da49bd6c70c9edfc6156c9..9716ade8bd60768feb61f1083c40e26d224ef80e 100755 --- a/client/bbb-html5-client/public/js/collections/users.coffee +++ b/client/bbb-html5-client/public/js/collections/users.coffee @@ -5,7 +5,6 @@ define [ 'cs!models/user' ], (_, Backbone, globals, UserModel) -> - # TODO: this class should actually store UserModel's, for now it is only trigerring events UsersCollection = Backbone.Collection.extend model: UserModel @@ -23,38 +22,41 @@ define [ _registerEvents: -> - globals.events.on "connection:user_list_change", (users) => - globals.events.trigger("users:user_list_change", users) - globals.events.on "connection:load_users", (users) => + #alert "load users" for userBlock in users @add [ - id : userBlock.id - userid: userBlock.id - username: userBlock.name + new UserModel {id: userBlock.id, userid: userBlock.id, username: userBlock.name} ] globals.events.trigger("users:load_users", users) - globals.events.on "connection:user_join", (userid, username) => - console.log "users.coffee: on(connection:user_join)" + username - @add [ - id : userid - userid: userid - username: username - ] - globals.events.trigger("users:user_join", userid, username) + #globals.events.on "getUsers", => + #users = @toJSON() + #globals.events.trigger("receiveUsers", users) - globals.events.on "connection:user_leave", (userid) => - toDel = @get(userid) - @remove(toDel) - globals.events.trigger("users:user_leave", userid) + globals.events.on "connection:user_join", (newUserid, newUsername) => + unless @get(newUserid)? #check if the user is already present + #newUser = new UserModel {id: newUserid, userid: newUserid, username: newUsername} + newUser = new UserModel() + newUser.id = newUserid + newUser.userid = newUserid + newUser.username = newUsername + + @add [ + newUser + ] + globals.events.trigger("user:add_new_user", newUser) globals.events.on "connection:user_left", (userid) => toDel = @get(userid) - @remove(toDel) - globals.events.trigger("users:user_left", userid) + if toDel? # only remove if the user model was found + @remove(toDel) + globals.events.trigger("users:user_left", userid) globals.events.on "connection:setPresenter", (userid) => globals.events.trigger("users:setPresenter", userid) + render: -> + alert "user collection rendering" + UsersCollection diff --git a/client/bbb-html5-client/public/js/models/connection.coffee b/client/bbb-html5-client/public/js/models/connection.coffee index 99f2300c88016ac38e07400fd0d6c7f69c3a699d..165deecbb2de6d88d44af982db322a177d13ea7e 100755 --- a/client/bbb-html5-client/public/js/models/connection.coffee +++ b/client/bbb-html5-client/public/js/models/connection.coffee @@ -17,13 +17,10 @@ define [ @userId = @getUrlVars()["user_id"] @meetingId = @getUrlVars()["meeting_id"] @username = @getUrlVars()["username"] + globals.meetingName = decodeURI(@getUrlVars()["meetingName"]) disconnect: -> - if @socket? - console.log "disconnecting from", @host - @socket.disconnect() - else - console.log "tried to disconnect but it's not connected" + alert( " i go through disconnect") # not used right now connect: -> console.log("user_id=" + @userId + " auth_token=" + @authToken + " meeting_id=" + @meetingId) @@ -57,12 +54,10 @@ define [ console.log "socket.io received: data" globals.events.trigger("message", data) - # Immediately say we are connected @socket.on "connect", => console.log "socket on: connect" globals.events.trigger("connection:connected") - #@socket.emit "user connect" # tell the server we have a new user message = { "payload": { @@ -80,17 +75,22 @@ define [ if @authToken? and @userId? and @meetingId? @socket.emit "message", message + # Received a list of users from bbb-apps + # param {object} message object @socket.on "get_users_reply", (message) => - users = [] - for user in message.payload?.users - users.push user + requesterId = message.payload?.requester_id - globals.events.trigger("connection:load_users", users) + if(requesterId is @userId) + users = [] + for user in message.payload?.users + users.push user + globals.events.trigger("connection:load_users", users) + + # Received a the chat history for a meeting + # @param {object} message object @socket.on "get_chat_history_reply", (message) => requesterId = message.payload?.requester_id - - #console.log("my_id=" + @userId + ", while requester_id=" + requesterId) if(requesterId is @userId) globals.events.trigger("connection:all_messages", message.payload?.chat_history) @@ -102,12 +102,6 @@ define [ text = message.payload.message.message globals.events.trigger("connection:msg", username, text) - # Received event to logout yourself - @socket.on "logout", -> - console.log "socket on: logout" - Utils.postToUrl "logout" - window.location.replace "./" - # If the server disconnects from the client or vice-versa @socket.on "disconnect", -> console.log "socket on: disconnect" @@ -115,83 +109,91 @@ define [ globals.events.trigger("connection:disconnected") @socket = null - @socket.on "reconnect", -> - console.log "socket on: reconnect" - globals.events.trigger("connection:reconnect") + #@socket.on "reconnect", -> + # console.log "socket on: reconnect" + # globals.events.trigger("connection:reconnect") - @socket.on "reconnecting", -> - console.log "socket on: reconnecting" - globals.events.trigger("connection:reconnecting") + #@socket.on "reconnecting", -> + # console.log "socket on: reconnecting" + # globals.events.trigger("connection:reconnecting") - @socket.on "reconnect_failed", -> - console.log "socket on: reconnect_failed" - globals.events.trigger("connection:reconnect_failed") + #@socket.on "reconnect_failed", -> + # console.log "socket on: reconnect_failed" + # globals.events.trigger("connection:reconnect_failed") # If an error occurs while not connected # @param {string} reason Reason for the error. - @socket.on "error", (reason) -> - console.error "unable to connect socket.io", reason + #@socket.on "error", (reason) -> #TODO + # console.error "unable to connect socket.io", reason # Received event to update all the slide images # @param {Array} urls list of URLs to be added to the paper (after old images are removed) - @socket.on "all_slides", (allSlidesEventObject) => - console.log "socket on: all_slides" - console.log "allSlidesEventObject: " + allSlidesEventObject - globals.events.trigger("connection:all_slides", allSlidesEventObject); + #@socket.on "all_slides", (allSlidesEventObject) => + # console.log "socket on: all_slides" + # console.log "allSlidesEventObject: " + allSlidesEventObject + # globals.events.trigger("connection:all_slides", allSlidesEventObject); # Received event to clear the whiteboard shapes - @socket.on "clrPaper",=> - console.log "socket on: clrPaper" - globals.events.trigger("connection:clrPaper") + #@socket.on "clrPaper",=> + # console.log "socket on: clrPaper" + # globals.events.trigger("connection:clrPaper") + + # Received event to update all the shapes in the whiteboard + # @param {Array} shapes Array of shapes to be drawn + #@socket.on "allShapes", (allShapesEventObject) => + # # check for the requester_id + # console.log "socket on: all_shapes" + allShapesEventObject + # globals.events.trigger("connection:all_shapes", allShapesEventObject) # Received event to update all the shapes in the whiteboard # @param {Array} shapes Array of shapes to be drawn - @socket.on "allShapes", (allShapesEventObject) => - console.log "socket on: all_shapes" + allShapesEventObject - globals.events.trigger("connection:all_shapes", allShapesEventObject) + #@socket.on "get_whiteboard_shapes_reply", (object) => + # if @userId is object.payload?.requester_id + # #alert("I am getting some shapes reply" + JSON.stringify object) + # for shape in object.payload?.shapes + # #alert("for a shape:") + # shape_type = shape.shape_type + # globals.events.trigger("connection:whiteboard_draw_event", shape_type, shape.shape) # TODO to change the name + + # globals.events.trigger("connection:updShape", shape_type, shape.shape) # Received event to update a shape being created # @param {string} shape type of shape being updated # @param {Array} data all information to update the shape - @socket.on "whiteboard_update_event", (data) => - console.log "socket on: whiteboard_update_event" - shape = data.payload.shape_type - globals.events.trigger("connection:updShape", shape, data) - - # Received event to create a shape on the whiteboard - # @param {string} shape type of shape being made - # @param {Array} data all information to make the shape - @socket.on "whiteboard_draw_event", (data) => - console.log "socket on: whiteboard_draw_event" - shape = data.payload.shape_type + @socket.on "send_whiteboard_shape_message", (data) => + alert "send_whiteboard_shape_message" + JSON.stringify data + shape = data.payload.shape.shape_type + for point in data.payload.shape.shape.points + point = point/100 #early attempt to scale down globals.events.trigger("connection:whiteboard_draw_event", shape, data) + globals.events.trigger("connection:updShape", shape, data) # Pencil drawings are received as points from the server and painted as lines. - @socket.on "whiteboardDrawPen", (data) => - console.log "socket on: whiteboardDrawPen"+ data - globals.events.trigger("connection:whiteboardDrawPen", data) + #@socket.on "whiteboardDrawPen", (data) => + # console.log "socket on: whiteboardDrawPen"+ data + # globals.events.trigger("connection:whiteboardDrawPen", data) # Received event to update the cursor coordinates # @param {number} x x-coord of the cursor as a percentage of page width # @param {number} y y-coord of the cursor as a percentage of page height - @socket.on "mvCur", (data) => - x = data.cursor.x #TODO change to new json structure - y = data.cursor.y #TODO change to new json structure - console.log "socket on: mvCur" - globals.events.trigger("connection:mvCur", x, y) + #@socket.on "mvCur", (data) => + # x = data.cursor.x #TODO change to new json structure + # y = data.cursor.y #TODO change to new json structure + # console.log "socket on: mvCur" + # globals.events.trigger("connection:mvCur", x, y) # Received event to update the zoom or move the slide # @param {number} x x-coord of the cursor as a percentage of page width # @param {number} y y-coord of the cursor as a percentage of page height - @socket.on "move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) => - console.log "socket on: move_and_zoom" - globals.events.trigger("connection:move_and_zoom", xOffset, yOffset, widthRatio, heightRatio) + #@socket.on "move_and_zoom", (xOffset, yOffset, widthRatio, heightRatio) => + # console.log "socket on: move_and_zoom" + # globals.events.trigger("connection:move_and_zoom", xOffset, yOffset, widthRatio, heightRatio) # Received event to update the slide image # @param {string} url URL of image to show - @socket.on "changeslide", (url) => - console.log "socket on: changeslide" - globals.events.trigger("connection:changeslide", url) + #@socket.on "changeslide", (url) => + # console.log "socket on: changeslide" + # globals.events.trigger("connection:changeslide", url) # Received event to update the viewBox value # @param {string} xperc Percentage of x-offset from top left corner @@ -199,69 +201,67 @@ define [ # @param {string} wperc Percentage of full width of image to be displayed # @param {string} hperc Percentage of full height of image to be displayed # TODO: not tested yet - @socket.on "viewBox", (xperc, yperc, wperc, hperc) => - console.log "socket on: viewBox" - globals.events.trigger("connection:viewBox", xperc, yperc, wperc, hperc) + #@socket.on "viewBox", (xperc, yperc, wperc, hperc) => + # console.log "socket on: viewBox" + # globals.events.trigger("connection:viewBox", xperc, yperc, wperc, hperc) # Received event to update the zoom level of the whiteboard. # @param {number} delta amount of change in scroll wheel - @socket.on "zoom", (delta) -> - console.log "socket on: zoom" - globals.events.trigger("connection:zoom", delta) + #@socket.on "zoom", (delta) -> + # console.log "socket on: zoom" + # globals.events.trigger("connection:zoom", delta) # Received event to update the whiteboard size and position # @param {number} cx x-offset from top left corner as percentage of original width of paper # @param {number} cy y-offset from top left corner as percentage of original height of paper # @param {number} sw slide width as percentage of original width of paper # @param {number} sh slide height as a percentage of original height of paper - @socket.on "paper", (cx, cy, sw, sh) -> - console.log "socket on: paper" - globals.events.trigger("connection:paper", cx, cy, sw, sh) + #@socket.on "paper", (cx, cy, sw, sh) -> + # console.log "socket on: paper" + # globals.events.trigger("connection:paper", cx, cy, sw, sh) # Received event when the panning action finishes - @socket.on "panStop", -> - console.log "socket on: panStop" - globals.events.trigger("connection:panStop") + #@socket.on "panStop", -> + # console.log "socket on: panStop" + # globals.events.trigger("connection:panStop") # Received event to denote when the text has been created - @socket.on "textDone", -> - console.log "socket on: textDone" - globals.events.trigger("connection:textDone") + #@socket.on "textDone", -> + # console.log "socket on: textDone" + # globals.events.trigger("connection:textDone") # Received event to update the status of the upload progress # @param {string} message update message of status of upload progress # @param {boolean} fade true if you wish the message to automatically disappear after 3 seconds - @socket.on "uploadStatus", (message, fade) => - console.log "socket on: uploadStatus" - globals.events.trigger("connection:uploadStatus", message, fade) + #@socket.on "uploadStatus", (message, fade) => + # console.log "socket on: uploadStatus" + # globals.events.trigger("connection:uploadStatus", message, fade) # Received event for a user list change # @param {Array} users Array of names and publicIDs of connected users # TODO: event name with spaces is bad - @socket.on "user list change", (users) => - console.log "socket on: user list change" - globals.events.trigger("connection:user_list_change", users) + #@socket.on "user list change", (users) => + # console.log "socket on: user list change" + # globals.events.trigger("connection:user_list_change", users) # Received event for a new user - @socket.on "user_joined_event", (message) => - console.log "message: " + message - userid = message.payload.user.id + @socket.on "user_joined_message", (message) => + userid = message.payload.user.userid username = message.payload.user.name - globals.events.trigger("connection:user_join", userid, username) #should it be user_joined?! #TODO + globals.events.trigger("connection:user_join", userid, username) # Received event when a user leaves - @socket.on "user_left_event", (message) => - console.log "message: " + message - userid = message.payload.user.id + @socket.on "user_left_message", (message) -> + userid = message.payload.user.userid globals.events.trigger("connection:user_left", userid) # Received event to set the presenter to a user # @param {string} userID publicID of the user that is being set as the current presenter - @socket.on "setPresenter", (userid) => - console.log "socket on: setPresenter" - globals.events.trigger("connection:setPresenter", userid) + #@socket.on "setPresenter", (userid) => + # console.log "socket on: setPresenter" + # globals.events.trigger("connection:setPresenter", userid) # Received event to update all the messages in the chat box # @param {Array} messages Array of messages in public chat box @@ -269,10 +269,11 @@ define [ # console.log "socket on: all_messages" + allMessagesEventObject # globals.events.trigger("connection:all_messages", allMessagesEventObject) - @socket.on "share_presentation_event", (data) => - console.log "socket on: share_presentation_event" - globals.events.trigger("connection:share_presentation_event", data) - + # Change the current slide/page [if any] with the one + # contained in the message + @socket.on "presentation_page", (message) -> + console.log "socket on: presentation_page" + globals.events.trigger("connection:display_page", message) # Emit an update to move the cursor around the canvas # @param {number} x x-coord of the cursor as a percentage of page width @@ -281,16 +282,12 @@ define [ @socket.emit "mvCur", x, y # Requests the shapes from the server. - emitAllShapes: -> - @socket.emit "all_shapes" + #emitAllShapes: -> + # @socket.emit "all_shapes" - - # Emit a message to the server - # @param {string} the message + # Emit a chat message to the server + # @param {string} the chat message emitMsg: (msg) -> - - console.log "emitting message: " + msg - object = { "header": { "name": "send_public_chat_message" @@ -314,33 +311,31 @@ define [ } } } - @socket.emit "message", object - # Emit the finish of a text shape - emitTextDone: -> - @socket.emit "textDone" + #emitTextDone: -> + # @socket.emit "textDone" # Emit the creation of a shape # @param {string} shape type of shape # @param {Array} data all the data required to draw the shape on the client whiteboard - emitMakeShape: (shape, data) -> - @socket.emit "makeShape", shape, data + #emitMakeShape: (shape, data) -> + # @socket.emit "makeShape", shape, data # Emit the update of a shape # @param {string} shape type of shape # @param {Array} data all the data required to update the shape on the client whiteboard - emitUpdateShape: (shape, data) -> - @socket.emit "updShape", shape, data + #emitUpdateShape: (shape, data) -> + # @socket.emit "updShape", shape, data # Emit an update in the whiteboard position/size values # @param {number} cx x-offset from top left corner as percentage of original width of paper # @param {number} cy y-offset from top left corner as percentage of original height of paper # @param {number} sw slide width as percentage of original width of paper # @param {number} sh slide height as a percentage of original height of paper - emitPaperUpdate: (cx, cy, sw, sh) -> - @socket.emit "paper", cx, cy, sw, sh + #emitPaperUpdate: (cx, cy, sw, sh) -> + # @socket.emit "paper", cx, cy, sw, sh # Update the zoom level for the clients # @param {number} delta amount of change in scroll wheel @@ -348,43 +343,58 @@ define [ @socket.emit "zoom", delta # Request the next slide - emitNextSlide: -> - @socket.emit "nextslide" + #emitNextSlide: -> + # @socket.emit "nextslide" # Request the previous slide - emitPreviousSlide: -> - @socket.emit "prevslide" + #emitPreviousSlide: -> + # @socket.emit "prevslide" # Logout of the meeting emitLogout: -> - @socket.emit "logout" + message = { + "payload": { + "meeting_id": @meetingId + "userid": @userId + }, + "header": { + "timestamp": new Date().getTime() + "name": "user_leaving_request" + "version": "0.0.1" + } + } + @socket.emit "message", message + @socket.disconnect() + + #Utils.postToUrl "logout" + #window.location.replace "./" # Emit panning has stopped - emitPanStop: -> - @socket.emit "panStop" + #emitPanStop: -> + # @socket.emit "panStop" # Publish a shape to the server to be saved # @param {string} shape type of shape to be saved # @param {Array} data information about shape so that it can be recreated later - emitPublishShape: (shape, data) -> - @socket.emit "saveShape", shape, JSON.stringify(data) + #emitPublishShape: (shape, data) -> + # @socket.emit "saveShape", shape, JSON.stringify(data) # Emit a change in the current tool # @param {string} tool [description] - emitChangeTool: (tool) -> - @socket.emit "changeTool", tool + #emitChangeTool: (tool) -> + # @socket.emit "changeTool", tool # Tell the server to undo the last shape - emitUndo: -> - @socket.emit "undo" + #emitUndo: -> + # @socket.emit "undo" # Emit a change in the presenter - emitSetPresenter: (id) -> - @socket.emit "setPresenter", id + #emitSetPresenter: (id) -> + # @socket.emit "setPresenter", id # Emit signal to clear the canvas - emitClearCanvas: (id) -> - @socket.emit "clrPaper", id + #emitClearCanvas: (id) -> + # @socket.emit "clrPaper", id # Helper method to get the meeting_id, user_id and auth_token from the url getUrlVars: -> diff --git a/client/bbb-html5-client/public/js/models/user.coffee b/client/bbb-html5-client/public/js/models/user.coffee old mode 100644 new mode 100755 index 5f9b5718430d3df4de4975abd9fc10f8a3b85eac..cd955792ce6721effddd4377b60bd5260f8e30c1 --- a/client/bbb-html5-client/public/js/models/user.coffee +++ b/client/bbb-html5-client/public/js/models/user.coffee @@ -2,10 +2,26 @@ define [ 'underscore', 'backbone', 'globals' -], (_, Backbone, globals) -> + 'text!templates/user.html' +], (_, Backbone, globals, userTemplate) -> UserModel = Backbone.Model.extend - initialize: -> + defaults: + id : null + userid: null + username: null + + initialize: -> + #alert("iiiiiinitialize"+newUserid+" "+newUsername) + console.log "creation" + + isValid: -> + console.log "inside is valid- id: #{@id} userid: #{@userid} username: #{@username}" + value = @id? and @userid? and @username? - UserModel + render: -> + _.template(userTemplate, {userID: @userid, username: @username}) + + + UserModel \ No newline at end of file diff --git a/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee b/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee index 5c75a166041e015c98d163f632ee68cb6ce5140d..ecbf2255e67bc40f7d30de2114bc96c070922960 100755 --- a/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee +++ b/client/bbb-html5-client/public/js/models/whiteboard_paper.coffee @@ -23,6 +23,7 @@ define [ # Container must be a DOM element initialize: (@container) -> + alert("initializing the paper model") # a WhiteboardCursorModel @cursor = null @@ -152,8 +153,7 @@ define [ # @return {Raphael.image} the image object added to the whiteboard addImageToPaper: (url, width, height) -> @_updateContainerDimensions() - - alert "addImageToPaper url=#{url} \n #{width}x#{height}" + if @fitToPage # solve for the ratio of what length is going to fit more than the other max = Math.max(width / @containerWidth, height / @containerHeight) @@ -169,7 +169,7 @@ define [ originalHeight = height else # fit to width - alert "no fit" + console.log "ERROR! The slide did not fit" # assume it will fit width ways sw = width / wr sh = height / wr @@ -279,7 +279,7 @@ define [ @cursor.undrag() @currentLine = @_createTool(tool) @cursor.drag(@currentLine.dragOnMove, @currentLine.dragOnStart, @currentLine.dragOnEnd) - when "rect" + when "rectangle" @cursor.undrag() @currentRect = @_createTool(tool) @cursor.drag(@currentRect.dragOnMove, @currentRect.dragOnStart, @currentRect.dragOnEnd) @@ -352,6 +352,7 @@ define [ # Draws an array of shapes to the paper. # @param {array} shapes the array of shapes to draw drawListOfShapes: (shapes) -> + alert("drawListOfShapes" + shapes.length) @currentShapesDefinitions = shapes @currentShapes = @raphaelObj.set() for shape in shapes @@ -365,11 +366,6 @@ define [ # make sure the cursor is still on top @cursor.toFront() - #Changes the currently displayed presentation (if any) with this one - #@param {object} containing the "presentation" object -id,name,pages[] - sharePresentation: (data) -> - globals.events.trigger("connection:all_slides", data.payload) - # Clear all shapes from this paper. clearShapes: -> if @currentShapes? @@ -398,6 +394,7 @@ define [ # Make a shape `shape` with the data in `data`. makeShape: (shape, data) -> + console.log("shape=" + shape + " data=" + JSON.stringify data) tool = null switch shape when "path", "line" @@ -415,6 +412,7 @@ define [ when "triangle" @currentTriangle = @_createTool(shape) toolModel = @currentTriangle + toolModel.draw(tool, data) tool = @currentTriangle.make(data) when "text" @currentText = @_createTool(shape) @@ -423,7 +421,12 @@ define [ else console.log "shape not recognized at makeShape", shape if tool? - @currentShapes.push(tool) + alert("in currentShapes") + if @currentShapes? #rewrite TODO + @currentShapes.push(tool) + else + @currentShapes = [] + @currentShapes.push(tool) @currentShapesDefinitions.push(toolModel.getDefinition()) # Update the cursor position on screen @@ -516,47 +519,12 @@ define [ # Registers listeners for events in the gloval event bus _registerEvents: -> - - - - - - - - - - - - - - - - - - - - - globals.events.on "connection:all_slides", (data) => - @removeAllImagesFromPaper() - ### - urls = data.slides - for url in urls - @addImageToPaper(url[0], url[1], url[2]) - #alert "registerEvents url[0]=" + url[0] - ### - - urls = data.presentation.pages - for url in urls - @addImageToPaper(url.png , 200, 200) - #alert "registerEvents url[0]=" + url[0] - globals.events.trigger("whiteboard:paper:all_slides", urls) - - globals.events.on "connection:clrPaper", => @clearShapes() globals.events.on "connection:allShapes", (allShapesEventObject) => # TODO: a hackish trick for making compatible the shapes from redis with the node.js + alert("on connection:allShapes:" + JSON.stringify allShapesEventObject) shapes = allShapesEventObject.shapes for shape in shapes properties = JSON.parse(shape.data) @@ -585,8 +553,9 @@ define [ globals.events.on "connection:whiteboard_draw_event", (shape, data) => @makeShape shape, data - globals.events.on "connection:share_presentation_event", (data) => - @sharePresentation data + globals.events.on "connection:display_page", (data) => + console.log ("connection:display_page in whiteboard_paper.coffee") + @_displayPage data globals.events.on "connection:whiteboardDrawPen", (startingData) => type = startingData.payload.shape_type @@ -876,4 +845,11 @@ define [ else globals.presentationServer + url + #Changes the currently displayed page/slide (if any) with this one + #@param {data} message object containing the "presentation" object + _displayPage: (data) -> + @removeAllImagesFromPaper() + page = data.payload.currentPage + @addImageToPaper(page.png_uri, 400, 400) #the dimensions should be modified + WhiteboardPaperModel diff --git a/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee b/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee index bc27f797daa79761a3ddc1d5d9b63cdfadb35f2c..d66cc99ea68c8f4a4981600712cb595a30c03390 100644 --- a/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee +++ b/client/bbb-html5-client/public/js/models/whiteboard_rect.coffee @@ -24,16 +24,16 @@ define [ # @param {string} colour the colour of the object # @param {number} thickness the thickness of the object's line(s) make: (startingData) -> - console.log "make startingData"+ startingData - x = startingData.payload.data.coordinate.first_x - y = startingData.payload.data.coordinate.first_y - color = startingData.payload.data.line.color - thickness = startingData.payload.data.line.weight + console.log "make startingData"#+ JSON.stringify startingData + x = startingData.payload.shape.shape.points[0] + y = startingData.payload.shape.shape.points[1] + color = startingData.payload.shape.shape.color + thickness = startingData.payload.shape.shape.thickness @obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1) @obj.attr Utils.strokeAndThickness(color, thickness) @definition = - shape: "rect" + shape: "rectangle" data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]] @obj @@ -44,11 +44,11 @@ define [ # @param {number} y2 the y value of the bottom right corner # @param {boolean} square (draw a square or not) update: (startingData) -> - x1 = startingData.payload.data.coordinate.first_x - y1 = startingData.payload.data.coordinate.first_y - x2 = startingData.payload.data.coordinate.last_x - y2 = startingData.payload.data.coordinate.last_y - square = startingData.payload.data.square + x1 = startingData.payload.shape.shape.points[0] + y1 = startingData.payload.shape.shape.points[1] + x2 = startingData.payload.shape.shape.points[2] + y2 = startingData.payload.shape.shape.points[3] + square = startingData.payload.shape.shape.square if @obj? [x1, x2] = [x2, x1] if x2 < x1 [x1, x2] = [x2, x1] if x2 < x1 diff --git a/client/bbb-html5-client/public/js/views/session.coffee b/client/bbb-html5-client/public/js/views/session.coffee index de7c1fd13c05ce6b2a950377ea6969ad0ee02350..10236a0f36e9ed88e23e0dc7675b5bd3cd23bc25 100755 --- a/client/bbb-html5-client/public/js/views/session.coffee +++ b/client/bbb-html5-client/public/js/views/session.coffee @@ -8,10 +8,11 @@ define [ 'cs!views/session_navbar_hidden', 'cs!views/session_chat', 'cs!views/session_users', + 'cs!views/SingleUserView', 'cs!views/session_video' 'cs!views/session_whiteboard' ], ($, _, Backbone, globals, sessionTemplate, SessionNavbarView, SessionNavbarHiddenView, - SessionChatView, SessionUsersView, SessionVideoView, SessionWhiteboardView) -> + SessionChatView, SessionUsersView, SingleUserView, SessionVideoView, SessionWhiteboardView) -> SessionView = Backbone.View.extend tagName: 'section' @@ -23,6 +24,7 @@ define [ @navbarHiddenView = new SessionNavbarHiddenView() @navbarHiddenView.$parentEl = @$el @chatView = new SessionChatView() + @singleUserView = new SingleUserView() @usersView = new SessionUsersView() @videoView = new SessionVideoView() @whiteboardView = new SessionWhiteboardView() diff --git a/client/bbb-html5-client/public/js/views/session_chat.coffee b/client/bbb-html5-client/public/js/views/session_chat.coffee index d8a405e49c2762d4409b34bff4e0ceaafb926c9e..3971b42967b8e346aaba214d158c67bd3fb1fa57 100755 --- a/client/bbb-html5-client/public/js/views/session_chat.coffee +++ b/client/bbb-html5-client/public/js/views/session_chat.coffee @@ -63,11 +63,8 @@ define [ #TODO check if public or private message, etc... @_scrollToBottom() - globals.events.on "users:user_leave", (userid) => - @_removeUserFromChatList(userid, username) - globals.events.on "users:user_left", (userid) => - @_removeUserFromChatList(userid) #do we need username or userid is sufficient? + @_removeUserFromChatList(userid) globals.events.on "users:user_join", (userid, username) => console.log "session_chat - user_join for user:#{username}" @@ -132,9 +129,7 @@ define [ # @param userid [string] the name of the user _addUserToChatList: (userid, username) -> # only add the new element if it doesn't exist yet - console.log("_addUserToChatList ", userid, " ", username) - console.log "chat-user-#{userid}.length =" + $("#chat-user-#{userid}").length - unless $("#chat-user-#{userid}").length > 0 + if $("#chat-user-#{userid}").length is 0 data = userid: userid username: username @@ -143,8 +138,7 @@ define [ # Removes a user from the list of users in the chat # @param userid [string] the ID of the user - # @param userid [string] the name of the user - _removeUserFromChatList: (userid, username) -> + _removeUserFromChatList: (userid) -> $("#chat-user-#{userid}").remove() # When a user clicks to start a private chat with a user @@ -208,7 +202,7 @@ define [ # Adds a default welcome message to the chat _addWelcomeMessage: -> - msg = "You are now connected to the meeting '#{globals.currentAuth?.get('meetingID')}'" + msg = "You are now connected to the meeting '#{globals.meetingName}'" @_addChatMessage("System", msg) SessionChatView diff --git a/client/bbb-html5-client/public/js/views/session_navbar.coffee b/client/bbb-html5-client/public/js/views/session_navbar.coffee index 35685ec6f85793f24a198736b0d6b258dc3c0571..267c4eba99c7d089606fb4af2882337bcf0d84e5 100755 --- a/client/bbb-html5-client/public/js/views/session_navbar.coffee +++ b/client/bbb-html5-client/public/js/views/session_navbar.coffee @@ -22,6 +22,7 @@ define [ initialize: -> @$parentEl = null + @usersShown = true # Whether the user's pane is displayed, it is displayed be default render: -> compiledTemplate = _.template(sessionNavbarTemplate) @@ -41,9 +42,15 @@ define [ @$parentEl.toggleClass('chat-on') @_setToggleButtonsStatus() - # Toggle the visibility of the users panel + # Toggle the visibility of the user's pane _toggleUsers: -> - @$parentEl.toggleClass('users-on') + if @usersShown # If the user's pane is displayed, hide it and mark flag as hidden + $("#users").hide() + @usersShown=false + else # Display the pane + $("#users").show() + @usersShown=true + #@$parentEl.toggleClass('users-on') @_setToggleButtonsStatus() _toggleVideo: -> @@ -65,6 +72,7 @@ define [ _scheduleResize: (id) -> attempts = 0 before = $(id).is(':visible') + console.log "isvisible: "+before interval = setInterval( -> if $(id).is(':visible') != before or attempts > 20 attempts += 1 @@ -77,6 +85,6 @@ define [ # Log out of the session _logout: -> globals.connection.emitLogout() - globals.currentAuth = null + #globals.currentAuth = null SessionNavbarView diff --git a/client/bbb-html5-client/public/js/views/session_users.coffee b/client/bbb-html5-client/public/js/views/session_users.coffee index 10063381c949bf41638c6d030b21dfb0e4cd16c3..7209b2993d1b3bcbe19cb8499e9a5a74bd741630 100755 --- a/client/bbb-html5-client/public/js/views/session_users.coffee +++ b/client/bbb-html5-client/public/js/views/session_users.coffee @@ -13,15 +13,16 @@ define [ # manage the events in the users. SessionUsersView = Backbone.View.extend model: new UserCollection() - + events: "click #switch-presenter": "_switchPresenter" "click .user": "_userClicked" initialize: -> - @userListID = "#user-list" + userListID = "#user-list" @model.start() - + @users = null + # Bind to the event triggered when the client connects to the server if globals.connection.isConnected() @_registerEvents() @@ -30,9 +31,10 @@ define [ @_registerEvents() render: -> + # this renders to unordered list where users will be appended to compiledTemplate = _.template(sessionUsersTemplate) @$el.html compiledTemplate - + # Registers listeners for events in the event bus. # TODO: bind to backbone events in UserCollection such as 'user added', 'user removed' _registerEvents: -> @@ -40,18 +42,22 @@ define [ globals.events.on "users:user_list_change", (users) => @_removeAllUsers() for userBlock in users + console.log("on user_list_change; adding user:" + JSON.stringify userBlock) @_addUser(userBlock.id, userBlock.name) + #globals.events.on "receiveUsers", (data) => + #@users = data + globals.events.on "users:load_users", (users) => @_removeAllUsers() for userBlock in users - @_addUser(userBlock.id, userBlock.name) + #@_addUser(userBlock.userid, userBlock.name) + globals.events.trigger "user:add_new_user", {id: userBlock.userid, userid: userBlock.userid, username: userBlock.name} globals.events.on "users:user_join", (userid, username) => - @_addUser(userid, username) - - globals.events.on "users:user_leave", (userid) => - @_removeUserByID(userid) + #@_addUser(userid, username) + console.log "fffffffffffffff" + globals.events.trigger "user:add_new_user", {id: userid, userid: userid, username: username} globals.events.on "users:user_left", (userid) => @_removeUserByID(userid) @@ -67,14 +73,6 @@ define [ _removeUserByID: (userID)-> @$("#user-"+userID).remove() - # Add a user to the screen. - _addUser: (userID, username) -> - data = - username: username - userID: userID - compiledTemplate = _.template(userTemplate, data) - @$el.children("ul").append compiledTemplate - # Marks a user as selected when clicked. _userClicked: (e) -> @$('.user.selected').removeClass('selected') diff --git a/client/bbb-html5-client/public/templates/user.html b/client/bbb-html5-client/public/templates/user.html index 796c1bf1c67278d9071ba11750e232337d3a4daf..816a12d461f925e4a7b421a6195a7043e44781e3 100644 --- a/client/bbb-html5-client/public/templates/user.html +++ b/client/bbb-html5-client/public/templates/user.html @@ -1,10 +1,10 @@ -<li class="user-wrapper"> - <div class="row"> - <div class="user-role col-md-1" id="user-<%= userID %>"><i class="icon fa fa-user"></i></div> - <div class="user-name col-md-5"><%= username %></div> - <div class="user-video col-md-1"><i class="icon fa fa-video-camera"></i></div> - <div class="user-audio col-md-1"><i class="icon fa fa-microphone"></i></div> - <div class="user-kick col-md-1"><i class="icon fa fa-sign-out"></i></div> - <div class="clearfix"></div> - </div> -</li> +<li class="user-wrapper" id="user-<%= userID %>"> + <div class="row"> + <div class="user-role col-md-1"><i class="icon fa fa-user"></i></div> + <div class="user-name col-md-5"><%= username %></div> + <div class="user-video col-md-1"><i class="icon fa fa-video-camera"></i></div> + <div class="user-audio col-md-1"><i class="icon fa fa-microphone"></i></div> + <div class="user-kick col-md-1"><i class="icon fa fa-sign-out"></i></div> + <div class="clearfix"></div> + </div> +</li> \ No newline at end of file diff --git a/labs/demos/.gitignore b/labs/demos/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3c3629e647f5ddf82548912e337bea9826b434af --- /dev/null +++ b/labs/demos/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/labs/demos/Gruntfile.js b/labs/demos/Gruntfile.js new file mode 100644 index 0000000000000000000000000000000000000000..65642fc5f5f4336f704a4471c5278c76b6da5ee6 --- /dev/null +++ b/labs/demos/Gruntfile.js @@ -0,0 +1,32 @@ +/* jshint node: true */ +'use strict'; + +module.exports = function(grunt) { + // configure Grunt + grunt.initConfig({ + // files to lint with the JSHint task + jshint: { + files: { + src: [ + 'Gruntfile.js', + 'public/**/*.js' + ] + } + }, + + coffeelint: { + files: { + src: ['**/*.coffee','!node_modules/**/*'] + } + } + }); + + // load the module containing the JSHint task + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-coffeelint'); + + // register a default task to run JSHint + // (allows `grunt` rather than `grunt jshint`) + + grunt.registerTask('default', ['jshint', 'coffeelint']); +}; diff --git a/labs/demos/config.json b/labs/demos/config.json index 1ffa20f1faf214dc5a0c04c79ae13ed2fcb1ca87..47b222d291bea5f100eb4304f247a2577f9ce2a8 100644 --- a/labs/demos/config.json +++ b/labs/demos/config.json @@ -1,7 +1,7 @@ { "settings": { - "IP": "http://192.168.0.203", + "IP": "http://192.168.0.119", "PORT": "4000", - "salt": "74a91f30f165423067bf3039722e33e0" + "salt": "97d777d100aed03b60b06420a0dd9dd5" } } \ No newline at end of file diff --git a/labs/demos/index.coffee b/labs/demos/index.coffee index bb46bb63528a50b233ef47c99b94da6403b52371..51ea607f74302869c80cfd01c016d703a7c3b5fa 100644 --- a/labs/demos/index.coffee +++ b/labs/demos/index.coffee @@ -21,8 +21,9 @@ app.get("/*", (req, res, next) -> file = req.params[0] console.log "\t :: Express :: file requested : " + file - if file is "public/js/app.js" or file is "config.json" + if file is "public/js/app.js" or file is "public/stylesheets/login.css" or file is "config.json" #Send the requesting client the file. res.sendfile __dirname + "/" + file next -) \ No newline at end of file +) + diff --git a/labs/demos/lib/bbbapi.coffee b/labs/demos/lib/bbbapi.coffee index 756004a709b9cc75e4cdb63a0ede7857f7909ce6..b579a7a1988ac4da0ef4ab78d49812943f5dac29 100644 --- a/labs/demos/lib/bbbapi.coffee +++ b/labs/demos/lib/bbbapi.coffee @@ -2,10 +2,13 @@ request = require 'request' sha1 = require 'js-sha1' urlEncode = (value) -> - encodeURIComponent(value).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A") + encodeURIComponent(value) + .replace(/%20/g, '+') + .replace(/[!'()]/g, escape) + .replace(/\*/g, "%2A") sortKeys = (params) -> - keys = []; + keys = [] for own propName of params keys.push(propName) @@ -40,7 +43,7 @@ create = (params, bbb, options, callback) -> console.log(reqStr) request(reqStr, (error, response, body) -> - callback error, response, body + callback error, response, body ) join = (params, bbb, options, callback) -> @@ -54,7 +57,7 @@ join = (params, bbb, options, callback) -> console.log(reqStr) request(reqStr, (error, response, body) -> - callback error, response, body + callback error, response, body ) exports.create = create diff --git a/labs/demos/lib/handlers.coffee b/labs/demos/lib/handlers.coffee old mode 100644 new mode 100755 index f9142ce7c795733cfa3c6bd0de0ddaea5d7758c0..483fae1ca882a0d67302aaa464fb1b5768db6d63 --- a/labs/demos/lib/handlers.coffee +++ b/labs/demos/lib/handlers.coffee @@ -2,6 +2,7 @@ xml2js = require 'xml2js' bbbapi = require './bbbapi' testapi = require './testapi' +configJson = require './../config.json' index = (request, response) -> response.sendfile('./views/index.html') @@ -12,16 +13,27 @@ login = (req, resp) -> serverAndSecret = testapi.serverAndSecret #use the name from the textbox - console.log "\n\nThe Username passed was=" + JSON.stringify(req.body.name) + "\n\n" - joinParams.fullName = JSON.stringify req.body.name - joinParams.fullName = joinParams.fullName.replace(/['"]/g,'') + console.log "\n\nThe Username passed was=" + JSON.stringify(req.body.name) + + "The Meetingname passed was=" + JSON.stringify(req.body.meetingName) + "\n\n" + + # grab the username and meeting name passed in. Strip the surrounding quotes + joinParams.fullName = (JSON.stringify req.body.name)?.replace(/['"]/g,'') + passedMeetingName = (JSON.stringify req.body.meetingName)?.replace(/["]/g,'') + + # use the meeting name from the form to [create if not existing and] join + # the meeting with such name + joinParams.meetingID = passedMeetingName + createParams.name = passedMeetingName + createParams.meetingID = passedMeetingName #calling createapi - bbbapi.create(createParams, serverAndSecret, {}, (errorOuter, responseOuter, bodyOuter) -> + bbbapi.create(createParams, serverAndSecret, {}, (eo, ro, bodyOuter) -> #console.log JSON.stringify(response) + console.log "\n\nouterXML=" + ro.body + console.log "\neo=" + JSON.stringify eo bbbapi.join(joinParams, serverAndSecret, {}, (error, response, body) -> if error - console.log error + console.log error else xml = '' + response.body console.log "\n\nxml=" + xml @@ -38,8 +50,8 @@ login = (req, resp) -> "\nuser_id = " + user_id + "\nauth_token = " + auth_token - url = "http://192.168.0.203:3000/html5.client?meeting_id=" + meeting_id + "&user_id=" + - user_id + "&auth_token=" + auth_token + "&username=" + joinParams.fullName + url = "#{configJson.settings.IP}:3000/login?meeting_id=" + meeting_id + + "&user_id=" + user_id + "&auth_token=" + auth_token json = resp.json({ diff --git a/labs/demos/lib/testapi.coffee b/labs/demos/lib/testapi.coffee old mode 100644 new mode 100755 index 664f9223dfddea36c529dc55d97a8088021f3f89..5038629c35c2d258a5d62b0dfa414ecfc1ba3720 --- a/labs/demos/lib/testapi.coffee +++ b/labs/demos/lib/testapi.coffee @@ -8,26 +8,31 @@ sharedSecret = config.settings.salt console.log "will be creating a meeting on server: " + bbbServer -str = "name=Demo+Meeting&meetingID=Demo+Meeting&voiceBridge=70827&attendeePW=ap&moderatorPW=mp&record=false" +str = "name=Demo+Meeting" + + "&meetingID=Demo+Meeting" + + "&voiceBridge=70827" + + "&attendeePW=ap" + + "&moderatorPW=mp" + + "&record=false" console.log(sha1("create" + str + sharedSecret)) - +tempName = "Demo Meeting" createParams = {} createParams.attendeePW = "ap" createParams.moderatorPW = "mp" createParams.record = false createParams.voiceBridge = 70827 -createParams.name = "Demo Meeting" -createParams.meetingID = "Demo Meeting" +createParams.name = tempName +createParams.meetingID = tempName joinParams = {} -joinParams.password = "mp" +joinParams.password = "ap" joinParams.fullName = "Richard" -joinParams.meetingID = "Demo Meeting" +joinParams.meetingID = tempName joinParams.redirect = false serverAndSecret = {server: bbbServer, secret: sharedSecret} exports.createParams = createParams exports.serverAndSecret = serverAndSecret -exports.joinParams = joinParams \ No newline at end of file +exports.joinParams = joinParams diff --git a/labs/demos/package.json b/labs/demos/package.json index db388d7b07012f245266b11a5bf09014acb8da0d..1d4e81efcf41b300f5730643e647ffae74430a26 100644 --- a/labs/demos/package.json +++ b/labs/demos/package.json @@ -12,5 +12,12 @@ "xml2js": "0.4.2", "request": "2.34.0", "connect": "2.15.0" + }, + "devDependencies": { + "coffeelint": "^1.6.0", + "grunt": "^0.4.5", + "grunt-coffeelint": "0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-jshint": "^0.10.0" } } diff --git a/labs/demos/public/js/app.js b/labs/demos/public/js/app.js old mode 100644 new mode 100755 index c0b221f0b4e86671723111afc92ddb6833d3e7d4..a6e6c5458e12f027f2b1eba05b987fbd83e61827 --- a/labs/demos/public/js/app.js +++ b/labs/demos/public/js/app.js @@ -5,7 +5,8 @@ myModule.controller('MainCtrl', function($scope, $http, $location, $window) { $scope.postUsername = function() { var account = { "name": $scope.username, - "password": 'oOoOoO' + "password": 'oOoOoO', + "meetingName": $scope.meetingName == undefined ? 'Demo Meeting' : $scope.meetingName }; jQuery.getJSON("config.json", function (json) { $http.post('/login', account).success(function(res) { @@ -13,7 +14,7 @@ myModule.controller('MainCtrl', function($scope, $http, $location, $window) { $window.location.href = res.success.url; }); }); - } + }; }); diff --git a/labs/demos/public/stylesheets/login.css b/labs/demos/public/stylesheets/login.css new file mode 100644 index 0000000000000000000000000000000000000000..bcf54992b10c557cb08e0030eb18922ea2fa6ae9 --- /dev/null +++ b/labs/demos/public/stylesheets/login.css @@ -0,0 +1,91 @@ +body { + margin:0; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 1.42857143; +} +.login { } +.login-banner { + background:#f2f2f2; + height:325px; + border-bottom: 1px solid rgba(0,0,0,.05); +} +.login-form { + width:25%; + margin:0 auto; + background:#fff; + border:1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + top:45%; + padding:40px; + display:block; + position: relative; + -webkit-box-shadow: inset 0 3px 6px rgba(0,0,0,.05); + box-shadow: inset 0 3px 6px rgba(0,0,0,.05); + } +.login-form label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: 700; +} +.login-form input { + display: block; + width: 100%; + padding: 12px 18px; + font-size: 14px; + line-height: 1.42857143; + background-image: none; + border: 1px solid #c7cdd1; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; + margin-bottom: 20px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.success { + color: #FFF; + background: #54A7DB; + border:0 !important; +} +.success:hover { + background: #4695c6; + cursor: pointer; +} + + +/* Large desktop */ +@media (min-width: 1200px) { + .login-form { + width:20%; + } +} + +/* Portrait tablet to landscape and desktop */ +@media (min-width: 768px) and (max-width: 979px) { + + + +} + +/* Landscape phone to portrait tablet */ +@media (max-width: 767px) { + .login-form { + width:45%; + } + +} + +/* Landscape phones and down */ +@media (max-width: 480px) { + .login-form { + width:65%; + } +} diff --git a/labs/demos/views/index.html b/labs/demos/views/index.html old mode 100644 new mode 100755 index 014eb94071ea6fa01aeaf121e202676a511c1dca..a4345b5b121ad3aca455b9704be37366a68797c1 --- a/labs/demos/views/index.html +++ b/labs/demos/views/index.html @@ -1,20 +1,23 @@ <!doctype html> <html ng-app="landingPage"> - <head> - <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.7/angular.min.js"></script> - <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script> - <script src="../public/js/app.js"></script> - </head> - <body> - <div> - <label>Username:</label> - <input type="text" ng-model="username" placeholder="Enter a name here"> - </div> - <div ng-controller="MainCtrl"> - <form ng-submit="postUsername()"> - <input type="submit" value="Send"> <br /><br /> - </form> - </div> - </body> -</html> + <head> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.4/angular.min.js"></script> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script> + <script src="../public/js/app.js"></script> + <link rel="stylesheet" href="../public/stylesheets/login.css"></link> + </head> + <body> + <div class="login" ng-controller="MainCtrl"> + <div class="login-banner"> + <form class="login-form" ng-submit="postUsername()"> + <label>Username:</label> + <input type="text" ng-model="username" placeholder="Enter Your Name" required autofocus> + <label>Meeting name:</label> + <input type="text" ng-model="meetingName" placeholder="Demo Meeting"> + <input type="submit" value="Send" class="success"> + </form> + </div> + </div> + </body> +</html> diff --git a/labs/meteor-client/.gitignore b/labs/meteor-client/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..6101d71bac328c54bc0def6a0907756f6c0f4c44 --- /dev/null +++ b/labs/meteor-client/.gitignore @@ -0,0 +1,4 @@ +app/packages +app/build/ +npm-debug.log +node_modules/ diff --git a/labs/meteor-client/Gruntfile.js b/labs/meteor-client/Gruntfile.js new file mode 100644 index 0000000000000000000000000000000000000000..ebe5fdf986fb39290ccc8e3a791d382394f8de2c --- /dev/null +++ b/labs/meteor-client/Gruntfile.js @@ -0,0 +1,36 @@ +/* jshint node: true */ +'use strict'; + +module.exports = function(grunt) { + // configure Grunt + grunt.initConfig({ + // files to lint with the JSHint task + jshint: { + files: { + src: [ + 'Gruntfile.js' + ] + } + }, + + coffeelint: { + files: { + src: [ + '**/*.coffee', + '!node_modules/**/*', + '!app/.meteor/**/*', + '!app/packages/**/*' + ] + } + } + }); + + // load the module containing the JSHint task + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-coffeelint'); + + // register a default task to run JSHint + // (allows `grunt` rather than `grunt jshint`) + + grunt.registerTask('default', ['jshint', 'coffeelint']); +}; diff --git a/labs/meteor-client/app/.meteor/.finished-upgraders b/labs/meteor-client/app/.meteor/.finished-upgraders new file mode 100644 index 0000000000000000000000000000000000000000..68df3d8d0d0954319298b1c2b419dceebf4e4179 --- /dev/null +++ b/labs/meteor-client/app/.meteor/.finished-upgraders @@ -0,0 +1,7 @@ +# This file contains information which helps Meteor properly upgrade your +# app when you run 'meteor update'. You should check it into version control +# with your project. + +notices-for-0.9.0 +notices-for-0.9.1 +0.9.4-platform-file diff --git a/labs/meteor-client/app/.meteor/.gitignore b/labs/meteor-client/app/.meteor/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..40830374235df1c19661a2901b7ca73cc9499f3d --- /dev/null +++ b/labs/meteor-client/app/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/labs/meteor-client/app/.meteor/.id b/labs/meteor-client/app/.meteor/.id new file mode 100644 index 0000000000000000000000000000000000000000..350c3adf10c0562a65ea842e9e50fc3919d539ac --- /dev/null +++ b/labs/meteor-client/app/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +jrnkwdjvicqgy6gtl8 diff --git a/labs/meteor-client/app/.meteor/cordova-plugins b/labs/meteor-client/app/.meteor/cordova-plugins new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/labs/meteor-client/app/.meteor/cordova-plugins @@ -0,0 +1 @@ + diff --git a/labs/meteor-client/app/.meteor/packages b/labs/meteor-client/app/.meteor/packages new file mode 100644 index 0000000000000000000000000000000000000000..c63074f728fc4fd918e41e7799f833a44737669a --- /dev/null +++ b/labs/meteor-client/app/.meteor/packages @@ -0,0 +1,24 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +coffeescript +mrt:redis@0.1.3 +arunoda:npm@0.2.6 +underscore +amplify +iron:router +blaze +less +sanjo:jasmine +francocatena:status +mizzao:jquery-ui +mrt:external-file-loader@0.1.4 +brentjanderson:winston-client@0.0.4 +mizzao:bootstrap-3 +duongthienduc:meteor-winston +mizzao:timesync +agnito:raphael + diff --git a/labs/meteor-client/app/.meteor/platforms b/labs/meteor-client/app/.meteor/platforms new file mode 100644 index 0000000000000000000000000000000000000000..efeba1b50c759324cbdaafd7da6c31b030bf9600 --- /dev/null +++ b/labs/meteor-client/app/.meteor/platforms @@ -0,0 +1,2 @@ +server +browser diff --git a/labs/meteor-client/app/.meteor/release b/labs/meteor-client/app/.meteor/release new file mode 100644 index 0000000000000000000000000000000000000000..fdc65835058566d8a94f89944c26eea03db1c4a6 --- /dev/null +++ b/labs/meteor-client/app/.meteor/release @@ -0,0 +1 @@ +METEOR@1.0.2.1 diff --git a/labs/meteor-client/app/.meteor/versions b/labs/meteor-client/app/.meteor/versions new file mode 100644 index 0000000000000000000000000000000000000000..4485a21952db63c574b00e747571efa2405573be --- /dev/null +++ b/labs/meteor-client/app/.meteor/versions @@ -0,0 +1,84 @@ +agnito:raphael@0.1.0 +alanning:package-stubber@0.0.9 +amplify@1.0.0 +application-configuration@1.0.4 +arunoda:npm@0.2.6 +autoupdate@1.1.4 +base64@1.0.2 +binary-heap@1.0.2 +blaze@2.0.4 +blaze-tools@1.0.2 +boilerplate-generator@1.0.2 +brentjanderson:winston-client@0.2.0 +callback-hook@1.0.2 +check@1.0.3 +coffeescript@1.0.5 +ddp@1.0.13 +deps@1.0.6 +duongthienduc:meteor-winston@1.0.0 +ejson@1.0.5 +fastclick@1.0.2 +follower-livedata@1.0.3 +francocatena:status@1.0.3 +geojson-utils@1.0.2 +html-tools@1.0.3 +htmljs@1.0.3 +http@1.0.9 +id-map@1.0.2 +infinitedg:winston@0.7.3 +iron:controller@1.0.6 +iron:core@1.0.6 +iron:dynamic-template@1.0.6 +iron:layout@1.0.6 +iron:location@1.0.6 +iron:middleware-stack@1.0.6 +iron:router@1.0.6 +iron:url@1.0.6 +jquery@1.0.2 +json@1.0.2 +launch-screen@1.0.1 +less@1.0.12 +livedata@1.0.12 +logging@1.0.6 +meteor@1.1.4 +meteor-platform@1.2.1 +minifiers@1.1.3 +minimongo@1.0.6 +mizzao:bootstrap-3@3.3.1_1 +mizzao:build-fetcher@0.2.0 +mizzao:jquery-ui@1.11.2 +mizzao:timesync@0.2.2 +mobile-status-bar@1.0.2 +mongo@1.0.11 +mrt:external-file-loader@0.1.4 +mrt:redis@0.1.3 +observe-sequence@1.0.4 +ordered-dict@1.0.2 +practicalmeteor:chai@1.9.2_3 +practicalmeteor:loglevel@1.1.0_3 +random@1.0.2 +reactive-dict@1.0.5 +reactive-var@1.0.4 +reload@1.1.2 +retry@1.0.2 +routepolicy@1.0.3 +sanjo:jasmine@0.9.3 +sanjo:karma@1.1.3 +session@1.0.5 +spacebars@1.0.4 +spacebars-compiler@1.0.4 +standard-app-packages@1.0.4 +tap:http-methods@0.0.23 +tap:i18n@1.0.7 +templating@1.0.10 +tracker@1.0.4 +ui@1.0.5 +underscore@1.0.2 +url@1.0.3 +velocity:core@0.4.5 +velocity:meteor-stubs@1.0.0_2 +velocity:node-soft-mirror@0.2.8 +velocity:shim@0.1.0 +velocity:test-proxy@0.0.4 +webapp@1.1.5 +webapp-hashing@1.0.2 diff --git a/labs/meteor-client/app/client/globals.coffee b/labs/meteor-client/app/client/globals.coffee new file mode 100755 index 0000000000000000000000000000000000000000..123487c77c92b611133d8a4448831152065bff2e --- /dev/null +++ b/labs/meteor-client/app/client/globals.coffee @@ -0,0 +1,323 @@ +@getBuildInformation = -> + appName = Meteor.config?.appName or "UNKNOWN NAME" + copyrightYear = Meteor.config?.copyrightYear or "UNKNOWN DATE" + dateOfBuild = Meteor.config?.dateOfBuild or "UNKNOWN DATE" + defaultWelcomeMessage = Meteor.config?.defaultWelcomeMessage or "UNKNOWN" + defaultWelcomeMessageFooter = Meteor.config?.defaultWelcomeMessageFooter or "UNKNOWN" + link = "<a href='http://bigbluebutton.org/' target='_blank'>http://bigbluebutton.org</a>" + bbbServerVersion = Meteor.config?.bbbServerVersion or "UNKNOWN VERSION" + + { + 'appName': appName + 'copyrightYear': copyrightYear + 'dateOfBuild': dateOfBuild + 'defaultWelcomeMessage': defaultWelcomeMessage + 'defaultWelcomeMessageFooter': defaultWelcomeMessageFooter + 'link': link + 'bbbServerVersion': bbbServerVersion + } + +# Convert a color `value` as integer to a hex color (e.g. 255 to #0000ff) +@colourToHex = (value) -> + hex = parseInt(value).toString(16) + hex = "0" + hex while hex.length < 6 + "##{hex}" + +# color can be a number (a hex converted to int) or a string (e.g. "#ffff00") +@formatColor = (color) -> + color ?= "0" # default value + if !color.toString().match(/\#.*/) + color = colourToHex(color) + color + +# thickness can be a number (e.g. "2") or a string (e.g. "2px") +@formatThickness = (thickness) -> + thickness ?= "1" # default value + if !thickness.toString().match(/.*px$/) + "#" + thickness + "px" # leading "#" - to be compatible with Firefox + thickness + +@getCurrentSlideDoc = -> # returns only one document + currentPresentation = Meteor.Presentations.findOne({"presentation.current": true}) + presentationId = currentPresentation?.presentation?.id + currentSlide = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true}) + +# retrieve account for selected user +@getCurrentUserFromSession = -> + Meteor.Users.findOne(userId: getInSession("userId")) + +@getInSession = (k) -> SessionAmplify.get k + +@getMeetingName = -> + return Meteor.Meetings.findOne()?.meetingName or "your meeting" + +@getTime = -> # returns epoch in ms + (new Date).valueOf() + +@getTimeOfJoining = -> + Meteor.Users.findOne(userId: getInSession "userId")?.user?.time_of_joining + +@getPresentationFilename = -> + currentPresentation = Meteor.Presentations.findOne({"presentation.current": true}) + currentPresentation?.presentation?.name + +Handlebars.registerHelper "colourToHex", (value) => + @window.colourToHex(value) + +Handlebars.registerHelper 'equals', (a, b) -> # equals operator was dropped in Meteor's migration from Handlebars to Spacebars + a is b + +Handlebars.registerHelper "getCurrentMeeting", -> + Meteor.Meetings.findOne() + +Handlebars.registerHelper "getCurrentSlide", -> + currentPresentation = Meteor.Presentations.findOne({"presentation.current": true}) + presentationId = currentPresentation?.presentation?.id + Meteor.Slides.find({"presentationId": presentationId, "slide.current": true}) + +# retrieve account for selected user +Handlebars.registerHelper "getCurrentUser", => + @window.getCurrentUserFromSession() + +# Allow access through all templates +Handlebars.registerHelper "getInSession", (k) -> SessionAmplify.get k + +Handlebars.registerHelper "getMeetingName", -> + return Meteor.Meetings.findOne()?.meetingName or "BigBlueButton" + +Handlebars.registerHelper "getShapesForSlide", -> + currentSlide = getCurrentSlideDoc() + + # try to reuse the lines above + Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}) + +# retrieves all users in the meeting +Handlebars.registerHelper "getUsersInMeeting", -> + # Users with raised hand last go first, then sorted by name + Meteor.Users.find({}, {sort: {'user.raise_hand': -1, 'user._sort_name': 1} }) + +Handlebars.registerHelper "getWhiteboardTitle", -> + "Whiteboard: " + (getPresentationFilename() or "Loading...") + +Handlebars.registerHelper "isCurrentUser", (userId) -> + userId is BBB.getCurrentUser()?.userId + +Handlebars.registerHelper "isCurrentUserMuted", -> + BBB.amIMuted() + +Handlebars.registerHelper "isCurrentUserRaisingHand", -> + user = BBB.getCurrentUser() + user?.user?.raise_hand + +Handlebars.registerHelper "isCurrentUserSharingAudio", -> + BBB.amISharingAudio() + +Handlebars.registerHelper "isCurrentUserSharingVideo", -> + BBB.amISharingVideo() + +Handlebars.registerHelper "isCurrentUserTalking", -> + BBB.amITalking() + +Handlebars.registerHelper "isDisconnected", -> + return !Meteor.status().connected + +Handlebars.registerHelper "isUserListenOnly", (userId) -> + user = Meteor.Users.findOne({userId:userId}) + return user?.user?.listenOnly + +Handlebars.registerHelper "isUserMuted", (userId) -> + BBB.isUserMuted(userId) + +Handlebars.registerHelper "isUserSharingAudio", (userId) -> + BBB.isUserSharingAudio(userId) + +Handlebars.registerHelper "isUserSharingVideo", (userId) -> + BBB.isUserSharingWebcam(userId) + +Handlebars.registerHelper "isUserTalking", (userId) -> + BBB.isUserTalking(userId) + +Handlebars.registerHelper "meetingIsRecording", -> + Meteor.Meetings.findOne()?.recorded # Should only ever have one meeting, so we dont need any filter and can trust result #1 + +Handlebars.registerHelper "messageFontSize", -> + style: "font-size: #{getInSession("messageFontSize")}px;" + +Handlebars.registerHelper "pointerLocation", -> + currentPresentation = Meteor.Presentations.findOne({"presentation.current": true}) + presentationId = currentPresentation?.presentation?.id + currentSlideDoc = Meteor.Slides.findOne({"presentationId": presentationId, "slide.current": true}) + pointer = currentPresentation?.pointer + pointer.x = (- currentSlideDoc.slide.x_offset * 2 + currentSlideDoc.slide.width_ratio * pointer.x) / 100 + pointer.y = (- currentSlideDoc.slide.y_offset * 2 + currentSlideDoc.slide.height_ratio * pointer.y) / 100 + pointer + +Handlebars.registerHelper "safeName", (str) -> + safeString(str) + +Handlebars.registerHelper "visibility", (section) -> + if getInSession "display_#{section}" + style: 'display:block;' + else + style: 'display:none;' + +# transform plain text links into HTML tags compatible with Flash client +@linkify = (str) -> + www = /(^|[^\/])(www\.[\S]+($|\b))/img + http = /\b(https?:\/\/[0-9a-z+|.,:;\/&?_~%#=@!-]*[0-9a-z+|\/&_~%#=@-])/img + str = str.replace http, "<a href='event:$1'><u>$1</u></a>" + str = str.replace www, "$1<a href='event:http://$2'><u>$2</u></a>" + +# check the chat history of the user and add tabs for the private chats +@populateChatTabs = (msg) -> + myUserId = getInSession "userId" + users = Meteor.Users.find().fetch() + + # assuming that I only have access only to private messages where I am the sender or the recipient + myPrivateChats = Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT'}).fetch() + + uniqueArray = [] + for chat in myPrivateChats + if chat.message.to_userid is myUserId + uniqueArray.push({userId: chat.message.from_userid, username: chat.message.from_username}) + if chat.message.from_userid is myUserId + uniqueArray.push({userId: chat.message.to_userid, username: chat.message.to_username}) + + #keep unique entries only + uniqueArray = uniqueArray.filter((itm, i, a) -> + i is a.indexOf(itm) + ) + + if msg.message.to_userid is myUserId + new_msg_userid = msg.message.from_userid + if msg.message.from_userid is myUserId + new_msg_userid = msg.message.to_userid + + #insert the unique entries in the collection + for u in uniqueArray + tabs = getInSession('chatTabs') + if tabs.filter((tab) -> tab.userId == u.userId).length is 0 and u.userId is new_msg_userid + tabs.push {userId: u.userId, name: u.username, gotMail: false, class: 'privateChatTab'} + setInSession 'chatTabs', tabs + +@setInSession = (k, v) -> SessionAmplify.set k, v + +@safeString = (str) -> + if typeof str is 'string' + str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); + +@toggleCam = (event) -> + # Meteor.Users.update {_id: context._id} , {$set:{"user.sharingVideo": !context.sharingVideo}} + # Meteor.call('userToggleCam', context._id, !context.sharingVideo) + +@toggleChatbar = -> + setInSession "display_chatbar", !getInSession "display_chatbar" + setTimeout(redrawWhiteboard, 0) + +@toggleMic = (event) -> + u = Meteor.Users.findOne({userId:getInSession("userId")}) + if u? + Meteor.call('muteUser', getInSession("meetingId"), u.userId, getInSession("userId"), getInSession("authToken"), not u.user.voiceUser.muted) + +@toggleNavbar = -> + setInSession "display_navbar", !getInSession "display_navbar" + +# toggle state of session variable +@toggleUsersList = -> + setInSession "display_usersList", !getInSession "display_usersList" + setTimeout(redrawWhiteboard, 0) + +@toggleVoiceCall = (event) -> + if BBB.amISharingAudio() + hangupCallback = -> + console.log "left voice conference" + BBB.leaveVoiceConference hangupCallback #TODO should we apply role permissions to this action? + else + # create voice call params + joinCallback = (message) -> + console.log "started webrtc_call" + BBB.joinVoiceConference joinCallback # make the call #TODO should we apply role permissions to this action? + return false + +@toggleWhiteBoard = -> + setInSession "display_whiteboard", !getInSession "display_whiteboard" + setTimeout(redrawWhiteboard, 0) + +@toggleSlidingMenu = -> + if $('#sliding-menu').hasClass('sliding-menu-opened') + setInSession 'display_slidingMenu', false + $('#sliding-menu').removeClass('sliding-menu-opened') + $('#darkened-screen').css('display', 'none') + $(document).unbind('scroll') + else + setInSession 'display_slidingMenu', true + $('#sliding-menu').addClass('sliding-menu-opened') + $('#darkened-screen').css('display', 'block') + $(document).bind 'scroll', () -> + window.scrollTo(0, 0) + +# Starts the entire logout procedure. +# meeting: the meeting the user is in +# the user's userId +@userLogout = (meeting, user) -> + Meteor.call("userLogout", meeting, user, getInSession("authToken")) + console.log "logging out #{Meteor.config.app.logOutUrl}" + document.location = Meteor.config.app.logOutUrl # navigate to logout + +# Clear the local user session +@clearSessionVar = (callback) -> + delete SessionAmplify.keys['authToken'] + delete SessionAmplify.keys['bbbServerVersion'] + delete SessionAmplify.keys['chatTabs'] + delete SessionAmplify.keys['dateOfBuild'] + delete SessionAmplify.keys['display_chatPane'] + delete SessionAmplify.keys['display_chatbar'] + delete SessionAmplify.keys['display_navbar'] + delete SessionAmplify.keys['display_usersList'] + delete SessionAmplify.keys['display_whiteboard'] + delete SessionAmplify.keys['inChatWith'] + delete SessionAmplify.keys['meetingId'] + delete SessionAmplify.keys['messageFontSize'] + delete SessionAmplify.keys['tabsRenderedTime'] + delete SessionAmplify.keys['userId'] + delete SessionAmplify.keys['userName'] + callback() + +# assign the default values for the Session vars +@setDefaultSettings = -> + console.log "in setDefaultSettings" + setInSession "display_usersList", true + setInSession "display_navbar", true + setInSession "display_chatbar", true + setInSession "display_whiteboard", true + setInSession "display_chatPane", true + setInSession "inChatWith", 'PUBLIC_CHAT' + setInSession "messageFontSize", 12 + setInSession 'display_slidingMenu', false + + +@onLoadComplete = -> + setDefaultSettings() + + Meteor.Users.find().observe({ + removed: (oldDocument) -> + if oldDocument.userId is getInSession 'userId' + document.location = Meteor.config.app.logOutUrl + }) + +# applies zooming to the stroke thickness +@zoomStroke = (thickness) -> + currentSlide = @getCurrentSlideDoc() + ratio = (currentSlide?.slide.width_ratio + currentSlide?.slide.height_ratio) / 2 + thickness * 100 / ratio + +# TODO TEMPORARY!! +# must not have this in production +@whoami = -> + console.log JSON.stringify + username: getInSession "userName" + userid: getInSession "userId" + authToken: getInSession "authToken" + +@listSessionVars = -> + console.log SessionAmplify.keys diff --git a/labs/meteor-client/app/client/lib/.gitignore b/labs/meteor-client/app/client/lib/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..553695ee28337e39a499abf935f5775ec60d677e --- /dev/null +++ b/labs/meteor-client/app/client/lib/.gitignore @@ -0,0 +1,2 @@ +custom.bootstrap.mixins.import.less +custom.bootstrap.less \ No newline at end of file diff --git a/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee b/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee new file mode 100644 index 0000000000000000000000000000000000000000..18747f6a8267d0616e203b78aaf545b8c7de3928 --- /dev/null +++ b/labs/meteor-client/app/client/lib/bbb_api_bridge.coffee @@ -0,0 +1,355 @@ +### +This file contains the BigBlueButton client APIs that will allow 3rd-party applications +to embed the HTML5 client and interact with it through Javascript. + +HOW TO USE: +Some APIs allow synchronous and asynchronous calls. When using asynchronous, the 3rd-party +JS should register as listener for events listed at the bottom of this file. For synchronous, +3rd-party JS should pass in a callback function when calling the API. + +For an example on how to use these APIs, see: + +https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/lib/3rd-party.js +https://github.com/bigbluebutton/bigbluebutton/blob/master/bigbluebutton-client/resources/prod/3rd-party.html +### + +@BBB = (-> + + BBB = {} + + returnOrCallback = (res, callback) -> + if callback? and typeof callback is "function" + callback res + else + res + + ### + Queryies the user object via it's id + ### + BBB.getUser = (userId) -> + Meteor.Users.findOne({userId: userId}) + + BBB.getCurrentUser = () -> + BBB.getUser(getInSession("userId")) + + ### + Query if the current user is sharing webcam. + + Param: + callback - function to return the result + + If you want to instead receive an event with the result, register a listener + for AM_I_SHARING_CAM_RESP (see below). + ### + BBB.amISharingWebcam = (callback) -> + BBB.isUserSharingWebcam BBB.getCurrentUser()?.userId + + ### + + Query if another user is sharing her camera. + + Param: + userID : the id of the user that may be sharing the camera + callback: function if you want to be informed synchronously. Don't pass a function + if you want to be informed through an event. You have to register for + IS_USER_PUBLISHING_CAM_RESP (see below). + ### + BBB.isUserSharingWebcam = (userId, callback) -> + BBB.getUser(userId)?.user?.webcam_stream?.length isnt 0 + + BBB.amITalking = (callback) -> + BBB.isUserTalking BBB.getCurrentUser()?.userId + + BBB.isUserTalking = (userId, callback) -> + BBB.getUser(userId)?.user?.voiceUser?.talking + + BBB.amISharingAudio = (callback) -> + BBB.isUserSharingAudio BBB.getCurrentUser()?.userId + + BBB.isUserSharingAudio = (userId) -> + BBB.getUser(userId)?.user?.voiceUser?.joined + + ### + Raise user's hand. + + Param: + raiseHand - [true/false] + ### + BBB.raiseHand = (raiseHand) -> + + ### + Issue a switch presenter command. + + Param: + newPresenterUserID - the user id of the new presenter + + 3rd-party JS must listen for SWITCHED_PRESENTER (see below) to get notified + of switch presenter events. + ### + BBB.switchPresenter = (newPresenterUserID) -> + + ### + Query if current user is presenter. + + Params: + callback - function if you want a callback as response. Otherwise, you need to listen + for AM_I_PRESENTER_RESP (see below). + ### + BBB.amIPresenter = (callback) -> + returnOrCallback false, callback + + ### + Eject a user. + + Params: + userID - userID of the user you want to eject. + ### + BBB.ejectUser = (userID) -> + + ### + Query who is presenter. + + Params: + callback - function that gets executed for the response. + ### + BBB.getPresenterUserID = (callback) -> + + ### + Query the current user's role. + Params: + callback - function if you want a callback as response. Otherwise, you need to listen + for GET_MY_ROLE_RESP (see below). + ### + BBB.getMyRole = (callback) -> + returnOrCallback "VIEWER", callback + + ### + Query the current user's id. + + Params: + callback - function that gets executed for the response. + ### + BBB.getMyUserID = (callback) -> + returnOrCallback getInSession("userId"), callback + + + BBB.getMyDBID = (callback) -> + returnOrCallback Meteor.Users.findOne({userId:getInSession("userId")})?._id, callback + + + BBB.getMyUserName = (callback) -> + name = getInSession "userName" # check if we actually have one in the session + + if name? + name # great return it, no database query + else # we need it from the database + user = BBB.getCurrentUser() + + if user? + name = BBB.getUserName(user.userId) + setInSession "userName", name # store in session for fast access next time + name + + BBB.getMyVoiceBridge = (callback) -> + res = Meteor.Meetings.findOne({}).voiceConf + returnOrCallback res, callback + + BBB.getUserName = (userId, callback) -> + returnOrCallback BBB.getUser(userId)?.user?.name, callback + + ### + Query the current user's role. + Params: + callback - function if you want a callback as response. Otherwise, you need to listen + for GET_MY_ROLE_RESP (see below). + ### + BBB.getMyUserInfo = (callback) -> + result = + myUserID: BBB.getMyUserID() + myUsername: BBB.getMyUserName() + myAvatarURL: null + myRole: BBB.getMyRole() + amIPresenter: BBB.amIPresenter() + voiceBridge: BBB.getMyVoiceBridge() + dialNumber: null + + returnOrCallback(result, callback) + + ### + Query the meeting id. + + Params: + callback - function that gets executed for the response. + ### + BBB.getMeetingID = (callback) -> + + BBB.getInternalMeetingID = (callback) -> + + ### + Join the voice conference. + ### + BBB.joinVoiceConference = (callback) -> + callIntoConference(BBB.getMyVoiceBridge(), callback) + + ### + Leave the voice conference. + ### + BBB.leaveVoiceConference = (callback) -> + webrtc_hangup callback # sign out of call + + ### + Share user's webcam. + + Params: + publishInClient : (DO NOT USE - Unimplemented) + ### + BBB.shareVideoCamera = (publishInClient) -> + + ### + Stop share user's webcam. + ### + BBB.stopSharingCamera = -> + + ### + Indicates if a user is muted + ### + BBB.isUserMuted = (id) -> + BBB.getUser(id)?.user?.voiceUser?.muted + + ### + Indicates if the current user is muted + ### + BBB.amIMuted = -> + BBB.isUserMuted(BBB.getCurrentUser().userId) + + ### + Mute the current user. + ### + BBB.muteMe = -> + + ### + Unmute the current user. + ### + BBB.unmuteMe = -> + + ### + Mute all the users. + ### + BBB.muteAll = -> + + ### + Unmute all the users. + ### + BBB.unmuteAll = -> + + ### + Switch to a new layout. + + Param: + newLayout : name of the layout as defined in layout.xml (found in /var/www/bigbluebutton/client/conf/layout.xml) + ### + BBB.switchLayout = (newLayout) -> + + ### + Lock the layout. + + Locking the layout means that users will have the same layout with the moderator that issued the lock command. + Other users won't be able to move or resize the different windows. + ### + BBB.lockLayout = (lock) -> + + ### + Request to send a public chat + fromUserID - the external user id for the sender + fontColor - the color of the font to display the message + localeLang - the 2-char locale code (e.g. en) for the sender + message - the message to send + ### + BBB.sendPublicChatMessage = (fontColor, localeLang, message) -> + + ### + Request to send a private chat + fromUserID - the external user id for the sender + fontColor - the color of the font to display the message + localeLang - the 2-char locale code (e.g. en) for the sender + message - the message to send + toUserID - the external user id of the receiver + ### + BBB.sendPrivateChatMessage = (fontColor, localeLang, message, toUserID) -> + + ### + Request to display a presentation. + presentationID - the presentation to display + ### + BBB.displayPresentation = (presentationID) -> + + ### + Query the list of uploaded presentations. + ### + BBB.queryListOfPresentations = -> + + ### + Request to delete a presentation. + presentationID - the presentation to delete + ### + BBB.deletePresentation = (presentationID) -> + + BBB.webRTCConferenceCallStarted = -> + + BBB.webRTCConferenceCallConnecting = -> + + BBB.webRTCConferenceCallEnded = -> + + BBB.webRTCConferenceCallFailed = (errorcode) -> + + BBB.webRTCConferenceCallWaitingForICE = -> + + BBB.webRTCCallProgressCallback = (progress) -> + + BBB.webRTCEchoTestStarted = -> + + BBB.webRTCEchoTestConnecting = -> + + BBB.webRTCEchoTestFailed = (reason) -> + + BBB.webRTCEchoTestWaitingForICE = -> + + BBB.webRTCEchoTestEnded = -> + + BBB.webRTCMediaRequest = -> + + BBB.webRTCMediaSuccess = -> + + BBB.webRTCMediaFail = -> + + BBB.webRTCWebcamRequest = -> + + BBB.webRTCWebcamRequestSuccess = -> + + BBB.webRTCWebcamRequestFail = (reason) -> + + # Third-party JS apps should use this to query if the BBB SWF file is ready to handle calls. + + # *********************************************************************************** + # * Broadcasting of events to 3rd-party apps. + # ************************************************************************************ + + ### + Stores the 3rd-party app event listeners ** + ### + listeners = {} + + ### + 3rd-party apps should user this method to register to listen for events. + ### + BBB.listen = (eventName, handler) -> + + ### + 3rd-party app should use this method to unregister listener for a given event. + ### + BBB.unlisten = (eventName, handler) -> + + BBB.init = (callback) -> + + BBB +)() \ No newline at end of file diff --git a/labs/meteor-client/app/client/lib/custom.bootstrap.import.less b/labs/meteor-client/app/client/lib/custom.bootstrap.import.less new file mode 100644 index 0000000000000000000000000000000000000000..0539c790a0c5bba6d12fb826726aaf9f38677ac6 --- /dev/null +++ b/labs/meteor-client/app/client/lib/custom.bootstrap.import.less @@ -0,0 +1,855 @@ +// This File is for you to modify! +// It won't be overwritten as long as it exists. +// You may include this file into your less files to benefit from +// mixins and variables that bootstrap provides. + +@import "custom.bootstrap.mixins.import.less"; + + +// @import "bootstrap/less/variables.less" +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-darker: lighten(#000, 13.5%); // #222 +@gray-dark: lighten(#000, 20%); // #333 +@gray: lighten(#000, 33.5%); // #555 +@gray-light: lighten(#000, 46.7%); // #777 +@gray-lighter: lighten(#000, 93.5%); // #eee + +@brand-primary: #428bca; +@brand-success: #5cb85c; +@brand-info: #5bc0de; +@brand-warning: #f0ad4e; +@brand-danger: #d9534f; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for `<body>`. +@body-bg: #fff; +//** Global text color on `<body>`. +@text-color: @gray-dark; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. +@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; +@font-family-base: @font-family-sans-serif; + +@font-size-base: 14px; +@font-size-large: ceil((@font-size-base * 1.25)); // ~18px +@font-size-small: ceil((@font-size-base * 0.85)); // ~12px + +@font-size-h1: floor((@font-size-base * 2.6)); // ~36px +@font-size-h2: floor((@font-size-base * 2.15)); // ~30px +@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px +@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px +@font-size-h5: @font-size-base; +@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px + +//** Unit-less `line-height` for use in components like buttons. +@line-height-base: 1.428571429; // 20/14 +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. +@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px + +//** By default, this inherits from the `<body>`. +@headings-font-family: inherit; +@headings-font-weight: 500; +@headings-line-height: 1.1; +@headings-color: inherit; + + +//== Iconography +// +//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower. + +//** Load fonts from this directory. +@icon-font-path: "../fonts/"; +//** File name for all font files. +@icon-font-name: "glyphicons-halflings-regular"; +//** Element ID within SVG icon file. +@icon-font-svg-id: "glyphicons_halflingsregular"; + + +//== Components +// +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). + +@padding-base-vertical: 6px; +@padding-base-horizontal: 12px; + +@padding-large-vertical: 10px; +@padding-large-horizontal: 16px; + +@padding-small-vertical: 5px; +@padding-small-horizontal: 10px; + +@padding-xs-vertical: 1px; +@padding-xs-horizontal: 5px; + +@line-height-large: 1.33; +@line-height-small: 1.5; + +@border-radius-base: 4px; +@border-radius-large: 6px; +@border-radius-small: 3px; + +//** Global color for active items (e.g., navs or dropdowns). +@component-active-color: #fff; +//** Global background color for active items (e.g., navs or dropdowns). +@component-active-bg: @brand-primary; + +//** Width of the `border` for generating carets that indicator dropdowns. +@caret-width-base: 4px; +//** Carets increase slightly in size for larger components. +@caret-width-large: 5px; + + +//== Tables +// +//## Customizes the `.table` component with basic values, each used across all table variations. + +//** Padding for `<th>`s and `<td>`s. +@table-cell-padding: 8px; +//** Padding for cells in `.table-condensed`. +@table-condensed-cell-padding: 5px; + +//** Default background color used for all tables. +@table-bg: transparent; +//** Background color used for `.table-striped`. +@table-bg-accent: #f9f9f9; +//** Background color used for `.table-hover`. +@table-bg-hover: #f5f5f5; +@table-bg-active: @table-bg-hover; + +//** Border color for table and cell borders. +@table-border-color: #ddd; + + +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. + +@btn-font-weight: normal; + +@btn-default-color: #333; +@btn-default-bg: #fff; +@btn-default-border: #ccc; + +@btn-primary-color: #fff; +@btn-primary-bg: @brand-primary; +@btn-primary-border: darken(@btn-primary-bg, 5%); + +@btn-success-color: #fff; +@btn-success-bg: @brand-success; +@btn-success-border: darken(@btn-success-bg, 5%); + +@btn-info-color: #fff; +@btn-info-bg: @brand-info; +@btn-info-border: darken(@btn-info-bg, 5%); + +@btn-warning-color: #fff; +@btn-warning-bg: @brand-warning; +@btn-warning-border: darken(@btn-warning-bg, 5%); + +@btn-danger-color: #fff; +@btn-danger-bg: @brand-danger; +@btn-danger-border: darken(@btn-danger-bg, 5%); + +@btn-link-disabled-color: @gray-light; + + +//== Forms +// +//## + +//** `<input>` background color +@input-bg: #fff; +//** `<input disabled>` background color +@input-bg-disabled: @gray-lighter; + +//** Text color for `<input>`s +@input-color: @gray; +//** `<input>` border color +@input-border: #ccc; +//** `<input>` border radius +@input-border-radius: @border-radius-base; +//** Border color for inputs on focus +@input-border-focus: #66afe9; + +//** Placeholder text color +@input-color-placeholder: @gray-light; + +//** Default `.form-control` height +@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); +//** Large `.form-control` height +@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); +//** Small `.form-control` height +@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); + +@legend-color: @gray-dark; +@legend-border-color: #e5e5e5; + +//** Background color for textual input addons +@input-group-addon-bg: @gray-lighter; +//** Border color for textual input addons +@input-group-addon-border-color: @input-border; + + +//== Dropdowns +// +//## Dropdown menu container and contents. + +//** Background for the dropdown menu. +@dropdown-bg: #fff; +//** Dropdown menu `border-color`. +@dropdown-border: rgba(0,0,0,.15); +//** Dropdown menu `border-color` **for IE8**. +@dropdown-fallback-border: #ccc; +//** Divider color for between dropdown items. +@dropdown-divider-bg: #e5e5e5; + +//** Dropdown link text color. +@dropdown-link-color: @gray-dark; +//** Hover color for dropdown links. +@dropdown-link-hover-color: darken(@gray-dark, 5%); +//** Hover background for dropdown links. +@dropdown-link-hover-bg: #f5f5f5; + +//** Active dropdown menu item text color. +@dropdown-link-active-color: @component-active-color; +//** Active dropdown menu item background color. +@dropdown-link-active-bg: @component-active-bg; + +//** Disabled dropdown menu item background color. +@dropdown-link-disabled-color: @gray-light; + +//** Text color for headers within dropdown menus. +@dropdown-header-color: @gray-light; + +//** Deprecated `@dropdown-caret-color` as of v3.1.0 +@dropdown-caret-color: #000; + + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +@zindex-navbar: 1000; +@zindex-dropdown: 1000; +@zindex-popover: 1060; +@zindex-tooltip: 1070; +@zindex-navbar-fixed: 1030; +@zindex-modal-background: 1040; +@zindex-modal: 1050; + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +//** Deprecated `@screen-xs` as of v3.0.1 +@screen-xs: 480px; +//** Deprecated `@screen-xs-min` as of v3.2.0 +@screen-xs-min: @screen-xs; +//** Deprecated `@screen-phone` as of v3.0.1 +@screen-phone: @screen-xs-min; + +// Small screen / tablet +//** Deprecated `@screen-sm` as of v3.0.1 +@screen-sm: 768px; +@screen-sm-min: @screen-sm; +//** Deprecated `@screen-tablet` as of v3.0.1 +@screen-tablet: @screen-sm-min; + +// Medium screen / desktop +//** Deprecated `@screen-md` as of v3.0.1 +@screen-md: 992px; +@screen-md-min: @screen-md; +//** Deprecated `@screen-desktop` as of v3.0.1 +@screen-desktop: @screen-md-min; + +// Large screen / wide desktop +//** Deprecated `@screen-lg` as of v3.0.1 +@screen-lg: 1200px; +@screen-lg-min: @screen-lg; +//** Deprecated `@screen-lg-desktop` as of v3.0.1 +@screen-lg-desktop: @screen-lg-min; + +// So media queries don't overlap when required, provide a maximum +@screen-xs-max: (@screen-sm-min - 1); +@screen-sm-max: (@screen-md-min - 1); +@screen-md-max: (@screen-lg-min - 1); + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +@grid-columns: 12; +//** Padding between columns. Gets divided in half for the left and right. +@grid-gutter-width: 30px; +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +@grid-float-breakpoint: @screen-sm-min; +//** Point at which the navbar begins collapsing. +@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); + + +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +@container-tablet: ((720px + @grid-gutter-width)); +//** For `@screen-sm-min` and up. +@container-sm: @container-tablet; + +// Medium screen / desktop +@container-desktop: ((940px + @grid-gutter-width)); +//** For `@screen-md-min` and up. +@container-md: @container-desktop; + +// Large screen / wide desktop +@container-large-desktop: ((1140px + @grid-gutter-width)); +//** For `@screen-lg-min` and up. +@container-lg: @container-large-desktop; + + +//== Navbar +// +//## + +// Basics of a navbar +@navbar-height: 50px; +@navbar-margin-bottom: @line-height-computed; +@navbar-border-radius: @border-radius-base; +@navbar-padding-horizontal: floor((@grid-gutter-width / 2)); +@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); +@navbar-collapse-max-height: 340px; + +@navbar-default-color: #777; +@navbar-default-bg: #f8f8f8; +@navbar-default-border: darken(@navbar-default-bg, 6.5%); + +// Navbar links +@navbar-default-link-color: #777; +@navbar-default-link-hover-color: #333; +@navbar-default-link-hover-bg: transparent; +@navbar-default-link-active-color: #555; +@navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%); +@navbar-default-link-disabled-color: #ccc; +@navbar-default-link-disabled-bg: transparent; + +// Navbar brand label +@navbar-default-brand-color: @navbar-default-link-color; +@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); +@navbar-default-brand-hover-bg: transparent; + +// Navbar toggle +@navbar-default-toggle-hover-bg: #ddd; +@navbar-default-toggle-icon-bar-bg: #888; +@navbar-default-toggle-border-color: #ddd; + + +// Inverted navbar +// Reset inverted navbar basics +@navbar-inverse-color: @gray-light; +@navbar-inverse-bg: #222; +@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); + +// Inverted navbar links +@navbar-inverse-link-color: @gray-light; +@navbar-inverse-link-hover-color: #fff; +@navbar-inverse-link-hover-bg: transparent; +@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; +@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%); +@navbar-inverse-link-disabled-color: #444; +@navbar-inverse-link-disabled-bg: transparent; + +// Inverted navbar brand label +@navbar-inverse-brand-color: @navbar-inverse-link-color; +@navbar-inverse-brand-hover-color: #fff; +@navbar-inverse-brand-hover-bg: transparent; + +// Inverted navbar toggle +@navbar-inverse-toggle-hover-bg: #333; +@navbar-inverse-toggle-icon-bar-bg: #fff; +@navbar-inverse-toggle-border-color: #333; + + +//== Navs +// +//## + +//=== Shared nav styles +@nav-link-padding: 10px 15px; +@nav-link-hover-bg: @gray-lighter; + +@nav-disabled-link-color: @gray-light; +@nav-disabled-link-hover-color: @gray-light; + +@nav-open-link-hover-color: #fff; + +//== Tabs +@nav-tabs-border-color: #ddd; + +@nav-tabs-link-hover-border-color: @gray-lighter; + +@nav-tabs-active-link-hover-bg: @body-bg; +@nav-tabs-active-link-hover-color: @gray; +@nav-tabs-active-link-hover-border-color: #ddd; + +@nav-tabs-justified-link-border-color: #ddd; +@nav-tabs-justified-active-link-border-color: @body-bg; + +//== Pills +@nav-pills-border-radius: @border-radius-base; +@nav-pills-active-link-hover-bg: @component-active-bg; +@nav-pills-active-link-hover-color: @component-active-color; + + +//== Pagination +// +//## + +@pagination-color: @link-color; +@pagination-bg: #fff; +@pagination-border: #ddd; + +@pagination-hover-color: @link-hover-color; +@pagination-hover-bg: @gray-lighter; +@pagination-hover-border: #ddd; + +@pagination-active-color: #fff; +@pagination-active-bg: @brand-primary; +@pagination-active-border: @brand-primary; + +@pagination-disabled-color: @gray-light; +@pagination-disabled-bg: #fff; +@pagination-disabled-border: #ddd; + + +//== Pager +// +//## + +@pager-bg: @pagination-bg; +@pager-border: @pagination-border; +@pager-border-radius: 15px; + +@pager-hover-bg: @pagination-hover-bg; + +@pager-active-bg: @pagination-active-bg; +@pager-active-color: @pagination-active-color; + +@pager-disabled-color: @pagination-disabled-color; + + +//== Jumbotron +// +//## + +@jumbotron-padding: 30px; +@jumbotron-color: inherit; +@jumbotron-bg: @gray-lighter; +@jumbotron-heading-color: inherit; +@jumbotron-font-size: ceil((@font-size-base * 1.5)); + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +@state-success-text: #3c763d; +@state-success-bg: #dff0d8; +@state-success-border: darken(spin(@state-success-bg, -10), 5%); + +@state-info-text: #31708f; +@state-info-bg: #d9edf7; +@state-info-border: darken(spin(@state-info-bg, -10), 7%); + +@state-warning-text: #8a6d3b; +@state-warning-bg: #fcf8e3; +@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); + +@state-danger-text: #a94442; +@state-danger-bg: #f2dede; +@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); + + +//== Tooltips +// +//## + +//** Tooltip max width +@tooltip-max-width: 200px; +//** Tooltip text color +@tooltip-color: #fff; +//** Tooltip background color +@tooltip-bg: #000; +@tooltip-opacity: .9; + +//** Tooltip arrow width +@tooltip-arrow-width: 5px; +//** Tooltip arrow color +@tooltip-arrow-color: @tooltip-bg; + + +//== Popovers +// +//## + +//** Popover body background color +@popover-bg: #fff; +//** Popover maximum width +@popover-max-width: 276px; +//** Popover border color +@popover-border-color: rgba(0,0,0,.2); +//** Popover fallback border color +@popover-fallback-border-color: #ccc; + +//** Popover title background color +@popover-title-bg: darken(@popover-bg, 3%); + +//** Popover arrow width +@popover-arrow-width: 10px; +//** Popover arrow color +@popover-arrow-color: #fff; + +//** Popover outer arrow width +@popover-arrow-outer-width: (@popover-arrow-width + 1); +//** Popover outer arrow color +@popover-arrow-outer-color: fadein(@popover-border-color, 5%); +//** Popover outer arrow fallback color +@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); + + +//== Labels +// +//## + +//** Default label background color +@label-default-bg: @gray-light; +//** Primary label background color +@label-primary-bg: @brand-primary; +//** Success label background color +@label-success-bg: @brand-success; +//** Info label background color +@label-info-bg: @brand-info; +//** Warning label background color +@label-warning-bg: @brand-warning; +//** Danger label background color +@label-danger-bg: @brand-danger; + +//** Default label text color +@label-color: #fff; +//** Default text color of a linked label +@label-link-hover-color: #fff; + + +//== Modals +// +//## + +//** Padding applied to the modal body +@modal-inner-padding: 15px; + +//** Padding applied to the modal title +@modal-title-padding: 15px; +//** Modal title line-height +@modal-title-line-height: @line-height-base; + +//** Background color of modal content area +@modal-content-bg: #fff; +//** Modal content border color +@modal-content-border-color: rgba(0,0,0,.2); +//** Modal content border color **for IE8** +@modal-content-fallback-border-color: #999; + +//** Modal backdrop background color +@modal-backdrop-bg: #000; +//** Modal backdrop opacity +@modal-backdrop-opacity: .5; +//** Modal header border color +@modal-header-border-color: #e5e5e5; +//** Modal footer border color +@modal-footer-border-color: @modal-header-border-color; + +@modal-lg: 900px; +@modal-md: 600px; +@modal-sm: 300px; + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +@alert-padding: 15px; +@alert-border-radius: @border-radius-base; +@alert-link-font-weight: bold; + +@alert-success-bg: @state-success-bg; +@alert-success-text: @state-success-text; +@alert-success-border: @state-success-border; + +@alert-info-bg: @state-info-bg; +@alert-info-text: @state-info-text; +@alert-info-border: @state-info-border; + +@alert-warning-bg: @state-warning-bg; +@alert-warning-text: @state-warning-text; +@alert-warning-border: @state-warning-border; + +@alert-danger-bg: @state-danger-bg; +@alert-danger-text: @state-danger-text; +@alert-danger-border: @state-danger-border; + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +@progress-bg: #f5f5f5; +//** Progress bar text color +@progress-bar-color: #fff; + +//** Default progress bar color +@progress-bar-bg: @brand-primary; +//** Success progress bar color +@progress-bar-success-bg: @brand-success; +//** Warning progress bar color +@progress-bar-warning-bg: @brand-warning; +//** Danger progress bar color +@progress-bar-danger-bg: @brand-danger; +//** Info progress bar color +@progress-bar-info-bg: @brand-info; + + +//== List group +// +//## + +//** Background color on `.list-group-item` +@list-group-bg: #fff; +//** `.list-group-item` border color +@list-group-border: #ddd; +//** List group border radius +@list-group-border-radius: @border-radius-base; + +//** Background color of single list items on hover +@list-group-hover-bg: #f5f5f5; +//** Text color of active list items +@list-group-active-color: @component-active-color; +//** Background color of active list items +@list-group-active-bg: @component-active-bg; +//** Border color of active list elements +@list-group-active-border: @list-group-active-bg; +//** Text color for content within active list items +@list-group-active-text-color: lighten(@list-group-active-bg, 40%); + +//** Text color of disabled list items +@list-group-disabled-color: @gray-light; +//** Background color of disabled list items +@list-group-disabled-bg: @gray-lighter; +//** Text color for content within disabled list items +@list-group-disabled-text-color: @list-group-disabled-color; + +@list-group-link-color: #555; +@list-group-link-hover-color: @list-group-link-color; +@list-group-link-heading-color: #333; + + +//== Panels +// +//## + +@panel-bg: #fff; +@panel-body-padding: 15px; +@panel-heading-padding: 10px 15px; +@panel-footer-padding: @panel-heading-padding; +@panel-border-radius: @border-radius-base; + +//** Border color for elements within panels +@panel-inner-border: #ddd; +@panel-footer-bg: #f5f5f5; + +@panel-default-text: @gray-dark; +@panel-default-border: #ddd; +@panel-default-heading-bg: #f5f5f5; + +@panel-primary-text: #fff; +@panel-primary-border: @brand-primary; +@panel-primary-heading-bg: @brand-primary; + +@panel-success-text: @state-success-text; +@panel-success-border: @state-success-border; +@panel-success-heading-bg: @state-success-bg; + +@panel-info-text: @state-info-text; +@panel-info-border: @state-info-border; +@panel-info-heading-bg: @state-info-bg; + +@panel-warning-text: @state-warning-text; +@panel-warning-border: @state-warning-border; +@panel-warning-heading-bg: @state-warning-bg; + +@panel-danger-text: @state-danger-text; +@panel-danger-border: @state-danger-border; +@panel-danger-heading-bg: @state-danger-bg; + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +@thumbnail-padding: 4px; +//** Thumbnail background color +@thumbnail-bg: @body-bg; +//** Thumbnail border color +@thumbnail-border: #ddd; +//** Thumbnail border radius +@thumbnail-border-radius: @border-radius-base; + +//** Custom text color for thumbnail captions +@thumbnail-caption-color: @text-color; +//** Padding around the thumbnail caption +@thumbnail-caption-padding: 9px; + + +//== Wells +// +//## + +@well-bg: #f5f5f5; +@well-border: darken(@well-bg, 7%); + + +//== Badges +// +//## + +@badge-color: #fff; +//** Linked badge text color on hover +@badge-link-hover-color: #fff; +@badge-bg: @gray-light; + +//** Badge text color in active nav link +@badge-active-color: @link-color; +//** Badge background color in active nav link +@badge-active-bg: #fff; + +@badge-font-weight: bold; +@badge-line-height: 1; +@badge-border-radius: 10px; + + +//== Breadcrumbs +// +//## + +@breadcrumb-padding-vertical: 8px; +@breadcrumb-padding-horizontal: 15px; +//** Breadcrumb background color +@breadcrumb-bg: #f5f5f5; +//** Breadcrumb text color +@breadcrumb-color: #ccc; +//** Text color of current page in the breadcrumb +@breadcrumb-active-color: @gray-light; +//** Textual separator for between breadcrumb elements +@breadcrumb-separator: "/"; + + +//== Carousel +// +//## + +@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); + +@carousel-control-color: #fff; +@carousel-control-width: 15%; +@carousel-control-opacity: .5; +@carousel-control-font-size: 20px; + +@carousel-indicator-active-bg: #fff; +@carousel-indicator-border-color: #fff; + +@carousel-caption-color: #fff; + + +//== Close +// +//## + +@close-font-weight: bold; +@close-color: #000; +@close-text-shadow: 0 1px 0 #fff; + + +//== Code +// +//## + +@code-color: #c7254e; +@code-bg: #f9f2f4; + +@kbd-color: #fff; +@kbd-bg: #333; + +@pre-bg: #f5f5f5; +@pre-color: @gray-dark; +@pre-border-color: #ccc; +@pre-scrollable-max-height: 340px; + + +//== Type +// +//## + +//** Horizontal offset for forms and lists. +@component-offset-horizontal: 180px; +//** Text muted color +@text-muted: @gray-light; +//** Abbreviations and acronyms border color +@abbr-border-color: @gray-light; +//** Headings small color +@headings-small-color: @gray-light; +//** Blockquote small color +@blockquote-small-color: @gray-light; +//** Blockquote font size +@blockquote-font-size: (@font-size-base * 1.25); +//** Blockquote border color +@blockquote-border-color: @gray-lighter; +//** Page header border color +@page-header-border-color: @gray-lighter; +//** Width of horizontal description list titles +@dl-horizontal-offset: @component-offset-horizontal; +//** Horizontal line color. +@hr-border: @gray-lighter; + + diff --git a/labs/meteor-client/app/client/lib/custom.bootstrap.json b/labs/meteor-client/app/client/lib/custom.bootstrap.json new file mode 100644 index 0000000000000000000000000000000000000000..01f000e56cc1aca9e2bee5b52814c35d5d60a6a1 --- /dev/null +++ b/labs/meteor-client/app/client/lib/custom.bootstrap.json @@ -0,0 +1,51 @@ +{"modules": { + "normalize": true, + "print": true, + + "scaffolding": true, + "type": true, + "code": true, + "grid": true, + "tables": true, + "forms": true, + "buttons": true, + + "glyphicons": true, + "button-groups": true, + "input-groups": true, + "navs": true, + "navbar": true, + "breadcrumbs": true, + "pagination": true, + "pager": true, + "labels": true, + "badges": true, + "jumbotron": true, + "thumbnails": true, + "alerts": true, + "progress-bars": true, + "media": true, + "list-group": true, + "panels": true, + "wells": true, + "close": true, + + "component-animations": true, + "dropdowns": true, + "modals": true, + "tooltip": true, + "popovers": true, + "carousel": true, + + "affix": true, + "alert": true, + "button": true, + "collapse": true, + "scrollspy": true, + "tab": true, + "transition": true, + + "utilities": true, + "responsive-utilities": true +}} + diff --git a/labs/meteor-client/app/client/lib/scale.raphael.js b/labs/meteor-client/app/client/lib/scale.raphael.js new file mode 100644 index 0000000000000000000000000000000000000000..4339ea9efa85fbe9dac93bd6e890060cd0775f46 --- /dev/null +++ b/labs/meteor-client/app/client/lib/scale.raphael.js @@ -0,0 +1,102 @@ +/* + * ScaleRaphael 0.8 by Zevan Rosser 2010 + * For use with Raphael library : www.raphaeljs.com + * Licensed under the MIT license. + * + * www.shapevent.com/scaleraphael/ + */ +(function(){ + window.ScaleRaphael = function(container, width, height){ + var wrapper = document.getElementById(container); + if (!wrapper.style.position) wrapper.style.position = "relative"; + wrapper.style.width = width + "px"; + wrapper.style.height = height + "px"; + wrapper.style.overflow = "hidden"; + var nestedWrapper; + + if (Raphael.type == "VML"){ + wrapper.innerHTML = "<rvml:group style='position : absolute; width: 1000px; height: 1000px; top: 0px; left: 0px' coordsize='1000,1000' class='rvml' id='vmlgroup'><\/rvml:group>"; + nestedWrapper = document.getElementById("vmlgroup"); + }else{ + wrapper.innerHTML = "<div id='svggroup'><\/div>"; + nestedWrapper = document.getElementById("svggroup"); + } + + var paper = new Raphael(nestedWrapper, width, height); + var vmlDiv; + + if (Raphael.type == "SVG"){ + paper.canvas.setAttribute("viewBox", "0 0 "+width+" "+height); + }else{ + vmlDiv = wrapper.getElementsByTagName("div")[0]; + } + + paper.changeSize = function(w, h, center, clipping){ + clipping = !clipping; + + var ratioW = w / width; + var ratioH = h / height; + var scale = ratioW < ratioH ? ratioW : ratioH; + + var newHeight = parseInt(height * scale); + var newWidth = parseInt(width * scale); + + if (Raphael.type == "VML"){ + // scale the textpaths + var txt = document.getElementsByTagName("textpath"); + for (var i in txt){ + var curr = txt[i]; + if (curr.style){ + if(!curr._fontSize){ + var mod = curr.style.font.split("px"); + curr._fontSize = parseInt(mod[0]); + curr._font = mod[1]; + } + curr.style.font = curr._fontSize * scale + "px" + curr._font; + } + } + var newSize; + + if (newWidth < newHeight){ + newSize = newWidth * 1000 / width; + }else{ + newSize = newHeight * 1000 / height; + } + newSize = parseInt(newSize); + nestedWrapper.style.width = newSize + "px"; + nestedWrapper.style.height = newSize + "px"; + if (clipping){ + nestedWrapper.style.left = parseInt((w - newWidth) / 2) + "px"; + nestedWrapper.style.top = parseInt((h - newHeight) / 2) + "px"; + } + vmlDiv.style.overflow = "visible"; + } + + if (clipping){ + newWidth = w; + newHeight = h; + } + + wrapper.style.width = newWidth + "px"; + wrapper.style.height = newHeight + "px"; + paper.setSize(newWidth, newHeight); + + if (center){ + wrapper.style.position = "absolute"; + wrapper.style.left = parseInt((w - newWidth) / 2) + "px"; + wrapper.style.top = parseInt((h - newHeight) / 2) + "px"; + } + } + + paper.scaleAll = function(amount){ + paper.changeSize(width * amount, height * amount); + } + + paper.changeSize(width, height); + + paper.w = width; + paper.h = height; + + return paper; + } +})(); \ No newline at end of file diff --git a/labs/meteor-client/app/client/main.coffee b/labs/meteor-client/app/client/main.coffee new file mode 100755 index 0000000000000000000000000000000000000000..2449332c3ce35ea34e5b1095c1104fa1eaef166b --- /dev/null +++ b/labs/meteor-client/app/client/main.coffee @@ -0,0 +1,199 @@ +# Helper to load javascript libraries from the BBB server +loadLib = (libname) -> + successCallback = -> + + retryMessageCallback = (param) -> + #Meteor.log.info "Failed to load library", param + console.log "Failed to load library", param + + Meteor.Loader.loadJs("http://#{window.location.hostname}/client/lib/#{libname}", successCallback, 10000).fail(retryMessageCallback) + +# These settings can just be stored locally in session, created at start up +Meteor.startup -> + + # Load SIP libraries before the application starts + loadLib('sip.js') + loadLib('bbb_webrtc_bridge_sip.js') + + @SessionAmplify = _.extend({}, Session, + keys: _.object(_.map(amplify.store(), (value, key) -> + [ + key + JSON.stringify(value) + ] + )) + set: (key, value) -> + Session.set.apply this, arguments + amplify.store key, value + return + ) +# +Template.footer.helpers + getFooterString: -> + info = getBuildInformation() + foot = "(c) #{info.copyrightYear} BigBlueButton Inc. [build #{info.bbbServerVersion} - #{info.dateOfBuild}] - For more information visit #{info.link}" + +Template.header.events + "click .audioFeedIcon": (event) -> + $('.audioFeedIcon').blur() + toggleSlidingMenu() + toggleVoiceCall @ + + "click .chatBarIcon": (event) -> + $(".tooltip").hide() + toggleSlidingMenu() + toggleChatbar() + + "click .collapseButton": (event) -> + toggleSlidingMenu() + $(".tooltip").hide() + $('.collapseButton').blur() + $('.myNavbar').css('z-index', 1032) + + "click .hideNavbarIcon": (event) -> + $(".tooltip").hide() + toggleNavbar() + + "click .lowerHand": (event) -> + $(".tooltip").hide() + toggleSlidingMenu() + Meteor.call('userLowerHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken")) + + "click .muteIcon": (event) -> + $(".tooltip").hide() + toggleMic @ + + "click .raiseHand": (event) -> + #Meteor.log.info "navbar raise own hand from client" + console.log "navbar raise own hand from client" + $(".tooltip").hide() + toggleSlidingMenu() + Meteor.call('userRaiseHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken")) + # "click .settingsIcon": (event) -> + # alert "settings" + + "click .signOutIcon": (event) -> + $('.signOutIcon').blur() + if window.matchMedia('(orientation: portrait)').matches + if $('#dialog').dialog('option', 'height') isnt 450 + $('#dialog').dialog('option', 'width', '100%') + $('#dialog').dialog('option', 'height', 450) + else + if $('#dialog').dialog('option', 'height') isnt 115 + $('#dialog').dialog('option', 'width', 270) + $('#dialog').dialog('option', 'height', 115) + $("#dialog").dialog("open") + "click .hideNavbarIcon": (event) -> + $(".tooltip").hide() + toggleNavbar() + # "click .settingsIcon": (event) -> + # alert "settings" + + "click .usersListIcon": (event) -> + $(".tooltip").hide() + toggleSlidingMenu + toggleUsersList() + + "click .videoFeedIcon": (event) -> + $(".tooltip").hide() + toggleCam @ + + "click .whiteboardIcon": (event) -> + $(".tooltip").hide() + toggleSlidingMenu + toggleWhiteBoard() + + "mouseout #navbarMinimizedButton": (event) -> + $("#navbarMinimizedButton").removeClass("navbarMinimizedButtonLarge") + $("#navbarMinimizedButton").addClass("navbarMinimizedButtonSmall") + + "mouseover #navbarMinimizedButton": (event) -> + $("#navbarMinimizedButton").removeClass("navbarMinimizedButtonSmall") + $("#navbarMinimizedButton").addClass("navbarMinimizedButtonLarge") + +Template.slidingMenu.events + 'click .audioFeedIcon': (event) -> + $('.audioFeedIcon').blur() + toggleSlidingMenu() + toggleVoiceCall @ + if BBB.amISharingAudio() + $('.navbarTitle').css('width', '70%') + else + $('.navbarTitle').css('width', '55%') + + 'click .chatBarIcon': (event) -> + $('.tooltip').hide() + toggleSlidingMenu() + toggleChatbar() + + 'click .lowerHand': (event) -> + $('.tooltip').hide() + toggleSlidingMenu() + Meteor.call('userLowerHand', getInSession('meetingId'), getInSession('userId'), getInSession('userId'), getInSession('authToken')) + + 'click .raiseHand': (event) -> + console.log 'navbar raise own hand from client' + $('.tooltip').hide() + toggleSlidingMenu() + Meteor.call('userRaiseHand', getInSession("meetingId"), getInSession("userId"), getInSession("userId"), getInSession("authToken")) + + 'click .usersListIcon': (event) -> + $('.tooltip').hide() + toggleSlidingMenu() + toggleUsersList() + + 'click .whiteboardIcon': (event) -> + $('.tooltip').hide() + toggleSlidingMenu() + toggleWhiteBoard() + + 'click .collapseButton': (event) -> + $('.tooltip').hide() + toggleSlidingMenu() + $('.collapseButton').blur() + +Template.main.helpers + setTitle: -> + document.title = "BigBlueButton #{window.getMeetingName() ? 'HTML5'}" + +Template.main.rendered = -> + $("#dialog").dialog( + modal: true + draggable: false + resizable: false + autoOpen: false + dialogClass: 'no-close logout-dialog' + buttons: [ + { + text: 'Yes' + click: () -> + userLogout getInSession("meetingId"), getInSession("userId"), true + $(this).dialog("close") + class: 'btn btn-xs btn-primary active' + } + { + text: 'No' + click: () -> + $(this).dialog("close") + $(".tooltip").hide() + class: 'btn btn-xs btn-default' + } + ] + position: + my: 'right top' + at: 'right bottom' + of: '.signOutIcon' + ) + + $(window).resize( -> + $('#dialog').dialog('close') + ) + + $('#darkened-screen').click () -> + toggleSlidingMenu() + +Template.makeButton.rendered = -> + $('button[rel=tooltip]').tooltip() + +Template.recordingStatus.rendered = -> + $('button[rel=tooltip]').tooltip() diff --git a/labs/meteor-client/app/client/main.html b/labs/meteor-client/app/client/main.html new file mode 100755 index 0000000000000000000000000000000000000000..4b1d227e123dc0fa3e7345eead44e4a174b4f2df --- /dev/null +++ b/labs/meteor-client/app/client/main.html @@ -0,0 +1,161 @@ +<template name="footer"> + <div class="myFooter gradientBar navbar navbar-default navbar-fixed-bottom" role="navigation"> + {{{getFooterString}}} + </div> +</template> + +<template name="header"> + {{#if getInSession "display_navbar"}} + <div id="navbar" class="myNavbar gradientBar navbar navbar-default navbar-fixed-top" role="navigation"> + <div class="navbarUserButtons navbarSection"> + <div id="collapseButtonSection"> + {{#if getInSession "display_slidingMenu"}} + {{> makeButton btn_class="navbarButton collapseButton" i_class="chevron-left" rel="tooltip" data_placement="bottom" title="Collapse"}} + {{else}} + {{> makeButton btn_class="navbarButton collapseButton" i_class="chevron-right" rel="tooltip" data_placement="bottom" title="Expand"}} + {{/if}} + </div> + <div class='collapseSection'> + <!-- display/hide users list toggle --> + {{#if getInSession "display_usersList"}} + {{> makeButton btn_class="navbarIconToggleActive usersListIcon navbarButton collapseSectionButton" i_class="user" rel="tooltip" data_placement="bottom" title="Hide List of Users"}} + {{else}} + {{> makeButton btn_class="usersListIcon navbarButton collapseSectionButton" i_class="user" rel="tooltip" data_placement="bottom" title="Show List of Users"}} + {{/if}} + + <!-- display/hide whiteboard toggle --> + {{#if getInSession "display_whiteboard"}} + {{> makeButton btn_class="navbarIconToggleActive whiteboardIcon navbarButton collapseSectionButton" i_class="pencil" rel="tooltip" data_placement="bottom" title="Hide Whiteboard"}} + {{else}} + {{> makeButton btn_class="whiteboardIcon navbarButton collapseSectionButton" i_class="pencil" rel="tooltip" data_placement="bottom" title="Show Whiteboard"}} + {{/if}} + + <!-- display/hide chat bar toggle --> + {{#if getInSession "display_chatbar"}} + {{> makeButton btn_class="navbarIconToggleActive chatBarIcon navbarButton collapseSectionButton" i_class="comment" rel="tooltip" data_placement="bottom" title="Hide Message Pane"}} + {{else}} + {{> makeButton btn_class="chatBarIcon navbarButton collapseSectionButton" i_class="comment" rel="tooltip" data_placement="bottom" title="Show Message Pane"}} + {{/if}} + + <!-- display/hide webcam streams toggle --> + <!-- {{#if isCurrentUserSharingVideo}} + {{> makeButton btn_class="navbarIconToggleActive videoFeedIcon navbarButton" i_class="stop" sharingVideo=true rel="tooltip" data_placement="bottom" title="Hide Webcams"}} + {{else}} + {{> makeButton btn_class="videoFeedIcon navbarButton" i_class="facetime-video" sharingVideo=false rel="tooltip" data_placement="bottom" title="Show Webcams"}} + {{/if}} --> + </div> + <div class='audioControllersSection'> + <!-- Join/hang up audio call --> + {{#if isCurrentUserSharingAudio}} + <div class='collapseSection'> + {{> makeButton btn_class="navbarIconToggleActive audioFeedIcon navbarButton audioButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="bottom" title="Leave Audio Call"}} + </div> + {{#if isCurrentUserMuted}} + {{> makeButton btn_class="muteIcon navbarButton audioButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="bottom" title="Unmute"}} + {{else}} + {{#if isCurrentUserTalking}} + {{> makeButton btn_class="navbarIconToggleActive muteIcon navbarButton audioButton" i_class="volume-up" sharingAudio=true rel="tooltip" data_placement="bottom" title="Mute"}} + {{else}} + {{> makeButton btn_class="navbarIconToggleActive muteIcon navbarButton audioButton" i_class="volume-down" sharingAudio=true rel="tooltip" data_placement="bottom" title="Mute"}} + {{/if}} + {{/if}} + {{else}} + <div class='collapseSection'> + {{> makeButton btn_class="audioFeedIcon navbarButton audioButton" i_class="headphones" sharingAudio=false rel="tooltip" data_placement="bottom" title="Join Audio Call"}} + </div> + {{/if}} + </div> + <div class='collapseSection'> + {{#if isCurrentUserRaisingHand}} + {{> makeButton btn_class="lowerHand navbarIconToggleActive navbarButton collapseSectionButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Lower your hand"}} + {{else}} + {{> makeButton btn_class="raiseHand navbarButton collapseSectionButton" i_class="hand-up" rel="tooltip" data_placement="bottom" title="Raise your hand"}} + {{/if}} + + {{> recordingStatus}} + </div> + </div> + <div class="navbarTitle navbarSection"><span>{{getMeetingName}}</span></div> + <div class="navbarSettingsButtons navbarSection"> + <!-- {{> makeButton id="userId" btn_class="settingsIcon navbarButton" i_class="cog" rel="tooltip" data_placement="bottom" title="Settings"}} --> + <!-- {{> makeButton btn_class="hideNavbarIcon navbarButton" i_class="chevron-up" rel="tooltip" data_placement="bottom" title="Hide Navbar"}} --> + {{> makeButton btn_class="signOutIcon navbarButton" i_class="log-out" rel="tooltip" data_placement="bottom" title="Logout"}} + </div> + </div> + <div class="navbarFiller"></div> + {{else}} + {{> makeButton id="navbarMinimizedButton" btn_class="hideNavbarIcon navbarMinimizedButtonSmall" i_class="chevron-down" rel="tooltip" data_placement="bottom" title="Display Navbar"}} + {{/if}} +</template> + +<template name="main"> + {{setTitle}} + <body> + <div id="dialog" title="Confirm Logout"> + <p>Are you sure you want to log out?</p> + </div> + <div id="main" class="mainContainer row-fluid"> + {{#if isDisconnected}} + {{>status}} + {{else}} + <div>{{> header}}</div> + {{> whiteboard id="whiteboard" title=getWhiteboardTitle name="whiteboard"}} + {{> chatbar id="chat" title="Chat" name="chatbar"}} + {{> usersList id="users" name="usersList"}} + <audio id="remote-media" autoplay="autoplay"></audio> + {{> footer}} + {{/if}} + </div> + {{> slidingMenu}} + <div id='darkened-screen'></div> + </body> +</template> + +<template name="recordingStatus"> + <!-- Recording status of the meeting --> + {{#with getCurrentMeeting}} + {{#if intendedForRecording}} + {{#if currentlyBeingRecorded}} + <button class="recordingStatus recordingStatusTrue" rel="tooltip" data-placement="bottom" title="This Meeting is Being Recorded"><span class="glyphicon glyphicon-record"></span> Recording</button> + {{else}} + <button class="recordingStatus recordingStatusFalse" rel="tooltip" data-placement="bottom" title="This Meeting is Not Currently Being Recorded"><span class="glyphicon glyphicon-record"></span></button> + {{/if}} + {{/if}} + {{/with}} +</template> + +<template name='slidingMenu'> + <div class="sliding-menu" id="sliding-menu"> + <div class="slideSection"> + {{#if getInSession "display_usersList"}} + {{> makeButton btn_class="navbarIconToggleActive usersListIcon slideButton" i_class="user" rel="tooltip" data_placement="right" title="Hide List of Users"}} + {{else}} + {{> makeButton btn_class="usersListIcon slideButton" i_class="user" rel="tooltip" data_placement="right" title="Show List of Users"}} + {{/if}} + + {{#if getInSession "display_whiteboard"}} + {{> makeButton btn_class="navbarIconToggleActive whiteboardIcon slideButton" i_class="pencil" rel="tooltip" data_placement="right" title="Hide Whiteboard"}} + {{else}} + {{> makeButton btn_class="whiteboardIcon slideButton" i_class="pencil" rel="tooltip" data_placement="right" title="Show Whiteboard"}} + {{/if}} + + {{#if getInSession "display_chatbar"}} + {{> makeButton btn_class="navbarIconToggleActive chatBarIcon slideButton" i_class="comment" rel="tooltip" data_placement="right" title="Hide Message Pane"}} + {{else}} + {{> makeButton btn_class="chatBarIcon slideButton" i_class="comment" rel="tooltip" data_placement="right" title="Show Message Pane"}} + {{/if}} + + {{#if isCurrentUserSharingAudio}} + {{> makeButton btn_class="navbarIconToggleActive audioFeedIcon slideButton" i_class="volume-off" sharingAudio=true rel="tooltip" data_placement="right" title="Leave Audio Call"}} + {{else}} + {{> makeButton btn_class="audioFeedIcon slideButton" i_class="headphones" sharingAudio=false rel="tooltip" data_placement="right" title="Join Audio Call"}} + {{/if}} + + {{#if isCurrentUserRaisingHand}} + {{> makeButton btn_class="lowerHand navbarIconToggleActive slideButton" i_class="hand-up" rel="tooltip" data_placement="right" title="Lower your hand"}} + {{else}} + {{> makeButton btn_class="raiseHand slideButton" i_class="hand-up" rel="tooltip" data_placement="right" title="Raise your hand"}} + {{/if}} + </div> + </div> +</template> diff --git a/labs/meteor-client/app/client/stylesheets/colors.less b/labs/meteor-client/app/client/stylesheets/colors.less new file mode 100755 index 0000000000000000000000000000000000000000..cad4bc012a41ef4190dd86cf70fc64f3ca47d954 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/colors.less @@ -0,0 +1,7 @@ +/* sorted by brightness */ +@white: #fff, #f5f5f5, #eee; +@lightGrey: #d7d7d7, #cccdd1, #ccc, #999; +@darkGrey: #666, #60636a, #40434c, #32353e; +@azure: #54a7db, #0099FF; +@black: #000; +@yellow: #E3E1B8, #F9DF6B; \ No newline at end of file diff --git a/labs/meteor-client/app/client/stylesheets/common/chat.less b/labs/meteor-client/app/client/stylesheets/common/chat.less new file mode 100755 index 0000000000000000000000000000000000000000..914d998e0375853c133fc19a95f53efa72e0b86a --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/common/chat.less @@ -0,0 +1,210 @@ +@import "../colors"; + +bottomEntry { + border: none; + padding-bottom: 0px; + padding-top: 0px; +} + +#chat { + background-color: extract(@white, 2); + border: 1px solid extract(@lightGrey, 3); + float: left; + background: extract(@white, 1); + margin-left: 0.25%; + margin-right: 0.25%; + overflow: hidden; + -webkit-flex: 1 6 25%; + flex: 1 6 25%; +} + +.chat { + list-style: none; + margin: 0px; + padding: 0px; + li { + margin: 0px; + padding: 15px; + word-wrap: break-word; + &:nth-child(even) { + background-color: rgb(245,245,245); + } + table { + width: 100%; + } + } +} + +#chatbar-contents { + background-color: extract(@white, 1); + height: 90%; + padding-left: 0px; + padding-right: 0px; + width: 100%; +} + +#chatbody { + height: 90%; + overflow-y: scroll; + padding-left: 0px; + padding-right: 0px; +} + +.chatGreeting { + color: blue; + margin-top: 5px; +} + +.chat-input-wrapper { + padding:10px; +} + +.chatNameSelector { + font-size: 14px; + &:hover { + color: black !important; + font-weight: bold; + } +} + +#chat-options-bar { + border-bottom: 1px solid extract(@lightGrey, 3); + position: relative; + width: 100%; +} + +#chat-user-list { + padding: 5px; +} + +.closeTab { + background-color: red !important; + border-radius: 5px; + color: extract(@white, 1); + cursor: pointer; + font-size: 12px; + font-weight: bold; + margin-left: 5px; + margin-top: -3px; + opacity: 0.2 !important; + padding-left: 3px !important; + padding-right: 3px !important; + padding-top: 4px !important; + text-shadow: 0 1px 0 extract(@white, 1); + -webkit-appearance: none; + + &:hover { + color: extract(@white, 1) !important; + opacity: 1 !important; + } +} + +.gotUnreadMail { + background: extract(@yellow, 2) !important; +} + +#MoreChatsDrop { + float: right; +} + +#MoreChatsbutton { + margin-right: 10px; + margin-top: 5px; + padding-bottom: 0px; + padding-top: 0px; +} + +#newMessageInput { + display: block; + float: left; + width: 77%; + resize: none; + padding:5px; + border-radius:4px; + border:1px solid extract(@lightGrey, 3); +} + +.optionsBar { + height: 100%; + padding-left: 15px; + padding-top: 15px; +} + +.panel-footer { + bottom: 0px; + padding: 10px; + position: relative; + background: extract(@white, 1); +} + +.private-chat-user-box { + border: left 1px grey; + height: 60%; + padding: 5px; + width: 90%; +} + +.private-chat-user-list { + font-size: 12px; + :hover { + background: extract(@azure, 2); + font-size: 14px; + font-style: italic; + } +} + +#sendMessageButton { + background-color: extract(@azure, 1); + margin-left: 8px; + color: extract(@white, 1); +} + +#tabsList { + display: table; + margin: 0; + padding: 0; + table-layout: fixed; + text-align: center; + width: 100%; + a { + display: block; + min-height: 100%; + /* overflow: hidden; */ + padding: 4px 10px; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + color: extract(@lightGrey, 4); + margin: 0; + border-top: 0; + border-radius: 0; + } + li:first-child a { + border-left: 0; + } + &:before, &:after { + content: none; + } +} + +.tab { + font-size: 14px; + height:auto !important; + &:hover { + border: 1px solid extract(@azure, 2); + border-bottom: none; + } +} + +.timestamp { + text-align: right; +} + +.dropdown { + float: left; + .dropdown-menu { + height: 80px; + overflow-y: scroll; + right: 0px; + } +} diff --git a/labs/meteor-client/app/client/stylesheets/common/style.less b/labs/meteor-client/app/client/stylesheets/common/style.less new file mode 100755 index 0000000000000000000000000000000000000000..13d9d3810f3d521bdcf01ee95d5cdc0950896655 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/common/style.less @@ -0,0 +1,241 @@ +@import "../colors"; + +.radius(@size: 4px) { + -moz-border-radius: @size; + -webkit-border-radius: @size; + border-radius: @size; +} + +.linear-gradient(@color1, @color2) { + background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */ + background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */ + background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */ + background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */ +} + +body { + background: extract(@white, 3); + bottom: 0; + color: extract(@darkGrey, 1); + left: 0; + right: 0; +} + +.btn { + background-color: extract(@white, 2); +} + +.component { + .radius; + background: extract(@white, 1); + border: 1px solid extract(@lightGrey, 3); + float: left; + height: 100%; + margin-top: 10px; +} + +.extraConversationScrollableMenu { + height: auto; + max-height: 200px; + overflow-x: hidden; +} + +.mainContainer { + height: 100%; +} + +.myFooter { + color: black; + font-size: 10px; + max-height: 20px; + padding-top: 13px; + text-align: center; +} + +.myNavbar { + margin-bottom: 0.5%; + &.gradientBar { + .linear-gradient(rgb(72,76,85), rgb(65,68,77)); + } + .btn { + .linear-gradient(rgb(72,76,85), rgb(65,68,77)); + border-left: 1px solid extract(@darkGrey, 2); + border-right: 1px solid extract(@darkGrey, 4); + &.navbarIconToggleActive { + background: extract(@darkGrey, 3); + border-bottom: 4px solid extract(@azure, 1); + } + i { + color: extract(@white, 1); + } + } +} + +.myNavbarMinimized { + background: extract(@white, 3); + height: 20px; + margin-bottom: 0.2%; + margin-top: 0px; + min-width: 900px; + padding-top: 0px; + text-align: right; +} + +.navbar { + min-height: 40px !important; +} + +.navbarFiller { + width: 100%; +} + +.navbarIconToggleActive i { + color: extract(@lightGrey, 2); +} + +#navbarMinimizedButton { + margin-bottom: 0px; + margin-left: 2px; + margin-right: 20px; + margin-top: 0px; + position: absolute; + right: 0; + top: 0; +} + +.navbarMinimizedButtonSmall { + height: 10px; + width: 40px; +} + +.navbarMinimizedButtonLarge { + height: 50px; + width: 40px; +} + +.navbarSettingsButtons { + float: right; + .btn { + display: block; + float: right; + } +} + +.navbarTitle { + color: extract(@white, 1); + display: block; + float: left; + font-weight: bold; + text-align: center; +} + +.navbarUserButtons .btn { + border-radius: 0; + display: block; + float: left; + &:hover { + background: extract(@darkGrey, 3); + } +} + +.navbarSettingsButtons .btn:hover { + background: extract(@darkGrey, 3); +} + +.panel-footer { + padding: 0; +} + +.recordingStatus { + background: none!important; + border: none; + margin-left: 10px; + padding: 0!important; +} + +.recordingStatusFalse { + color: maroon; +} + +.recordingStatusTrue { + color: green; +} + +.ScrollableWindow { + height: 100%; + overflow-y: scroll; +} + +.tab { + height: 40px; +} + +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + border-top: 0px; +} + +.title { + border-bottom: 1px solid extract(@lightGrey, 1); + color: extract(@darkGrey, 1); + font-weight: bold; + line-height: 2em; + margin: 0; + padding-bottom: 5px; + padding-left: 10px; + padding-top: 5px; +} + +#main { + padding-top: 50px; + padding-left: 0.25%; + padding-right: 0.25%; + display: -webkit-flex; + display: flex; +} + +/* Custom alert box */ + +.no-close .ui-dialog-titlebar-close { + display: none; /* no close button */ +} + +.logout-dialog.ui-dialog { + .ui-widget-header { + color: extract(@white, 1); + font-weight: bold; + background: extract(@darkGrey, 3); + } + .ui-dialog-content { + font-weight: bold; + text-align: center; + } +} + +.logout-dialog.ui-widget-content { + background: extract(@white, 3); + border: 5px solid extract(@darkGrey, 3); +} + +.custom-button { + background-color: extract(@yellow, 1); +} + +.fullScreenPresentation { + width: 90% !important; + height: 90% !important; +} + +.halfScreen { + width: 44% !important; + height: 80% !important; +} + +.quarterScreen { + width: 22% !important; + height: 80% !important; +} diff --git a/labs/meteor-client/app/client/stylesheets/common/users.less b/labs/meteor-client/app/client/stylesheets/common/users.less new file mode 100755 index 0000000000000000000000000000000000000000..b07f1ef30c8b4209eb4238f9430beab7d8eae7f8 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/common/users.less @@ -0,0 +1,55 @@ +#content { + margin-top: 10px; + overflow: hidden; +} + +#usericons { + margin-left: 10px; + float: right; + span { + margin-left: 3px; + padding: 2px; + border: 1px solid white; + } + .raisedHandIcon, .muteIcon { + &:hover { + cursor: pointer; + border: 1px solid #f2f2f2; + background-color: #f2f2f2; + border-radius: 4px; + } + } +} + +#username { + float:left; +} + +#users { + margin-left: 0.25%; + margin-right: 0.25%; + padding-bottom: 10px; /*min-width:230px;*/ + -webkit-flex: 1 6 20%; + flex: 1 6 20%; +} + +#user-contents { + height: 95%; + padding-bottom: 10px; +} + +.userCurrent { + font-weight: bold; +} + +/*.userNameContainer { + border-right: 1px solid extract(@lightGrey, 3); + padding-right: 0px; +}*/ + +.userNameEntry { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100px; +} diff --git a/labs/meteor-client/app/client/stylesheets/common/whiteboard.less b/labs/meteor-client/app/client/stylesheets/common/whiteboard.less new file mode 100755 index 0000000000000000000000000000000000000000..fc66702642ff7cbdd2eb28943c510d425112bf4f --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/common/whiteboard.less @@ -0,0 +1,26 @@ +#whiteboard { + padding: 0 !important; + margin-left: 0.25%; + margin-right: 0.25%; + -webkit-flex: 3 1 53%; + flex: 3 1 53%; +} + +#whiteboard-paper { + background-color: white !important; + /*height: auto !important;*/ + margin-left: auto; + margin-right: auto; + #svggroup { + display: block; + height: 100% !important; + width: 100% !important; + } +} + +#whiteboard-navbar { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} \ No newline at end of file diff --git a/labs/meteor-client/app/client/stylesheets/landscape/chat.less b/labs/meteor-client/app/client/stylesheets/landscape/chat.less new file mode 100755 index 0000000000000000000000000000000000000000..98faa09dfa37069e59c508583e05924f3d9eb441 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/landscape/chat.less @@ -0,0 +1,23 @@ +@import "../colors"; + +@media all and (orientation: landscape) { + #chat { + min-height: 500px; + -webkit-order: 3; + order: 3; + } + + #newMessageInput { + height: 40px; + } + + #sendMessageButton { + height: 40px; + } + + #tabsList { + a { + font-size:12px; + } + } +} diff --git a/labs/meteor-client/app/client/stylesheets/landscape/style.less b/labs/meteor-client/app/client/stylesheets/landscape/style.less new file mode 100755 index 0000000000000000000000000000000000000000..d79c57d7c917cae2e0818c2fac2c0012d8c5441d --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/landscape/style.less @@ -0,0 +1,81 @@ +@import "../colors"; + +@media all and (orientation: landscape) { + .radius(@size: 4px) { + -moz-border-radius: @size; + -webkit-border-radius: @size; + border-radius: @size; + } + + .linear-gradient(@color1, @color2) { + background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */ + background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */ + background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */ + background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */ + } + + body { + height: 100%; + min-width: 768px; + position: absolute; + top: 0; + } + + .myNavbar { + min-width: 768px; + } + + .navbarButton { + height: 50px; + width: 51.2px; + } + + .navbarFiller { + height: 50px; + } + + .navbarSection { + min-width: 256px; + width: 33%; + } + + .navbarTitle { + font-size: 16px; + padding-top: 13px; + width: 30% !important; + } + + .navbarUserButtons { + float: left; + } + + .title { + font-size: 14px; + } + + #main { + -webkit-flex-flow: row; + flex-flow: row; + } + + #collapseButtonSection { + display: none; + } + + .collapseSection { + display: block !important; + } + + .logout-dialog.ui-dialog { + .ui-widget-header { + font-size: 12px; + } + .ui-dialog-content { + font-size: 11px; + } + } + + .logout-dialog.ui-widget-content { + font-size: 10px; + } +} diff --git a/labs/meteor-client/app/client/stylesheets/landscape/users.less b/labs/meteor-client/app/client/stylesheets/landscape/users.less new file mode 100755 index 0000000000000000000000000000000000000000..c3e743a81758adfd22b8055300ec02ff3820b1a7 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/landscape/users.less @@ -0,0 +1,21 @@ +@media all and (orientation: landscape) { + #username { + display:block; + width:70%; + } + + #users { + -webkit-order: 1; + order: 1; + } + + #user-contents { + .userlist { + padding:10px; + } + } + + .userNameEntry { + height: 20px; + } +} diff --git a/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less b/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less new file mode 100755 index 0000000000000000000000000000000000000000..348e6bd75262e6715c53af5ba995708cbd50fec0 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/landscape/whiteboard.less @@ -0,0 +1,6 @@ +@media all and (orientation: landscape) { + #whiteboard { + -webkit-order: 2; + order: 2; + } +} diff --git a/labs/meteor-client/app/client/stylesheets/portrait/chat.less b/labs/meteor-client/app/client/stylesheets/portrait/chat.less new file mode 100755 index 0000000000000000000000000000000000000000..10d0aa52356fb764ad27ac3ca7c470bd0d6f340d --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/portrait/chat.less @@ -0,0 +1,38 @@ +@import "../colors"; + +@media all and (orientation: portrait) { + #chat { + -webkit-order: 2; + order: 2; + min-height: 40%; + max-height: 40%; + } + + .chat { + li { + td { + font-size: 30px; + } + div { + font-size: 15px; + } + } + } + + #newMessageInput { + height: 60px; + font-size: 25px; + } + + #sendMessageButton { + height: 60px; + width: 100px; + font-size: 30px; + } + + #tabsList { + a { + font-size: 25px; + } + } +} diff --git a/labs/meteor-client/app/client/stylesheets/portrait/style.less b/labs/meteor-client/app/client/stylesheets/portrait/style.less new file mode 100755 index 0000000000000000000000000000000000000000..bd58daeea531a0e3f37c86ccd36ab4ec2b500902 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/portrait/style.less @@ -0,0 +1,146 @@ +@import "../colors"; + +@media all and (orientation: portrait) { + .radius(@size: 4px) { + -moz-border-radius: @size; + -webkit-border-radius: @size; + border-radius: @size; + } + + .linear-gradient(@color1, @color2) { + background: -moz-linear-gradient(@color1, @color2); /* For Firefox 3.6 to 15 */ + background: -o-linear-gradient(@color1, @color2); /* For Opera 11.1 to 12.0 */ + background: -webkit-linear-gradient(@color1, @color2); /* For Safari 5.1 to 6.0 */ + background: linear-gradient(@color1, @color2); /* Standard syntax (must be last) */ + } + + body { + position: relative; + top: 15px; + } + + .navbarButton { + height: 100px; + width: 15%; + min-width: 60px; + } + + .navbarTitle { + font-size: 30px; + padding-top: 30px; + padding-left: 5px; + overflow: hidden; + height: 72px; + width: 70%; + white-space: nowrap; + text-overflow: ellipsis; + margin-left: auto; + margin-right: auto; + } + + .title { + font-size: 30px; + } + + #main { + -webkit-flex-flow: column; + flex-flow: column; + flex-direction: column; + min-height: 125%; + max-height: 125%; + } + + .glyphicon { + font-size: 35px; + } + + .myNavbar { + min-width: 630px; + } + + .collapseSection { + display: none; + } + + .logout-dialog.ui-dialog { + .ui-widget-header { + font-size: 40px; + } + } + + .logout-dialog.ui-widget-content { + font-size: 280%; + } + + .ui-dialog-buttonset { + width: 100%; + } + + .ui-dialog-buttonset button { + width: 40%; + margin-left: 5% !important; + margin-right: 5% !important; + } + + /* Sliding menu */ + + .sliding-menu { + width: 15%; + height: 100%; + position: fixed; + top: 0; + left: -15%; + z-index: 1031; + &.sliding-menu-opened { + left: 0px; + } + a { + border-bottom: 1px solid #258ecd; + padding: 1em; + } + } + + .slideSection { + float: left; + margin-top: 101px; + height: 100%; + width: 100%; + } + + .slideButton { + display: block; + width: 100%; + height: calc(~'20% - 20px'); + } + + .slideSection { + margin-bottom: 0.5%; + &.gradientBar { + .linear-gradient(rgb(72,76,85), rgb(65,68,77)); + } + .btn { + .linear-gradient(rgb(72,76,85), rgb(65,68,77)); + border-left: 1px solid extract(@darkGrey, 2); + border-right: 1px solid extract(@darkGrey, 4); + &.navbarIconToggleActive { + background: extract(@darkGrey, 3); + border-bottom: 4px solid extract(@azure, 1); + } + i { + color: extract(@white, 1); + } + } + } + + #darkened-screen { + display: none; + background: black; + opacity: 0.7; + z-index: 1030; + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + } +} diff --git a/labs/meteor-client/app/client/stylesheets/portrait/users.less b/labs/meteor-client/app/client/stylesheets/portrait/users.less new file mode 100755 index 0000000000000000000000000000000000000000..bcc7eed7cb78c6b61645fda88a9c9c6118c4ccaf --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/portrait/users.less @@ -0,0 +1,16 @@ +@media all and (orientation: portrait) { + #users { + -webkit-order: 3; + order: 3; + min-height: 20%; + max-height: 20%; + } + + .userNameEntry { + height: 35px; + font-size: 30px; + strong { + font-size: 25px; + } + } +} diff --git a/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less b/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less new file mode 100755 index 0000000000000000000000000000000000000000..752e668862ccf7c5d261c362f0f2159341bcf142 --- /dev/null +++ b/labs/meteor-client/app/client/stylesheets/portrait/whiteboard.less @@ -0,0 +1,6 @@ +@media all and (orientation: portrait) { + #whiteboard { + -webkit-order: 1; + order: 1; + } +} diff --git a/labs/meteor-client/app/client/views/chat/chat_bar.coffee b/labs/meteor-client/app/client/views/chat/chat_bar.coffee new file mode 100755 index 0000000000000000000000000000000000000000..702b245f09c3ad8252751e7687d8839ca806d62c --- /dev/null +++ b/labs/meteor-client/app/client/views/chat/chat_bar.coffee @@ -0,0 +1,341 @@ +# -------------------------------------------------------------------------------------------------------------------- +# If a function's last line is the statement false that represents the function returning false +# A function such as a click handler will continue along with the propogation and default behaivour if not stopped +# Returning false stops propogation/prevents default. You cannot always use the event object to call these methods +# Because most Meteor event handlers set the event object to the exact context of the event which does not +# allow you to simply call these methods. +# -------------------------------------------------------------------------------------------------------------------- + +@activateBreakLines = (str) -> + if typeof str is 'string' + res = str.replace /\\n/gim, '<br/>' + res = res.replace /\r/gim, '<br/>' + +@detectUnreadChat = -> + #if the current tab is not the same as the tab we just published in + Meteor.Chat.find({}).observe({ + added: (chatMessage) => + findDestinationTab = -> + if chatMessage.message?.chat_type is "PUBLIC_CHAT" + "PUBLIC_CHAT" + else + chatMessage.message?.from_userid + Tracker.autorun (comp) -> + tabsTime = getInSession('tabsRenderedTime') + if tabsTime? and chatMessage.message.from_userid isnt "SYSTEM_MESSAGE" and chatMessage.message.from_time - tabsTime > 0 + populateChatTabs(chatMessage) # check if we need to open a new tab + destinationTab = findDestinationTab() + if destinationTab isnt getInSession "inChatWith" + setInSession 'chatTabs', getInSession('chatTabs').map((tab) -> + tab.gotMail = true if tab.userId is destinationTab + tab + ) + comp.stop() + }) + +# This method returns all messages for the user. It looks at the session to determine whether the user is in +# private or public chat. If true is passed, messages returned are from before the user joined. Else, the messages are from after the user joined +@getFormattedMessagesForChat = -> + chattingWith = getInSession('inChatWith') + if chattingWith is 'PUBLIC_CHAT' # find all public and system messages + return Meteor.Chat.find({'message.chat_type': $in: ["SYSTEM_MESSAGE","PUBLIC_CHAT"]},{sort: {'message.from_time': 1}}).fetch() + else + unless chattingWith is 'OPTIONS' + return Meteor.Chat.find({'message.chat_type': 'PRIVATE_CHAT', $or: [{'message.to_userid': chattingWith},{'message.from_userid': chattingWith}]}).fetch() + +# Scrolls the message container to the bottom. The number of pixels to scroll down is the height of the container +Handlebars.registerHelper "autoscroll", -> + $('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight) + false + +Handlebars.registerHelper "grabChatTabs", -> + if getInSession('chatTabs') is undefined + initTabs = [ + userId: "PUBLIC_CHAT" + name: "Public" + gotMail: false + class: "publicChatTab" + , + userId: "OPTIONS" + name: "Options" + gotMail: false + class: "optionsChatTab" + ] + setInSession 'chatTabs', initTabs + getInSession('chatTabs')[0..3] + +@sendMessage = -> + message = linkify $('#newMessageInput').val() # get the message from the input box + unless (message?.length > 0 and (/\S/.test(message))) # check the message has content and it is not whitespace + return # do nothing if invalid message + + chattingWith = getInSession('inChatWith') + + if chattingWith isnt "PUBLIC_CHAT" + toUsername = Meteor.Users.findOne(userId: chattingWith)?.user.name + + messageForServer = { # construct message for server + "message": message + "chat_type": if chattingWith is "PUBLIC_CHAT" then "PUBLIC_CHAT" else "PRIVATE_CHAT" + "from_userid": getInSession("userId") + "from_username": BBB.getMyUserName() + "from_tz_offset": "240" + "to_username": if chattingWith is "PUBLIC_CHAT" then "public_chat_username" else toUsername + "to_userid": if chattingWith is "PUBLIC_CHAT" then "public_chat_userid" else chattingWith + "from_lang": "en" + "from_time": getTime() + "from_color": "0x000000" + # "from_color": "0x#{getInSession("messageColor")}" + } + + Meteor.call "sendChatMessagetoServer", getInSession("meetingId"), messageForServer, getInSession("userId"), getInSession("authToken") + + $('#newMessageInput').val '' # Clear message box + +Template.chatbar.helpers + getCombinedMessagesForChat: -> + msgs = getFormattedMessagesForChat() + len = msgs?.length # get length of messages + i = 0 + while i < len # Must be a do while, for loop compiles and stores the length of array which can change inside the loop! + if msgs[i].message.from_userid isnt 'System' # skip system messages + j = i+1 # Start looking at messages right after the current one + + while j < len + deleted = false + if msgs[j].message.from_userid isnt 'System' # Ignore system messages + # Check if the time discrepancy between the two messages exceeds window for grouping + if (parseFloat(msgs[j].message.from_time)-parseFloat(msgs[i].message.from_time)) >= 60000 # 60 seconds/1 minute + break # Messages are too far between, so them seperated and stop joining here + + if msgs[i].message.from_userid is msgs[j].message.from_userid # Both messages are from the same user + msgs[i].message.message += "\r#{msgs[j].message.message}" # Combine the messages + msgs.splice(j,1) # Delete the message from the collection + deleted = true + else break # Messages are from different people, move on + # + else break # This is the break point in the chat, don't merge + # + len = msgs.length + ++j if not deleted + # + ++i + len = msgs.length + + msgs + + userExists: -> + if getInSession('inChatWith') in ["PUBLIC_CHAT", "OPTIONS"] + return true + else + return Meteor.Users.findOne({userId: getInSession('inChatWith')})? + +# When chatbar gets rendered, launch the auto-check for unread chat +Template.chatbar.rendered = -> + detectUnreadChat() + +# When message gets rendered, scroll to the bottom +Template.message.rendered = -> + $('#chatbody').scrollTop($('#chatbody')[0]?.scrollHeight) + false + +Template.chatInput.events + 'click #sendMessageButton': (event) -> + $('#sendMessageButton').blur() + sendMessage() + + 'keypress #newMessageInput': (event) -> # user pressed a button inside the chatbox + key = (if event.charCode then event.charCode else (if event.keyCode then event.keyCode else 0)) + + if event.shiftKey and (key is 13) + event.preventDefault() + $("#newMessageInput").append("\r") # Change newline character + return + + if key is 13 # Check for pressing enter to submit message + event.preventDefault() + sendMessage() + $('#newMessageInput').val("") + return false + +Template.chatInput.rendered = -> + $('input[rel=tooltip]').tooltip() + $('button[rel=tooltip]').tooltip() + +Template.extraConversations.events + "click .extraConversation": (event) -> + console.log "extra conversation" + user = @ + console.log user + console.log "#{user.name} #{user.userId}" + # put this conversation in the 3rd position in the chat tabs collection (after public and options) + # Take all the tabs and turn into an array + tabArray = getInSession('chatTabs') + + # find the index of the selected tab + index = do -> + for value, idx in tabArray + if value.userId is user.userId + selected = value + return idx + null + + if index? + # take object + selected = tabArray[index] + + if selected? + # remove it + tabArray.splice(index, 1) + # insert it at the 3rd index + tabArray.splice(2, 0, selected) + # update collection + setInSession 'chatTabs', tabArray + +Template.extraConversations.helpers + getExtraConversations: -> + getInSession('chatTabs')[4..] + + tooManyConversations: -> + return false if getInSession('chatTabs') is undefined + getInSession('chatTabs').length > 4 + +Template.message.helpers + sanitizeAndFormat: (str) -> + if typeof str is 'string' + # First, replace replace all tags with the ascii equivalent (excluding those involved in anchor tags) + res = str.replace(/&/g, '&').replace(/<(?![au\/])/g, '<').replace(/\/([^au])>/g, '$1>').replace(/([^=])"(?!>)/g, '$1"'); + res = toClickable res + res = activateBreakLines res + + toClockTime: (epochTime) -> + if epochTime is null + return "" + local = new Date() + offset = local.getTimezoneOffset() + epochTime = epochTime - offset * 60000 # 1 min = 60 s = 60,000 ms + dateObj = new Date(epochTime) + hours = dateObj.getUTCHours() + minutes = dateObj.getUTCMinutes() + if minutes < 10 + minutes = "0" + minutes + hours + ":" + minutes + +Template.optionsBar.events + 'click .private-chat-user-entry': (event) -> # clicked a user's name to begin private chat + tabs = getInSession('chatTabs') + _this = @ + + # if you are starting a private chat + if tabs.filter((tab) -> tab.userId is _this.userId).length is 0 + userName = Meteor.Users.findOne({userId: _this.userId})?.user?.name + tabs.push {userId: _this.userId, name: userName, gotMail: false, class: 'privateChatTab'} + setInSession 'chatTabs', tabs + + setInSession 'display_chatPane', true + setInSession "inChatWith", _this.userId + $("#newMessageInput").focus() + +Template.optionsBar.helpers + thereArePeopletoChatWith: -> # Subtract 1 for the current user. Returns whether there are other people in the chat + # TODO: Add a check for the count to only include users who are allowed to private chat + (Meteor.Users.find({'meetingId': getInSession("meetingId")}).count()-1) >= 1 + +Template.optionsBar.rendered = -> + $('div[rel=tooltip]').tooltip() + +Template.optionsFontSize.events + "click .fontSizeSelector": (event) -> + selectedFontSize = parseInt(event.target.id) + if selectedFontSize + setInSession "messageFontSize", selectedFontSize + else setInSession "messageFontSize", 12 + +Template.tabButtons.events + 'click .close': (event) -> # user closes private chat + setInSession 'inChatWith', 'PUBLIC_CHAT' + setInSession 'display_chatPane', true + console.log "userId: #{@userId}" + _this = @ + tabs = getInSession('chatTabs') + if tabs.filter((tab) -> tab.userId is _this.userId).length > 0 + tabs = $.grep(tabs, (t) -> t.userId isnt _this.userId) + setInSession 'chatTabs', tabs + + return false # stops propogation/prevents default + + 'click .gotUnreadMail': (event) -> + #chatTabs.update({userId: @userId}, {$set: {gotMail: false}}) + _this = @ + setInSession 'chatTabs', getInSession('chatTabs').map((tab) -> + tab.gotMail = false if tab.userId is _this.userId + tab + ) + + 'click .optionsChatTab': (event) -> + console.log "options" + setInSession "inChatWith", "OPTIONS" + setInSession 'display_chatPane', false + + 'click .privateChatTab': (event) -> + console.log "private:" + console.log @ + setInSession "inChatWith", @userId + setInSession 'display_chatPane', true + + 'click .publicChatTab': (event) -> + console.log "public" + setInSession "inChatWith", "PUBLIC_CHAT" + setInSession 'display_chatPane', true + + 'click .tab': (event) -> + unless getInSession "inChatWith" is "OPTIONS" + $("#newMessageInput").focus() + console.log "tab" + +Template.tabButtons.helpers + hasGotUnreadMailClass: (gotMail) -> + if gotMail + return "gotUnreadMail" + else + return "" + + isTabActive: (userId) -> + if getInSession("inChatWith") is userId + return "active" + + makeSafe: (string) -> + safeString(string) + +Template.tabButtons.rendered = -> + Tracker.autorun (comp) -> + setInSession 'tabsRenderedTime', TimeSync.serverTime() + if getInSession('tabsRenderedTime') isnt undefined + comp.stop() + +# make links received from Flash client clickable in HTML +@toClickable = (str) -> + if typeof str is 'string' + res = str.replace /<a href='event:/gim, "<a target='_blank' href='" + res = res.replace /<a href="event:/gim, '<a target="_blank" href="' + +Template.message.helpers + toClockTime: (epochTime) -> + if epochTime is null + return "" + local = new Date() + offset = local.getTimezoneOffset() + epochTime = epochTime - offset * 60000 # 1 min = 60 s = 60,000 ms + dateObj = new Date(epochTime) + hours = dateObj.getUTCHours() + minutes = dateObj.getUTCMinutes() + if minutes < 10 + minutes = "0" + minutes + hours + ":" + minutes + + sanitizeAndFormat: (str) -> + if typeof str is 'string' + # First, replace replace all tags with the ascii equivalent (excluding those involved in anchor tags) + res = str.replace(/&/g, '&').replace(/<(?![au\/])/g, '<').replace(/\/([^au])>/g, '$1>').replace(/([^=])"(?!>)/g, '$1"'); + res = toClickable res + res = activateBreakLines res diff --git a/labs/meteor-client/app/client/views/chat/chat_bar.html b/labs/meteor-client/app/client/views/chat/chat_bar.html new file mode 100755 index 0000000000000000000000000000000000000000..138c4594a160b6fb27ecf6f19848375107091315 --- /dev/null +++ b/labs/meteor-client/app/client/views/chat/chat_bar.html @@ -0,0 +1,147 @@ +<template name="chatbar"> + <div id="{{id}}" {{visibility name}} class="component"> + <div id="chatbar-contents"> + <h3 class="title gradientBar"> + <span class="glyphicon glyphicon-comment"></span> + {{title}} + {{> extraConversations}} + </h3> + {{>tabButtons}} <!-- Display public/options tabs, and private chat tabs --> + {{#if getInSession "display_chatPane"}} + {{#if userExists}} + <div id="chatbody"> + <ul class="chat"> + {{#each getCombinedMessagesForChat}} + {{#if message}} + <li {{messageFontSize}}>{{> message}}</li> + {{/if}} + {{/each}} + <bottomEntry></bottomEntry> <!-- Intentionally blank, fixes issue of last message being cut off while scrolling --> + </ul> + </div> + <div class="panel-footer">{{> chatInput}}</div> + {{else}} + <div id="chatbody"> + <ul class="chat"> + {{#each getCombinedMessagesForChat}} + {{#if message}} + <li {{messageFontSize}}>{{> message}}</li> + {{/if}} + {{/each}} + <bottomEntry></bottomEntry> <!-- Intentionally blank, fixes issue of last message being cut off while scrolling --> + <li>The user has left</li> + </ul> + </div> + {{/if}} + {{else}} + {{> optionsBar}} + {{/if}} + </div> + </div> +</template> + +<template name="chatInput"> + <div class="chat-input-wrapper"> + <textarea id="newMessageInput" placeholder="Write a message..." rel="tooltip" data-placement="top" title="Write a new message" autofocus></textarea> + <button type="submit" id="sendMessageButton" class="btn" rel="tooltip" data-placement="top" title="Click to send your message"> + Send + </button> + </div> +</template> + +<template name="chatOptions"> + <p>Chat Options:</p> + {{> optionsFontSize}} +</template> + +<template name="extraConversations"> + {{#if tooManyConversations}} + <div id="MoreChatsDrop" class="btn-group"> + <button type="button" id="MoreChatsbutton" class="btn btn-default dropdown-toggle" data-toggle="dropdown">More Chats<span class="caret"></span></button> + <ul class="dropdown-menu extraConversationScrollableMenu" role="menu"> + {{#each getExtraConversations}} + <li class="extraConversation" id="{{safeName name}}"><a href="#">{{safeName name}}</a></li> + {{/each}} + </ul> + </div> + {{/if}} +</template> + +<!-- Displays and styles an individual message in the chat --> +<template name="message"> + <span style="float:left;"> + {{#if message.from_username}} + <span {{messageFontSize}} class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{message.from_username}}"> + <strong>{{message.from_username}}</strong> + </span> + {{/if}} + </span> + <span style="float:right;"> + {{#if message.from_time}} + {{toClockTime message.from_time}} + <span class="glyphicon glyphicon-time"></span> + {{/if}} + </span> + <br/> + <div style="color:{{colourToHex message.from_color}}">{{{sanitizeAndFormat message.message}}}</div> + {{autoscroll}} +</template> + +<!-- Displays the list of options available --> +<template name="optionsBar"> + <div class="optionsBar"> + {{#if thereArePeopletoChatWith}} <!-- There are people we can chat with, display the user list --> + <p>Select a person to chat with privately</p> + <div class="private-chat-user-box" rel="tooltip" data-placement="top" title="Select a participant to open a private chat"> + {{#Layout template="scrollWindow" id="privateChatUserScrollWindow"}} + {{#contentFor region="scrollContents"}} + <div class="private-chat-user-list"> + {{#each getUsersInMeeting}} + <div class="private-chat-user-entry">{{#unless isCurrentUser userId}}{{user.name}}{{/unless}}</div> + {{/each}} + </div> + {{/contentFor}} + {{/Layout}} + </div> + {{else}} + <p>There are no participants to chat with right now.</p> + {{/if}} + <br/> + {{> chatOptions}} + </div> +</template> + +<template name="optionsFontSize"> + <div class="dropdown"> + <span {{messageFontSize}}>Chat Message Font Size: </span> + <button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown"> + Font Size ({{getInSession "messageFontSize"}}) + <span class="caret"></span> + </button> + + <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1"> + <li role="presentation"><a class="fontSizeSelector" id="8" role="menuitem" tabindex="-1" href="#">8</a></li> + <li role="presentation"><a class="fontSizeSelector" id="10" role="menuitem" tabindex="-1" href="#">10</a></li> + <li role="presentation"><a class="fontSizeSelector" id="12" role="menuitem" tabindex="-1" href="#">12</a></li> + <li role="presentation"><a class="fontSizeSelector" id="14" role="menuitem" tabindex="-1" href="#">14</a></li> + <li role="presentation"><a class="fontSizeSelector" id="16" role="menuitem" tabindex="-1" href="#">16</a></li> + <li role="presentation"><a class="fontSizeSelector" id="18" role="menuitem" tabindex="-1" href="#">18</a></li> + </ul> + </div> +</template> + +<!-- Display buttons on the chat tab, public, options, and all the private chat tabs --> +<template name="tabButtons"> + <ul id="tabsList" class="nav nav-tabs"> + {{#each grabChatTabs}} + <li class="{{isTabActive userId}} tab {{makeSafe class}}"> + <a href='#' data-toggle='tab' id="#{{makeSafe name}}" class="{{hasGotUnreadMailClass gotMail}} chatNameSelector"> + {{makeSafe name}} + {{#if equals class "privateChatTab"}} + <button class="close closeTab" type="button"><sup><b>X</b></sup></button> + {{/if}} + </a> + </li> + {{/each}} + </ul> +</template> diff --git a/labs/meteor-client/app/client/views/layout.html b/labs/meteor-client/app/client/views/layout.html new file mode 100755 index 0000000000000000000000000000000000000000..a666cb28a9a2330884a9a544c7dca45b7712e8f2 --- /dev/null +++ b/labs/meteor-client/app/client/views/layout.html @@ -0,0 +1,3 @@ +<template name="layout"> + {{> yield}} +</template> diff --git a/labs/meteor-client/app/client/views/sharedTemplates.html b/labs/meteor-client/app/client/views/sharedTemplates.html new file mode 100755 index 0000000000000000000000000000000000000000..33d44282691ef3531cbb4e58652e0db2b664fae4 --- /dev/null +++ b/labs/meteor-client/app/client/views/sharedTemplates.html @@ -0,0 +1,12 @@ +<template name="scrollWindow"> + {{> yield}} + <div class="ScrollableWindow" id="{{id}}"> + {{> yield region="scrollContents"}} + </div> +</template> + +<template name="makeButton"> + <button type="submit" id="{{id}}" class="{{btn_class}} btn" {{isDisabled}} rel="{{rel}}" data-placement="{{data_placement}}" title="{{title}}"> + <i class="glyphicon glyphicon-{{i_class}}"></i> + </button> +</template> diff --git a/labs/meteor-client/app/client/views/users/user_item.coffee b/labs/meteor-client/app/client/views/users/user_item.coffee new file mode 100755 index 0000000000000000000000000000000000000000..24f33884fef64af9754012f0a27b5f2e730931c6 --- /dev/null +++ b/labs/meteor-client/app/client/views/users/user_item.coffee @@ -0,0 +1,11 @@ +Template.displayUserIcons.events + 'click .muteIcon': (event) -> + toggleMic @ + + 'click .raisedHandIcon': (event) -> + # the function to call 'userLowerHand' + # the meeting id + # the _id of the person whose land is to be lowered + # the userId of the person who is lowering the hand + console.log "lower hand- client click handler" + Meteor.call('userLowerHand', getInSession("meetingId"), @userId, getInSession("userId"), getInSession("authToken")) diff --git a/labs/meteor-client/app/client/views/users/user_item.html b/labs/meteor-client/app/client/views/users/user_item.html new file mode 100755 index 0000000000000000000000000000000000000000..d24f2208abc39a7ada8738398d4c4334b92281c6 --- /dev/null +++ b/labs/meteor-client/app/client/views/users/user_item.html @@ -0,0 +1,72 @@ + + +<template name="displayUserIcons"> + {{#if isUserSharingVideo userId}} + <span class="userListSettingIcon glyphicon glyphicon-facetime-video" rel="tooltip" data-placement="bottom" title="{{user.name}} is sharing their webcam"></span> + {{/if}} + + {{#if isUserSharingAudio userId}} + {{#if isCurrentUser userId}} + {{#if isUserMuted userId}} + <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-off" rel="tooltip" data-placement="bottom" title="Unmute yourself"></span> + {{else}} + {{#if isCurrentUserTalking}} + <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-up" rel="tooltip" data-placement="bottom" title="is talking"></span> + {{else}} + <span class="muteIcon userListSettingIcon glyphicon glyphicon-volume-down" rel="tooltip" data-placement="bottom" title="is not talking"></span> + {{/if}} + {{/if}} + {{else}} + {{#if isUserMuted userId}} + <span class="userListSettingIcon glyphicon glyphicon-volume-off" rel="tooltip" data-placement="bottom" title="{{user.name}} is muted"></span> + {{else}} + {{#if isUserTalking userId}} + <span class="userListSettingIcon glyphicon glyphicon-volume-up" rel="tooltip" data-placement="bottom" title="{{user.name}} is talking"></span> + {{else}} + <span class="userListSettingIcon glyphicon glyphicon-volume-down" rel="tooltip" data-placement="bottom" title="{{user.name}} is not talking"></span> + {{/if}} + {{/if}} + {{/if}} + {{/if}} + + {{#if isUserListenOnly userId}} + <span class="userListSettingIcon glyphicon glyphicon-headphones" title="Listening only"></span> + {{/if}} + + {{#if user.presenter}} + <span class="userListSettingIcon glyphicon glyphicon-picture" rel="tooltip" data-placement="bottom" title="{{user.name}} is the presenter"></span> + {{else}} + {{#if equals user.role "MODERATOR"}} + <span class="userListSettingIcon glyphicon glyphicon-user" rel="tooltip" data-placement="bottom" title="{{user.name}} is a moderator"></span> + {{/if}} + {{/if}} + + {{#if user.raise_hand}} + {{#if isCurrentUser userId}} + <span class="raisedHandIcon userListSettingIcon glyphicon glyphicon-hand-up" rel="tooltip" data-placement="bottom" title="Lower your hand"></span> + {{else}} + <span class="userListSettingIcon glyphicon glyphicon-hand-up" rel="tooltip" data-placement="bottom" title="{{user.name}} has raised their hand"></span> + {{/if}} + {{/if}} +</template> + +<template name="usernameEntry"> + {{#if isCurrentUser userId}} + <span class="userCurrent userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}} (you)"> + {{user.name}} (you) + </span> + {{else}} + <span class="userNameEntry" rel="tooltip" data-placement="bottom" title="{{user.name}}"> + {{user.name}} + </span> + {{/if}} +</template> + +<template name="userItem"> + <div id="username"> + {{> usernameEntry}} + </div> + <div id="usericons"> + {{> displayUserIcons}} + </div> +</template> diff --git a/labs/meteor-client/app/client/views/users/user_list.coffee b/labs/meteor-client/app/client/views/users/user_list.coffee new file mode 100755 index 0000000000000000000000000000000000000000..96d0fbcb2c217db88bb21c2d005db624d661d49a --- /dev/null +++ b/labs/meteor-client/app/client/views/users/user_list.coffee @@ -0,0 +1,3 @@ +Template.usersList.helpers + getMeetingSize: -> # Retreieve the number of users in the chat, or "error" string + return Meteor.Users.find().count() diff --git a/labs/meteor-client/app/client/views/users/users_list.html b/labs/meteor-client/app/client/views/users/users_list.html new file mode 100755 index 0000000000000000000000000000000000000000..97db348ea756ae2a7fde0c00aaaea5bf42c6caa3 --- /dev/null +++ b/labs/meteor-client/app/client/views/users/users_list.html @@ -0,0 +1,19 @@ +<template name="usersList"> + <div id="{{id}}" {{visibility name}} class="component"> + <h3 class="title gradientBar"><span class="glyphicon glyphicon-user"></span> Participants: {{getMeetingSize}} User(s)</h3> + + <div id="user-contents"> + {{#Layout template="scrollWindow" id="publicUserScrollWindow"}} + {{#contentFor region="scrollContents"}} + <div class="userlist"> + {{#each getUsersInMeeting}} + <div id="content"> + {{>userItem}} + </div> + {{/each}} + </div> + {{/contentFor}} + {{/Layout}} + </div> + </div> +</template> diff --git a/labs/meteor-client/app/client/views/whiteboard/slide.coffee b/labs/meteor-client/app/client/views/whiteboard/slide.coffee new file mode 100755 index 0000000000000000000000000000000000000000..6100336af6ba108140f773cf82467d8f00203511 --- /dev/null +++ b/labs/meteor-client/app/client/views/whiteboard/slide.coffee @@ -0,0 +1,115 @@ +Template.slide.rendered = -> + currentSlide = getCurrentSlideDoc() + pic = new Image() + pic.onload = -> + setInSession 'slideOriginalWidth', this.width + setInSession 'slideOriginalHeight', this.height + $(window).resize( -> + redrawWhiteboard() + ) + if window.matchMedia('(orientation: portrait)').matches + $('#whiteboard-paper').height($('#whiteboard-paper').width() * this.height / this.width) + else + # set the slide height to the max available + $('#whiteboard-paper').height($('#whiteboard').height()) + if currentSlide?.slide?.png_uri? + createWhiteboardPaper (wpm) -> + displaySlide wpm + pic.src = currentSlide?.slide?.png_uri + +@createWhiteboardPaper = (callback) => + @whiteboardPaperModel = new Meteor.WhiteboardPaperModel('whiteboard-paper') + callback(@whiteboardPaperModel) + +@displaySlide = (wpm) -> + currentSlide = getCurrentSlideDoc() + wpm.create() + adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight')) + wpm._displayPage(currentSlide?.slide?.png_uri, getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight')) + manuallyDisplayShapes() + wpm.scale(adjustedDimensions.width, adjustedDimensions.height) + +@manuallyDisplayShapes = -> + currentSlide = getCurrentSlideDoc() + wpm = @whiteboardPaperModel + shapes = Meteor.Shapes.find({whiteboardId: currentSlide?.slide?.id}).fetch() + for s in shapes + shapeInfo = s.shape?.shape or s?.shape + shapeType = shapeInfo?.type + + if shapeType isnt "text" + len = shapeInfo.points.length + for num in [0..len] # the coordinates must be in the range 0 to 1 + shapeInfo?.points[num] = shapeInfo?.points[num] / 100 + wpm?.makeShape(shapeType, shapeInfo) + wpm?.updateShape(shapeType, shapeInfo) + + +# calculates and returns the best fitting {width, height} pair +# based on the image's original width and height +@scaleSlide = (originalWidth, originalHeight) -> + + # the size of the whiteboard space (frame) where the slide will be displayed + boardWidth = $("#whiteboard").width() + boardHeight = null # see under + + # calculate boardHeight + whiteboardBottom = $("#whiteboard").offset().top + $("#whiteboard").height() + footerTop = $(".myFooter").offset().top + if footerTop < whiteboardBottom + boardHeight = footerTop - $("#whiteboard").offset().top - $("#whiteboard-navbar").height() - 10 + else + boardHeight = $("#whiteboard").height() - $("#whiteboard-navbar").height() - 10 + + # this is the best fitting pair + adjustedWidth = null + adjustedHeight = null + + + # the slide image is in portrait orientation + if originalWidth <= originalHeight + adjustedWidth = boardHeight * originalWidth / originalHeight + if boardWidth < adjustedWidth + adjustedHeight = boardHeight * boardWidth / adjustedWidth + adjustedWidth = boardWidth + else + adjustedHeight = boardHeight + + # ths slide image is in landscape orientation + else + adjustedHeight = boardWidth * originalHeight / originalWidth + if boardHeight < adjustedHeight + adjustedWidth = boardWidth * boardHeight / adjustedHeight + adjustedHeight = boardHeight + else + adjustedWidth = boardWidth + + { width: adjustedWidth, height: adjustedHeight } + +Template.slide.helpers + updatePointerLocation: (pointer) -> + if whiteboardPaperModel? + wpm = whiteboardPaperModel + wpm?.moveCursor(pointer.x, pointer.y) + +#### SHAPE #### +Template.shape.rendered = -> + # @data is the shape object coming from the {{#each}} in the html file + shapeInfo = @data.shape?.shape or @data.shape + shapeType = shapeInfo?.type + + if shapeType isnt "text" + len = shapeInfo.points.length + for num in [0..len] # the coordinates must be in the range 0 to 1 + shapeInfo.points[num] = shapeInfo.points[num] / 100 + + if whiteboardPaperModel? + wpm = whiteboardPaperModel + wpm?.makeShape(shapeType, shapeInfo) + wpm?.updateShape(shapeType, shapeInfo) + +Template.shape.destroyed = -> + if whiteboardPaperModel? + wpm = whiteboardPaperModel + wpm.clearShapes() + manuallyDisplayShapes() diff --git a/labs/meteor-client/app/client/views/whiteboard/slide.html b/labs/meteor-client/app/client/views/whiteboard/slide.html new file mode 100755 index 0000000000000000000000000000000000000000..8ad52c131d45192053337a18241fa95c3b66d823 --- /dev/null +++ b/labs/meteor-client/app/client/views/whiteboard/slide.html @@ -0,0 +1,9 @@ +<template name="slide"> + {{#each getShapesForSlide}} + {{> shape}} + {{/each}} + {{updatePointerLocation pointerLocation}} +</template> + +<template name="shape"> +</template> diff --git a/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee b/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee new file mode 100755 index 0000000000000000000000000000000000000000..4e801baf776b572e5d26122ee5ae0e1975e8d712 --- /dev/null +++ b/labs/meteor-client/app/client/views/whiteboard/whiteboard.coffee @@ -0,0 +1,12 @@ +@redrawWhiteboard = () -> + if window.matchMedia('(orientation: portrait)').matches + $('#whiteboard').height($('#whiteboard').width() * getInSession('slideOriginalHeight') / getInSession('slideOriginalWidth') + $('#whiteboard-navbar').height() + 10) + else if $('#whiteboard').height() isnt $('#users').height() + 10 + $('#whiteboard').height($('#users').height() + 10) + adjustedDimensions = scaleSlide(getInSession('slideOriginalWidth'), getInSession('slideOriginalHeight')) + wpm = whiteboardPaperModel + wpm.clearShapes() + wpm.clearCursor() + manuallyDisplayShapes() + wpm.scale(adjustedDimensions.width, adjustedDimensions.height) + wpm.createCursor() diff --git a/labs/meteor-client/app/client/views/whiteboard/whiteboard.html b/labs/meteor-client/app/client/views/whiteboard/whiteboard.html new file mode 100755 index 0000000000000000000000000000000000000000..e680882df3d628adfcd2f781b06361c636376ce1 --- /dev/null +++ b/labs/meteor-client/app/client/views/whiteboard/whiteboard.html @@ -0,0 +1,13 @@ +<template name="whiteboard"> + <div id="{{id}}" {{visibility name}} class="component gradientBar"> + <h3 id="whiteboard-navbar" class="title"> + <span class="glyphicon glyphicon-pencil"></span> + {{title}} + </h3> + {{#each getCurrentSlide}} + {{> slide}} + {{/each}} + <div id="whiteboard-paper"> + </div> + </div> +</template> diff --git a/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee b/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee new file mode 100644 index 0000000000000000000000000000000000000000..eb16119a91d5849f53c235bc5bcc3ef8efee7ffd --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/_whiteboard_tool.coffee @@ -0,0 +1,26 @@ +# A base class for whiteboard tools +class @WhiteboardToolModel + + initialize: (@paper) -> + console.log "paper:" + @paper + @gh = 0 + @gw = 0 + @obj = 0 + # the defintion of this shape, kept so we can redraw the shape whenever needed + @definition = [] + + #set the size of the paper + # @param {number} @gh gh parameter + # @param {number} @gw gw parameter + setPaperSize: (@gh, @gw) -> + + setOffsets: (@xOffset, @yOffset) -> + + setPaperDimensions: (@paperWidth, @paperHeight) -> + # TODO: can't we simply take the width and the height from `@paper`? + + getDefinition: () -> + @definition + + hide: () -> + @obj.hide() if @obj? diff --git a/labs/meteor-client/app/client/whiteboard_models/utils.coffee b/labs/meteor-client/app/client/whiteboard_models/utils.coffee new file mode 100755 index 0000000000000000000000000000000000000000..b79c4c4f105ad28bd4aa6593f69f27577b0c48a2 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/utils.coffee @@ -0,0 +1,25 @@ +# General utility methods + +Meteor.methods + # POST request using javascript + # @param {string} path path of submission + # @param {string} params parameters to submit + # @param {string} method method of submission ("post" is default) + # @return {undefined} + postToUrl: (path, params, method) -> + method = method or "post" + form = $("<form></form>") + form.attr + "method" : method, + "action" : path + for key of params + if params.hasOwnProperty(key) + $hiddenField = $ "input" + $hiddenField.attr + "type" : "hidden", + "name" : key, + "value" : params[key] + form.append $hiddenField + + $('body').append form + form.submit() diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee new file mode 100755 index 0000000000000000000000000000000000000000..429a73c0b7e4832be5e823c3483174b06c01d8f6 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_cursor.coffee @@ -0,0 +1,43 @@ +# The cursor/pointer in the whiteboard +class @WhiteboardCursorModel + + constructor: (@paper, @radius=null, @color=null) -> + @radius ?= 6 + @color ?= "#ff6666" # a pinkish red + @cursor = null + @paper + + draw: () => + @cursor = @paper.circle(0, 0, @radius) + @cursor.attr + "fill": @color + "stroke": @color + "opacity": "0.8" + $(@cursor.node).on "mousewheel", _.bind(@_onMouseWheel, @) + + toFront: () -> + @cursor.toFront() if @cursor? + + setRadius: (value) -> + if @cursor? + @cursor.attr r: value + + setPosition: (x, y) -> + if @cursor? + @cursor.attr + cx: x + cy: y + + undrag: () -> + @cursor.undrag() if @cursor? + + drag: (onMove, onStart, onEnd, target=null) -> + if @cursor? + target or= @ + @cursor.drag _.bind(onMove, target), _.bind(onStart, target), _.bind(onEnd, target) + + _onMouseWheel: (e, delta) -> + @trigger("cursor:mousewheel", e, delta) + + remove: () -> + @cursor.remove() \ No newline at end of file diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee new file mode 100755 index 0000000000000000000000000000000000000000..adaa7cfa23e3b644feec467d2fa1c3832ed4f13b --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_ellipse.coffee @@ -0,0 +1,151 @@ +# An ellipse in the whiteboard +class @WhiteboardEllipseModel extends WhiteboardToolModel + + constructor: (@paper) -> + super @paper + + # the defintion of this shape, kept so we can redraw the shape whenever needed + # format: top left x, top left y, bottom right x, bottom right y, stroke color, thickness + @definition = [0, 0, 0, 0, "#000", "0px"] + + # Make an ellipse on the whiteboard + # @param {[type]} x the x value of the top left corner + # @param {[type]} y the y value of the top left corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + make: (info) -> + #console.log "Whiteboard - Making ellipse: " + #console.log info + if info?.points? + x = info.points[0] + y = info.points[1] + color = info.color + thickness = info.thickness + + @obj = @paper.ellipse(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0) + @obj.attr "stroke", formatColor(color) + @obj.attr "stroke-width", zoomStroke(formatThickness(thickness)) + @definition = [x, y, y, x, @obj.attrs["stroke"], @obj.attrs["stroke-width"]] + + @obj + + # Update ellipse drawn + # @param {number} x1 the x value of the top left corner in percent of current slide size + # @param {number} y1 the y value of the top left corner in percent of current slide size + # @param {number} x2 the x value of the bottom right corner in percent of current slide size + # @param {number} y2 the y value of the bottom right corner in percent of current slide size + # @param {boolean} square (draw a circle or not + update: (info) -> + #console.log info + if info?.points? + x1 = info.points[0] + y1 = info.points[1] + x2 = info.points[2] + y2 = info.points[3] + + circle = info.square + + if @obj? + [x1, x2] = [x2, x1] if x2 < x1 + + if y2 < y1 + [y1, y2] = [y2, y1] + reversed = true + + #if the control key is pressed then the width and height of the ellipse are equal (a circle) + #we calculate this by making the y2 coord equal to the y1 coord plus the width of x2-x1 and corrected for the slide size + if circle + if reversed # if reveresed, the y1 coordinate gets updated, not the y2 coordinate + y1 = y2 - (x2 - x1) * @gw / @gh + else + y2 = y1 + (x2 - x1) * @gw / @gh + + coords = + x1: x1 + x2: x2 + y1: y1 + y2: y2 + + #console.log(coords) + + rx = (x2 - x1) / 2 + ry = (y2 - y1) / 2 + + r = + rx: rx * @gw + ry: ry * @gh + cx: (rx + x1) * @gw + @xOffset + cy: (ry + y1) * @gh + @yOffset + + @obj.attr(r) + + #console.log( "@gw: " + @gw + "\n@gh: " + @gh + "\n@xOffset: " + @xOffset + "\n@yOffset: " + @yOffset ); + # we need to update all these values, specially for when shapes are drawn backwards + @definition[0] = x1 + @definition[1] = y1 + @definition[2] = x2 + @definition[3] = y2 + + # Draw an ellipse on the whiteboard + # @param {number} x1 the x value of the top left corner + # @param {number} y1 the y value of the top left corner + # @param {number} x2 the x value of the bottom right corner + # @param {number} y2 the y value of the bottom right corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + draw: (x1, y1, x2, y2, colour, thickness) -> + [x1, x2] = [x2, x1] if x2 < x1 + [y1, y2] = [y2, y1] if y2 < y1 + + rx = (x2 - x1) / 2 + ry = (y2 - y1) / 2 + x = (rx + x1) * @gw + @xOffset + y = (ry + y1) * @gh + @yOffset + elip = @paper.ellipse(x, y, rx * @gw, ry * @gh) + elip.attr Utils.strokeAndThickness(colour, thickness) + elip + + # When first starting drawing the ellipse + # @param {number} x the x value of cursor at the time in relation to the left side of the browser + # @param {number} y the y value of cursor at the time in relation to the top of the browser + # TODO: moved here but not finished + dragOnStart: (x, y) -> + # sx = (@paperWidth - @gw) / 2 + # sy = (@paperHeight - @gh) / 2 + # # find the x and y values in relation to the whiteboard + # @ellipseX = (x - @containerOffsetLeft - sx + @xOffset) + # @ellipseY = (y - @containerOffsetTop - sy + @yOffset) + # globals.connection.emitMakeShape "ellipse", + # [ @ellipseX / @paperWidth, @ellipseY / @paperHeight, @currentColour, @currentThickness ] + + # When first starting to draw an ellipse + # @param {number} dx the difference in the x value at the start as opposed to the x value now + # @param {number} dy the difference in the y value at the start as opposed to the y value now + # @param {number} x the x value of cursor at the time in relation to the left side of the browser + # @param {number} y the y value of cursor at the time in relation to the top of the browser + # @param {Event} e the mouse event + # TODO: moved here but not finished + dragOnMove: (dx, dy, x, y, e) -> + # # if shift is pressed, draw a circle instead of ellipse + # dy = dx if @shiftPressed + # dx = dx / 2 + # dy = dy / 2 + # # adjust for negative values as well + # x = @ellipseX + dx + # y = @ellipseY + dy + # dx = (if dx < 0 then -dx else dx) + # dy = (if dy < 0 then -dy else dy) + # globals.connection.emitUpdateShape "ellipse", + # [ x / @paperWidth, y / @paperHeight, dx / @paperWidth, dy / @paperHeight ] + + # When releasing the mouse after drawing the ellipse + # @param {Event} e the mouse event + # TODO: moved here but not finished + dragOnStop: (e) -> + # attrs = undefined + # attrs = @currentEllipse.attrs if @currentEllipse? + # if attrs? + # globals.connection.emitPublishShape "ellipse", + # [ attrs.cx / @gw, attrs.cy / @gh, attrs.rx / @gw, attrs.ry / @gh, + # @currentColour, @currentThickness ] + # @currentEllipse = null # late updates will be blocked by this diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee new file mode 100755 index 0000000000000000000000000000000000000000..54f7246e47162c674da193600f3e77c2e8262f22 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_line.coffee @@ -0,0 +1,192 @@ +MAX_PATHS_IN_SEQUENCE = 30 + +# A line in the whiteboard +# Note: is used to draw lines from the pencil tool and from the line tool, this is why some +# methods can receive different set of parameters. +# TODO: Maybe this should be split in WhiteboardPathModel for the pencil and +# WhiteboardLineModel for the line tool +class @WhiteboardLineModel extends WhiteboardToolModel + + constructor: (@paper) -> + super @paper + + # the defintion of this shape, kept so we can redraw the shape whenever needed + # format: svg path, stroke color, thickness + @definition = ["", "#000", "0px"] + + # Creates a line in the paper + # @param {number} x the x value of the line start point as a percentage of the original width + # @param {number} y the y value of the line start point as a percentage of the original height + # @param {string} colour the colour of the shape to be drawn + # @param {number} thickness the thickness of the line to be drawn + make: (info) -> + + if info?.points? + x = info.points[0] + y = info.points[1] + color = info.color + thickness = info.thickness + + x1 = x * @gw + @xOffset + y1 = y * @gh + @yOffset + path = "M" + x1 + " " + y1 + " L" + x1 + " " + y1 + pathPercent = "M" + x + " " + y + " L" + x + " " + y + @obj = @paper.path(path) + @obj.attr "stroke", formatColor(color) + @obj.attr "stroke-width", zoomStroke(formatThickness(thickness)) + @obj.attr({"stroke-linejoin": "round"}) + @obj.attr "stroke-linecap", "round" + + @definition = [pathPercent, @obj.attrs["stroke"], @obj.attrs["stroke-width"]] + + @obj + + # Update the line dimensions + # @param {number} x1 1) the x of the first point + # 2) the next x point to be added to the line + # @param {number} y1 1) the y of the first point + # 2) the next y point to be added to the line + # @param {number,boolean} x2 1) the x of the second point + # 2) true if the line should be added to the current line, + # false if it should replace the last point + # @param {number} y2 1) the y of the second point + # 2) undefined + update: (info) -> + + if info?.points? + x1 = info.points[0] + y1 = info.points[1] + x2 = info.points[2] + y2 = info.points[3] + + if @obj? + path = @_buildPath(info.points) + + @definition[0] = path + + path = @_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset) + @obj.attr path: path + + # Draw a line on the paper + # @param {number,string} x1 1) the x value of the first point + # 2) the string path + # @param {number,string} y1 1) the y value of the first point + # 2) the colour + # @param {number} x2 1) the x value of the second point + # 2) the thickness + # @param {number} y2 1) the y value of the second point + # 2) undefined + # @param {string} colour 1) the colour of the shape to be drawn + # 2) undefined + # @param {number} thickness 1) the thickness of the line to be drawn + # 2) undefined + draw: (x1, y1, x2, y2, colour, thickness) -> + + # if the drawing is from the pencil tool, it comes as a path first + # if _.isString(x1) + # colour = y1 + # thickness = x2 + # path = x1 + + # # if the drawing is from the line tool, it comes with two points + # else + # path = @_buildPath(points) + + # line = @paper.path(@_scaleLinePath(path, @gw, @gh, @xOffset, @yOffset)) + # line.attr Utils.strokeAndThickness(colour, thickness) + # line.attr({"stroke-linejoin": "round"}) + # line + + # When dragging for drawing lines starts + # @param {number} x the x value of the cursor + # @param {number} y the y value of the cursor + # TODO: moved here but not finished + dragOnStart: (x, y) -> + # # find the x and y values in relation to the whiteboard + # sx = (@paperWidth - @gw) / 2 + # sy = (@paperHeight - @gh) / 2 + # @lineX = x - @containerOffsetLeft - sx + @xOffset + # @lineY = y - @containerOffsetTop - sy + @yOffset + # values = [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ] + # globals.connection.emitMakeShape "line", values + + # As line drawing drag continues + # @param {number} dx the difference between the x value from _lineDragStart and now + # @param {number} dy the difference between the y value from _lineDragStart and now + # @param {number} x the x value of the cursor + # @param {number} y the y value of the cursor + # TODO: moved here but not finished + dragOnMove: (dx, dy, x, y) -> + # sx = (@paperWidth - @gw) / 2 + # sy = (@paperHeight - @gh) / 2 + # [cx, cy] = @_currentSlideOffsets() + # # find the x and y values in relation to the whiteboard + # @cx2 = x - @containerOffsetLeft - sx + @xOffset + # @cy2 = y - @containerOffsetTop - sy + @yOffset + # if @shiftPressed + # globals.connection.emitUpdateShape "line", [ @cx2 / @paperWidth, @cy2 / @paperHeight, false ] + # else + # @currentPathCount++ + # if @currentPathCount < MAX_PATHS_IN_SEQUENCE + # globals.connection.emitUpdateShape "line", [ @cx2 / @paperHeight, @cy2 / @paperHeight, true ] + # else if @obj? + # @currentPathCount = 0 + # # save the last path of the line + # @obj.attrs.path.pop() + # path = @obj.attrs.path.join(" ") + # @obj.attr path: (path + "L" + @lineX + " " + @lineY) + + # # scale the path appropriately before sending + # pathStr = @obj.attrs.path.join(",") + # globals.connection.emitPublishShape "path", + # [ @_scaleLinePath(pathStr, 1 / @gw, 1 / @gh), + # @currentColour, @currentThickness ] + # globals.connection.emitMakeShape "line", + # [ @lineX / @paperWidth, @lineY / @paperHeight, @currentColour, @currentThickness ] + # @lineX = @cx2 + # @lineY = @cy2 + + # Drawing line has ended + # @param {Event} e the mouse event + # TODO: moved here but not finished + dragOnEnd: (e) -> + # if @obj? + # path = @obj.attrs.path + # @obj = null # any late updates will be blocked by this + # # scale the path appropriately before sending + # globals.connection.emitPublishShape "path", + # [ @_scaleLinePath(path.join(","), 1 / @gw, 1 / @gh), + # @currentColour, @currentThickness ] + + _buildPath: (points) -> + path = "" + + if points and points.length >= 2 + path += "M #{points[0]} #{points[1]}" + i = 2 + + while i < points.length + path += "L#{points[i]} #{points[i + 1]}" + i += 2 + + path += "Z" + path + + # Scales a path string to fit within a width and height of the new paper size + # @param {number} w width of the shape as a percentage of the original width + # @param {number} h height of the shape as a percentage of the original height + # @return {string} the path string after being manipulated to new paper size + _scaleLinePath: (string, w, h, xOffset=0, yOffset=0) -> + path = null + points = string.match(/(\d+[.]?\d*)/g) + len = points.length + j = 0 + + # go through each point and multiply it by the new height and width + while j < len + if j isnt 0 + path += "L" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset) + else + path = "M" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset) + j += 2 + path diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee new file mode 100755 index 0000000000000000000000000000000000000000..d1addb56a438aa4d89fdbb2102a5e98e3af64523 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_paper.coffee @@ -0,0 +1,346 @@ +# "Paper" which is the Raphael term for the entire SVG object on the webpage. +# This class deals with this SVG component only. +class Meteor.WhiteboardPaperModel + + # Container must be a DOM element + constructor: (@container) -> + # a WhiteboardCursorModel + @cursor = null + + # all slides in the presentation indexed by url + @slides = {} + + @panX = null + @panY = null + + @current = {} + + # the slide being shown + @current.slide = null + + # a raphaeljs set with all the shapes in the current slide + @current.shapes = null + # a list of shapes as passed to this client when it receives `all_slides` + # (se we are able to redraw the shapes whenever needed) + @current.shapeDefinitions = [] + + @zoomLevel = 1 + @shiftPressed = false + @currentPathCount = 0 + + @_updateContainerDimensions() + + @zoomObserver = null + + @adjustedWidth = 0 + @adjustedHeight = 0 + + @widthRatio = 100 + @heightRatio = 100 + + # Initializes the paper in the page. + # Can't do these things in initialize() because by then some elements + # are not yet created in the page. + create: -> + # paper is embedded within the div#slide of the page. + # @raphaelObj ?= ScaleRaphael(@container, "900", "500") + + h = $("#"+@container).height() + w = $("#"+@container).width() + + @raphaelObj ?= ScaleRaphael(@container, w, h) + @raphaelObj ?= ScaleRaphael(@container, $container.innerHeight(), $container.innerWidth()) + + @raphaelObj.canvas.setAttribute "preserveAspectRatio", "xMinYMin slice" + + @createCursor() + + if @slides + @rebuild() + else + @slides = {} # if previously loaded + unless navigator.userAgent.indexOf("Firefox") is -1 + @raphaelObj.renderfix() + + @raphaelObj + + # Re-add the images to the paper that are found + # in the slides array (an object of urls and dimensions). + rebuild: -> + @current.slide = null + for url of @slides + if @slides.hasOwnProperty(url) + @addImageToPaper url, @slides[url].getWidth(), @slides[url].getHeight() + + scale: (width, height) -> + @raphaelObj?.changeSize(width, height) + + # Add an image to the paper. + # @param {string} url the URL of the image to add to the paper + # @param {number} width the width of the image (in pixels) + # @param {number} height the height of the image (in pixels) + # @return {Raphael.image} the image object added to the whiteboard + addImageToPaper: (url, width, height) -> + @_updateContainerDimensions() + + # solve for the ratio of what length is going to fit more than the other + max = Math.max(width / @containerWidth, height / @containerHeight) + # fit it all in appropriately + url = @_slideUrl(url) + sw = width / max + sh = height / max + #cx = (@containerWidth / 2) - (width / 2) + #cy = (@containerHeight / 2) - (height / 2) + img = @raphaelObj.image(url, cx = 0, cy = 0, width, height) + + # sw slide width as percentage of original width of paper + # sh slide height as a percentage of original height of paper + # x-offset from top left corner as percentage of original width of paper + # y-offset from top left corner as percentage of original height of paper + @slides[url] = new WhiteboardSlideModel(img.id, url, img, width, height, sw, sh, cx, cy) + + unless @current.slide? + img.toBack() + @current.slide = @slides[url] + else if @current.slide.url is url + img.toBack() + else + img.hide() + + # TODO: other places might also required an update in these dimensions + @_updateContainerDimensions() + + @_updateZoomRatios() + @cursor.setRadius(6 * @widthRatio / 100) + + img + + # Removes all the images from the Raphael paper. + removeAllImagesFromPaper: -> + for url of @slides + if @slides.hasOwnProperty(url) + @raphaelObj.getById(@slides[url]?.getId())?.remove() + #@trigger('paper:image:removed', @slides[url].getId()) # Removes the previous image preventing images from being redrawn over each other repeatedly + @slides = {} + @current.slide = null + + + # Switches the tool and thus the functions that get + # called when certain events are fired from Raphael. + # @param {string} tool the tool to turn on + # @return {undefined} + setCurrentTool: (tool) -> + @currentTool = tool + console.log "setting current tool to", tool + switch tool + when "line" + @cursor.undrag() + @current.line = @_createTool(tool) + @cursor.drag(@current.line.dragOnMove, @current.line.dragOnStart, @current.line.dragOnEnd) + when "rectangle" + @cursor.undrag() + @current.rectangle = @_createTool(tool) + @cursor.drag(@current.rectangle.dragOnMove, @current.rectangle.dragOnStart, @current.rectangle.dragOnEnd) + else + console.log "ERROR: Cannot set invalid tool:", tool + + # Clear all shapes from this paper. + clearShapes: -> + if @current.shapes? + @current.shapes.forEach (element) -> + element.remove() + @currentShapes = [] + @currentShapesDefinitions = [] + @clearCursor() + @createCursor() + + clearCursor: -> + @cursor?.remove() + + createCursor: -> + @cursor = new WhiteboardCursorModel(@raphaelObj) + @cursor.setRadius(6 * @widthRatio / 100) + @cursor.draw() + + # Updated a shape `shape` with the data in `data`. + # TODO: check if the objects exist before calling update, if they don't they should be created + updateShape: (shape, data) -> + @current[shape].update(data) + + # Make a shape `shape` with the data in `data`. + makeShape: (shape, data) -> + data.thickness *= @adjustedWidth / 1000 + + tool = null + + @current[shape] = @_createTool(shape) + toolModel = @current[shape] + tool = @current[shape].make(data) + + if tool? + @current.shapes ?= @raphaelObj.set() + @current.shapes.push(tool) + @current.shapeDefinitions.push(toolModel.getDefinition()) + + # Update the cursor position on screen + # @param {number} x the x value of the cursor as a percentage of the width + # @param {number} y the y value of the cursor as a percentage of the height + moveCursor: (x, y) -> + [cx, cy] = @_currentSlideOffsets() + [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() + @cursor.setPosition(x * slideWidth + cx, y * slideHeight + cy) + + #if the slide is zoomed in then move the cursor based on where the viewBox is looking + if @viewBoxXpos? && @viewBoxYPos? && @viewBoxWidth? && @viewBoxHeight? + @cursor.setPosition( @viewBoxXpos + x * @viewBoxWidth, @viewBoxYPos + y * @viewBoxHeight ) + + zoomAndPan: (widthRatio, heightRatio, xOffset, yOffset) -> + console.log "zoomAndPan #{widthRatio} #{heightRatio} #{xOffset} #{yOffset}" + newX = - xOffset * 2 * @adjustedWidth / 100 + newY = - yOffset * 2 * @adjustedHeight / 100 + newWidth = @adjustedWidth * widthRatio / 100 + newHeight = @adjustedHeight * heightRatio / 100 + @raphaelObj.setViewBox(newX, newY, newWidth, newHeight) # zooms and pans + + setAdjustedDimensions: (width, height) -> + @adjustedWidth = width + @adjustedHeight = height + + # Update the dimensions of the container. + _updateContainerDimensions: -> + #console.log "update Container Dimensions" + + $container = $('#whiteboard-paper') + @containerWidth = $container.innerWidth() + @containerHeight = $container.innerHeight() + + @containerOffsetLeft = $container.offset()?.left + @containerOffsetTop = $container.offset()?.top + + _updateZoomRatios: -> + currentSlideDoc = getCurrentSlideDoc() + @widthRatio = currentSlideDoc.slide.width_ratio + @heightRatio = currentSlideDoc.slide.height_ratio + + # Retrieves an image element from the paper. + # The url must be in the slides array. + # @param {string} url the url of the image (must be in slides array) + # @return {Raphael.image} return the image or null if not found + _getImageFromPaper: (url) -> + if @slides[url] + id = @slides[url].getId() + return @raphaelObj.getById(id) if id? + null + + _currentSlideDimensions: -> + if @current.slide? then @current.slide.getDimensions() else [0, 0] + + _currentSlideOriginalDimensions: -> + if @current.slide? then @current.slide.getOriginalDimensions() else [0, 0] + + _currentSlideOffsets: -> + if @current.slide? then @current.slide.getOffsets() else [0, 0] + + # Wrapper method to create a tool for the whiteboard + _createTool: (type) -> + switch type + when "pencil" + model = WhiteboardLineModel + when "path", "line" + model = WhiteboardLineModel + when "rectangle" + model = WhiteboardRectModel + when "ellipse" + model = WhiteboardEllipseModel + when "triangle" + model = WhiteboardTriangleModel + when "text" + model = WhiteboardTextModel + + if model? + [slideWidth, slideHeight] = @_currentSlideOriginalDimensions() + [xOffset, yOffset] = @_currentSlideOffsets() + [width, height] = @_currentSlideDimensions() + + tool = new model(@raphaelObj) + # TODO: why are the parameters inverted and it works? + tool.setPaperSize(slideHeight, slideWidth) + tool.setOffsets(xOffset, yOffset) + tool.setPaperDimensions(width,height) + tool + else + null + + # Adds the base url (the protocol+server part) to `url` if needed. + _slideUrl: (url) -> + if url?.match(/http[s]?:/) + url + else + console.log "The url '#{url}'' did not match the expected format of: http/s" + #globals.presentationServer + url + + #Changes the currently displayed page/slide (if any) with this one + #@param {data} message object containing the "presentation" object + _displayPage: (data, originalWidth, originalHeight) -> + @removeAllImagesFromPaper() + + @_updateContainerDimensions() + boardWidth = @containerWidth + boardHeight = @containerHeight + + currentSlide = getCurrentSlideDoc() + + # TODO currentSlide undefined in some cases - will check later why + imageWidth = boardWidth * (currentSlide?.slide.width_ratio/100) or boardWidth + imageHeight = boardHeight * (currentSlide?.slide.height_ratio/100) or boardHeight + + # console.log "xBegin: #{xBegin}" + # console.log "xEnd: #{xEnd}" + # console.log "yBegin: #{yBegin}" + # console.log "yEnd: #{yEnd}" + # console.log "boardWidth: #{boardWidth}" + # console.log "boardHeight: #{boardHeight}" + #console.log "imageWidth: #{imageWidth}" + #console.log "imageHeight: #{imageHeight}" + + currentPresentation = Meteor.Presentations.findOne({"presentation.current": true}) + presentationId = currentPresentation?.presentation?.id + currentSlideCursor = Meteor.Slides.find({"presentationId": presentationId, "slide.current": true}) + + if @zoomObserver isnt null + @zoomObserver.stop() + _this = this + @zoomObserver = currentSlideCursor.observe # watching the current slide changes + changed: (newDoc, oldDoc) -> + if originalWidth <= originalHeight + @adjustedWidth = boardHeight * originalWidth / originalHeight + @adjustedHeight = boardHeight + else + @adjustedHeight = boardWidth * originalHeight / originalWidth + @adjustedWidth = boardWidth + + _this.zoomAndPan(newDoc.slide.width_ratio, newDoc.slide.height_ratio, + newDoc.slide.x_offset, newDoc.slide.y_offset) + + oldRatio = (oldDoc.slide.width_ratio + oldDoc.slide.height_ratio) / 2 + newRatio = (newDoc.slide.width_ratio + newDoc.slide.height_ratio) / 2 + + _this?.current?.shapes?.forEach (shape) -> + shape.attr "stroke-width", shape.attr('stroke-width') * oldRatio / newRatio + + _this.cursor.setRadius(6 * newDoc.slide.width_ratio / 100) + + if originalWidth <= originalHeight + # square => boardHeight is the shortest side + @adjustedWidth = boardHeight * originalWidth / originalHeight + $('#whiteboard-paper').width(@adjustedWidth) + @addImageToPaper(data, @adjustedWidth, boardHeight) + @adjustedHeight = boardHeight + else + @adjustedHeight = boardWidth * originalHeight / originalWidth + $('#whiteboard-paper').height(@adjustedHeight) + @addImageToPaper(data, boardWidth, @adjustedHeight) + @adjustedWidth = boardWidth + + @zoomAndPan(currentSlide.slide.width_ratio, currentSlide.slide.height_ratio, + currentSlide.slide.x_offset, currentSlide.slide.y_offset) diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee new file mode 100755 index 0000000000000000000000000000000000000000..90cb4ed2a940b3d8ed7606caac83ce594074b2d2 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_rect.coffee @@ -0,0 +1,145 @@ +# A rectangle in the whiteboard +class @WhiteboardRectModel extends WhiteboardToolModel + constructor: (@paper) -> + super @paper + + # the defintion of this shape, kept so we can redraw the shape whenever needed + # format: x1, y1, x2, y2, stroke color, thickness + @definition = [0, 0, 0, 0, "#000", "0px"] + @paper + + # Creates a rectangle in the paper + # @param {number} x the x value of the top left corner + # @param {number} y the y value of the top left corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + make: (startingData) => + x = startingData.points[0] + y = startingData.points[1] + color = startingData.color + thickness = startingData.thickness + + @obj = @paper.rect(x * @gw + @xOffset, y * @gh + @yOffset, 0, 0, 1) + @obj.attr "stroke", formatColor(color) + @obj.attr "stroke-width", zoomStroke(formatThickness(thickness)) + @definition = + shape: "rect" + data: [x, y, 0, 0, @obj.attrs["stroke"], @obj.attrs["stroke-width"]] + @obj + + # Update the rectangle dimensions + # @param {number} x1 the x value of the top left corner + # @param {number} y1 the y value of the top left corner + # @param {number} x2 the x value of the bottom right corner + # @param {number} y2 the y value of the bottom right corner + # @param {boolean} square (draw a square or not) + update: (startingData) -> + + x1 = startingData.points[0] + y1 = startingData.points[1] + x2 = startingData.points[2] + y2 = startingData.points[3] + + square = startingData.square + if @obj? + [x1, x2] = [x2, x1] if x2 < x1 + + if y2 < y1 + [y1, y2] = [y2, y1] + reversed = true + + if square + if reversed #if reveresed, the y1 coordinate gets updated, not the y2 coordinate + y1 = y2 - (x2 - x1) * @gw / @gh + else + y2 = y1 + (x2 - x1) * @gw / @gh + + x = x1 * @gw + @xOffset + y = y1 * @gh + @yOffset + width = (x2 * @gw + @xOffset) - x + height = (y2 * @gh + @yOffset) - y + #if !square + @obj.attr + x: x + y: y + width: width + height: height + ###else + @obj.attr + x: x + y: y + width: width + height: width### + + # we need to update all these values, specially for when shapes are drawn backwards + @definition.data[0] = x1 + @definition.data[1] = y1 + @definition.data[2] = x2 + @definition.data[3] = y2 + + # Draw a rectangle on the paper + # @param {number} x1 the x value of the top left corner + # @param {number} y1 the y value of the top left corner + # @param {number} x2 the x value of the bottom right corner + # @param {number} y2 the y value of the bottom right corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + draw: (x1, y1, x2, y2, colour, thickness) -> + [x1, x2] = [x2, x1] if x2 < x1 + [y1, y2] = [y2, y1] if y2 < y1 + + x = x1 * @gw + y = y1 * @gh + r = @paper.rect(x + @xOffset, y + @yOffset, (x2 * @gw) - x, (y2 * @gh) - y, 1) + r.attr Meteor.call("strokeAndThickness", colour, thickness) + r + + # Creating a rectangle has started + # @param {number} x the x value of cursor at the time in relation to the left side of the browser + # @param {number} y the y value of cursor at the time in relation to the top of the browser + # TODO: moved here but not finished + dragOnStart: (x, y) -> + # sx = (@paperWidth - @gw) / 2 + # sy = (@paperHeight - @gh) / 2 + # # find the x and y values in relation to the whiteboard + # @cx2 = (x - @containerOffsetLeft - sx + @xOffset) / @paperWidth + # @cy2 = (y - @containerOffsetTop - sy + @yOffset) / @paperHeight + # globals.connection.emitMakeShape "rect", + # [ @cx2, @cy2, @currentColour, @currentThickness ] + + # Adjusting rectangle continues + # @param {number} dx the difference in the x value at the start as opposed to the x value now + # @param {number} dy the difference in the y value at the start as opposed to the y value now + # @param {number} x the x value of cursor at the time in relation to the left side of the browser + # @param {number} y the y value of cursor at the time in relation to the top of the browser + # @param {Event} e the mouse event + # TODO: moved here but not finished + dragOnMove: (dx, dy, x, y, e) -> + # # if shift is pressed, make it a square + # dy = dx if @shiftPressed + # dx = dx / @paperWidth + # dy = dy / @paperHeight + # # adjust for negative values as well + # if dx >= 0 + # x1 = @cx2 + # else + # x1 = @cx2 + dx + # dx = -dx + # if dy >= 0 + # y1 = @cy2 + # else + # y1 = @cy2 + dy + # dy = -dy + # globals.connection.emitUpdateShape "rect", [ x1, y1, dx, dy ] + + # When rectangle finished being drawn + # @param {Event} e the mouse event + # TODO: moved here but not finished + dragOnEnd: (e) -> + # if @obj? + # attrs = @obj.attrs + # if attrs? + # globals.connection.emitPublishShape "rect", + # [ attrs.x / @gw, attrs.y / @gh, attrs.width / @gw, attrs.height / @gh, + # @currentColour, @currentThickness ] + # @obj = null diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee new file mode 100644 index 0000000000000000000000000000000000000000..3a882b8988188f8f480673902c94930debcfd4bc --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_slide.coffee @@ -0,0 +1,21 @@ +# A slide in the whiteboard +class @WhiteboardSlideModel + + # TODO: check if we really need original and display width and heights separate or if they can be the same + constructor: (@id, @url, @img, @originalWidth, @originalHeight, @displayWidth, @displayHeight, @xOffset=0, @yOffset=0) -> + + getWidth: -> @displayWidth + + getHeight: -> @displayHeight + + getOriginalWidth: -> @originalWidth + + getOriginalHeight: -> @originalHeight + + getId: -> @id + + getDimensions: -> [@getWidth(), @getHeight()] + + getOriginalDimensions: -> [@getOriginalWidth(), @getOriginalHeight()] + + getOffsets: -> [@xOffset, @yOffset] diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee new file mode 100644 index 0000000000000000000000000000000000000000..83fad347386e7558959e73b350e4a860e8b39256 --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_text.coffee @@ -0,0 +1,248 @@ +# A text in the whiteboard +class @WhiteboardTextModel extends WhiteboardToolModel + + constructor: (@paper) -> + super @paper + # the defintion of this shape, kept so we can redraw the shape whenever needed + # format: x, y, width, height, colour, fontSize, calcFontSize, text + @definition = [0, 0, 0, 0, "#000", 0, 0, ""] + + # Make a text on the whiteboard + make: (startingData) -> + #console.log "making text:" + JSON.stringify startingData + + x = startingData.x + y = startingData.y + width = startingData.textBoxWidth + height = startingData.textBoxHeight + colour = formatColor(startingData.fontColor) + fontSize = startingData.fontSize + calcFontSize = startingData.calcedFontSize + text = startingData.text + + @definition = + shape: "text" + data: [x, y, width, height, colour, fontSize, calcFontSize, text] + + #calcFontSize = (calcFontSize/100 * @gh) + x = (x * @gw) + @xOffset + y = (y * @gh) + @yOffset + calcFontSize + width = width/100 * @gw + + @obj = @paper.text(x/100, y/100, "") + @obj.attr + "fill": colour + "font-family": "Arial" # TODO: make dynamic + "font-size": calcFontSize + @obj.node.style["text-anchor"] = "start" # force left align + @obj.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different + @obj + + # Update text shape drawn + # @param {object} the object containing the shape info + update: (startingData) -> + #console.log "updating text" + JSON.stringify startingData + + x = startingData.x + y = startingData.y + maxWidth = startingData.textBoxWidth + height = startingData.textBoxHeight + colour = formatColor(startingData.fontColor) + fontSize = startingData.fontSize + calcFontSize = startingData.calcedFontSize + myText = startingData.text + + if @obj? + @definition.data = [x, y, maxWidth, height, colour, fontSize, calcFontSize, myText] + + calcFontSize = (calcFontSize/100 * @gh) + x = (x * @gw)/100 + @xOffset + maxWidth = maxWidth/100 * @gw + + @obj.attr + "fill": colour + "font-family": "Arial" # TODO: make dynamic + "font-size": calcFontSize + cell = @obj.node + while cell? and cell.hasChildNodes() + cell.removeChild(cell.firstChild) + + # used code from textFlow lib http://www.carto.net/papers/svg/textFlow/ + # but had to merge it here because "cell" was bigger than what the stack could take + + #extract and add line breaks for start + dashArray = new Array() + dashFound = true + indexPos = 0 + cumulY = 0 + svgNS = "http://www.w3.org/2000/svg" + while dashFound is true + result = myText.indexOf("-", indexPos) + if result is -1 + #could not find a dash + dashFound = false + else + dashArray.push result + indexPos = result + 1 + #split the text at all spaces and dashes + words = myText.split(/[\s-]/) + line = "" + dy = 0 + curNumChars = 0 + computedTextLength = 0 + myTextNode = undefined + tspanEl = undefined + i = 0 + while i < words.length + word = words[i] + curNumChars += word.length + 1 + if computedTextLength > maxWidth or i is 0 + if computedTextLength > maxWidth + tempText = tspanEl.firstChild.nodeValue + tempText = tempText.slice(0, (tempText.length - words[i - 1].length - 2)) #the -2 is because we also strip off white space + tspanEl.firstChild.nodeValue = tempText + + #alternatively one could use textLength and lengthAdjust, however, currently this is not too well supported in SVG UA's + tspanEl = document.createElementNS(svgNS, "tspan") + tspanEl.setAttributeNS null, "x", x + tspanEl.setAttributeNS null, "dy", dy + myTextNode = document.createTextNode(line) + tspanEl.appendChild myTextNode + cell.appendChild tspanEl + if checkDashPosition(dashArray, curNumChars - 1) + line = word + "-" + else + line = word + " " + line = words[i - 1] + " " + line unless i is 0 + dy = calcFontSize + cumulY += dy + else + if checkDashPosition(dashArray, curNumChars - 1) + line += word + "-" + else + line += word + " " + tspanEl.firstChild.nodeValue = line + computedTextLength = tspanEl.getComputedTextLength() + if i is words.length - 1 + if computedTextLength > maxWidth + tempText = tspanEl.firstChild.nodeValue + tspanEl.firstChild.nodeValue = tempText.slice(0, (tempText.length - words[i].length - 1)) + tspanEl = document.createElementNS(svgNS, "tspan") + tspanEl.setAttributeNS null, "x", x + tspanEl.setAttributeNS null, "dy", dy + myTextNode = document.createTextNode(words[i]) + tspanEl.appendChild myTextNode + cell.appendChild tspanEl + i++ + cumulY + + + #this function checks if there should be a dash at the given position, instead of a blank + checkDashPosition = (dashArray, pos) -> + result = false + i = 0 + while i < dashArray.length + result = true if dashArray[i] is pos + i++ + result + + + # Draw a text on the whiteboard + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + # draw: (x, y, width, height, colour, fontSize, calcFontSize, text) -> + # calcFontSize = (calcFontSize/100 * @gh) + # x = x * @gw + @xOffset + # y = (y * @gh) + @yOffset + calcFontSize + # width = width/100 * @gw + # #colour = Utils.strokeAndThickness(colour)["stroke"] + + + # el = @paper.text(x, y, "") + # el.attr + # fill: Meteor.call("strokeAndThickness",colour, false) + # "font-family": "Arial" # TODO: make dynamic + # "font-size": calcFontSize + # el.node.style["text-anchor"] = "start" # force left align + # el.node.style["textAnchor"] = "start" # for firefox, 'cause they like to be different + # Meteor.call("textFlow", text, el.node, width, x, calcFontSize, false) + # el + + # When first dragging the mouse to create the textbox size + # @param {number} x the x value of cursor at the time in relation to the left side of the browser + # @param {number} y the y value of cursor at the time in relation to the top of the browser + # TODO: moved here but not finished nor tested + # _textStart: (x, y) -> + # [sw, sh] = @_currentSlideDimensions() + # [cx, cy] = @_currentSlideOffsets() + # if @currentText? + # globals.connection.emitPublishShape "text", + # [ @textbox.value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh, + # @textbox.clientWidth, 16, @currentColour, "Arial", 14 ] + # globals.connection.emitTextDone() + # @textbox.value = "" + # @textbox.style.visibility = "hidden" + # @textX = x + # @textY = y + # sx = (@containerWidth - @gw) / 2 + # sy = (@containerHeight - @gh) / 2 + # @cx2 = (x - @containerOffsetLeft - sx + cx) / sw + # @cy2 = (y - @containerOffsetTop - sy + cy) / sh + # @_makeRect @cx2, @cy2, "#000", 1 + # globals.connection.emitMakeShape "rect", [ @cx2, @cy2, "#000", 1 ] + + # Finished drawing the rectangle that the text will fit into + # @param {Event} e the mouse event + # TODO: moved here but not finished nor tested + # _textStop: (e) -> + # @currentRect.hide() if @currentRect? + # [sw, sh] = @_currentSlideDimensions() + # [cx, cy] = @_currentSlideOffsets() + # tboxw = (e.pageX - @textX) + # tboxh = (e.pageY - @textY) + # if tboxw >= 14 or tboxh >= 14 # restrict size + # @textbox.style.width = tboxw * (@gw / sw) + "px" + # @textbox.style.visibility = "visible" + # @textbox.style["font-size"] = 14 + "px" + # @textbox.style["fontSize"] = 14 + "px" # firefox + # @textbox.style.color = @currentColour + # @textbox.value = "" + # sx = (@containerWidth - @gw) / 2 + # sy = (@containerHeight - @gh) / 2 + # x = @textX - @containerOffsetLeft - sx + cx + 1 # 1px random padding + # y = @textY - @containerOffsetTop - sy + cy + # @textbox.focus() + + # # if you click outside, it will automatically sumbit + # @textbox.onblur = (e) => + # if @currentText + # globals.connection.emitPublishShape "text", + # [ @value, @currentText.attrs.x / @gw, @currentText.attrs.y / @gh, + # @textbox.clientWidth, 16, @currentColour, "Arial", 14 ] + # globals.connection.emitTextDone() + # @textbox.value = "" + # @textbox.style.visibility = "hidden" + + # # if user presses enter key, then automatically submit + # @textbox.onkeypress = (e) -> + # if e.keyCode is "13" + # e.preventDefault() + # e.stopPropagation() + # @onblur() + + # # update everyone with the new text at every change + # _paper = @ + # @textbox.onkeyup = (e) -> + # @style.color = _paper.currentColour + # @value = @value.replace(/\n{1,}/g, " ").replace(/\s{2,}/g, " ") + # globals.connection.emitUpdateShape "text", + # [ @value, x / _paper.sw, (y + (14 * (_paper.sh / _paper.gh))) / _paper.sh, + # tboxw * (_paper.gw / _paper.sw), 16, _paper.currentColour, "Arial", 14 ] + + # The server has said the text is finished, + # so set it to null for the next text object + # TODO: moved here but not finished nor tested + # textDone: -> + # if @currentText? + # @currentText = null + # @currentRect.hide() if @currentRect? diff --git a/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee b/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee new file mode 100755 index 0000000000000000000000000000000000000000..e32da4c8c4563d77d3aef03a45faabcde6fdbf1a --- /dev/null +++ b/labs/meteor-client/app/client/whiteboard_models/whiteboard_triangle.coffee @@ -0,0 +1,104 @@ +# A triangle in the whiteboard +class @WhiteboardTriangleModel extends WhiteboardToolModel + + constructor: (@paper) -> + super @paper + + # the defintion of this shape, kept so we can redraw the shape whenever needed + # format: x1, y1, x2, y2, stroke color, thickness + @definition = [0, 0, 0, 0, "#000", "0px"] + + # Make a triangle on the whiteboard + # @param {[type]} x the x value of the top left corner + # @param {[type]} y the y value of the top left corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + make: (info) -> + if info?.points? + x = info.points[0] + y = info.points[1] + color = info.color + thickness = info.thickness + + path = @_buildPath(x, y, x, y, x, y) + @obj = @paper.path(path) + @obj.attr "stroke", formatColor(color) + @obj.attr "stroke-width", zoomStroke(formatThickness(thickness)) + @obj.attr({"stroke-linejoin": "round"}) + + @definition = [x, y, x, y, @obj.attrs["stroke"], @obj.attrs["stroke-width"]] + + @obj + + # Update triangle drawn + # @param {number} x1 the x value of the top left corner + # @param {number} y1 the y value of the top left corner + # @param {number} x2 the x value of the bottom right corner + # @param {number} y2 the y value of the bottom right corner + update: (info) -> + if info?.points? + x1 = info.points[0] + y1 = info.points[1] + x2 = info.points[2] + y2 = info.points[3] + + if @obj? + [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2) + + path = @_buildPath(xTop * @gw + @xOffset, yTop * @gh + @yOffset, + xBottomLeft * @gw + @xOffset, yBottomLeft * @gh + @yOffset, + xBottomRight * @gw + @xOffset, yBottomRight * @gh + @yOffset) + @obj.attr path: path + + @definition[0] = x1 + @definition[1] = y1 + @definition[2] = x2 + @definition[3] = y2 + + # Draw a triangle on the whiteboard + # @param {number} x1 the x value of the top left corner + # @param {number} y1 the y value of the top left corner + # @param {number} x2 the x value of the bottom right corner + # @param {number} y2 the y value of the bottom right corner + # @param {string} colour the colour of the object + # @param {number} thickness the thickness of the object's line(s) + draw: (x1, y1, x2, y2, colour, thickness) -> + [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] = @_getCornersFromPoints(x1, y1, x2, y2) + path = @_buildPath(xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) + path = @_scaleTrianglePath(path, @gw, @gh, @xOffset, @yOffset) + triangle = @paper.path(path) + triangle.attr Utils.strokeAndThickness(colour, thickness) + triangle.attr({"stroke-linejoin": "round"}) + triangle + + _getCornersFromPoints: (x1, y1, x2, y2) -> + xTop = (((x2 - x1) / 2) + x1) + yTop = y1 + xBottomLeft = x1 + yBottomLeft = y2 + xBottomRight = x2 + yBottomRight = y2 + [xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight] + + _buildPath: (xTop, yTop, xBottomLeft, yBottomLeft, xBottomRight, yBottomRight) -> + "M#{xTop},#{yTop},#{xBottomLeft},#{yBottomLeft},#{xBottomRight},#{yBottomRight}z" + + # Scales a triangle path string to fit within a width and height of the new paper size + # @param {number} w width of the shape as a percentage of the original width + # @param {number} h height of the shape as a percentage of the original height + # @return {string} the path string after being manipulated to new paper size + _scaleTrianglePath: (string, w, h, xOffset=0, yOffset=0) -> + path = null + points = string.match(/(\d+[.]?\d*)/g) + len = points.length + j = 0 + + # go through each point and multiply it by the new height and width + path = "M" + while j < len + path += "," unless j is 0 + path += "" + (points[j] * w + xOffset) + "," + (points[j + 1] * h + yOffset) + j += 2 + path + "z" + +WhiteboardTriangleModel diff --git a/labs/meteor-client/app/collections/collections.coffee b/labs/meteor-client/app/collections/collections.coffee new file mode 100755 index 0000000000000000000000000000000000000000..49987e25c01fc64aea070a8367777c523be89c46 --- /dev/null +++ b/labs/meteor-client/app/collections/collections.coffee @@ -0,0 +1,6 @@ +Meteor.Users = new Meteor.Collection("bbb_users") +Meteor.Chat = new Meteor.Collection("bbb_chat") +Meteor.Meetings = new Meteor.Collection("meetings") +Meteor.Presentations = new Meteor.Collection("presentations") +Meteor.Shapes = new Meteor.Collection("shapes") +Meteor.Slides = new Meteor.Collection("slides") diff --git a/labs/meteor-client/app/config.coffee b/labs/meteor-client/app/config.coffee new file mode 100755 index 0000000000000000000000000000000000000000..e918fa4fc47d350f2bd8ebb414fa0dce225e4158 --- /dev/null +++ b/labs/meteor-client/app/config.coffee @@ -0,0 +1,60 @@ +# TODO: should be split on server and client side +# # Global configurations file + +config = {} + +# Default global variables +config.appName = 'BigBlueButton HTML5 Client' +config.bbbServerVersion = '0.9.0' +config.copyrightYear = '2014' +config.dateOfBuild = 'Sept 25, 2014' #TODO +config.defaultWelcomeMessage = 'Welcome to %%CONFNAME%%!\r\rFor help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.\r\rTo join the audio bridge click the headset icon (upper-left hand corner). Use a headset to avoid causing background noise for others.\r\r\r' +config.defaultWelcomeMessageFooter = "This server is running a build of <a href='https://code.google.com/p/bigbluebutton/wiki/090Overview' target='_blank'><u>BigBlueButton #{config.bbbServerVersion}</u></a>." + +config.maxUsernameLength = 30 +config.maxChatLength = 140 + +## Application configurations +config.app = {} + +# server ip +config.app.serverIP = "http://192.168.0.119" +config.app.logOutUrl = "http://192.168.0.119:4000" # TODO temporary + +# port for the HTML5 client +config.app.htmlClientPort = "3000" + +# Configs for redis +config.redis = {} +config.redis.host = "127.0.0.1" +config.redis.post = "6379" +config.redis.timeout = 5000 +config.redis.channels = {} +config.redis.channels.fromBBBApps = "bigbluebutton:from-bbb-apps:*" +config.redis.channels.toBBBApps = {} +config.redis.channels.toBBBApps.pattern = "bigbluebutton:to-bbb-apps:*" +config.redis.channels.toBBBApps.chat = "bigbluebutton:to-bbb-apps:chat" +config.redis.channels.toBBBApps.meeting = "bigbluebutton:to-bbb-apps:meeting" +config.redis.channels.toBBBApps.users = "bigbluebutton:to-bbb-apps:users" +config.redis.channels.toBBBApps.voice = "bigbluebutton:to-bbb-apps:voice" +config.redis.channels.toBBBApps.whiteboard = "bigbluebutton:to-bbb-apps:whiteboard" + +# Logging +config.log = {} + +if Meteor.isServer + config.log.path = if process?.env?.NODE_ENV is "production" + "/var/log/bigbluebutton/bbbnode.log" + else + # logs in the directory immediatly before the meteor app + process.env.PWD + '/../log/development.log' + + # Setting up a logger in Meteor.log + winston = Winston #Meteor.require 'winston' + file = config.log.path + transports = [ new winston.transports.Console(), new winston.transports.File { filename: file } ] + + Meteor.log = new winston.Logger + transports: transports + +Meteor.config = config diff --git a/labs/meteor-client/app/lib/router.coffee b/labs/meteor-client/app/lib/router.coffee new file mode 100755 index 0000000000000000000000000000000000000000..3f811a168143ceece570d6df0190d9a46d22ae3c --- /dev/null +++ b/labs/meteor-client/app/lib/router.coffee @@ -0,0 +1,38 @@ +@Router.configure layoutTemplate: 'layout' + +@Router.map -> + @route "login", + path: "/login" + action: -> + meetingId = @params.query.meeting_id + userId = @params.query.user_id + authToken = @params.query.auth_token + + if meetingId? and userId? and authToken? + Meteor.call("validateAuthToken", meetingId, userId, authToken) + + applyNewSessionVars = -> + setInSession("authToken", authToken) + setInSession("meetingId", meetingId) + setInSession("userId", userId) + Router.go('/') + + clearSessionVar(applyNewSessionVars) + + @route "main", + path: "/" + onBeforeAction: -> + console.log "in main. onBeforeAction" + authToken = getInSession 'authToken' + meetingId = getInSession 'meetingId' + userId = getInSession 'userId' + console.log "currently #{authToken} #{meetingId} #{userId}" + Meteor.subscribe 'chat', meetingId, userId, authToken, -> + Meteor.subscribe 'shapes', meetingId, -> + Meteor.subscribe 'slides', meetingId, -> + Meteor.subscribe 'meetings', meetingId, -> + Meteor.subscribe 'presentations', meetingId, -> + Meteor.subscribe 'users', meetingId, userId, authToken, -> + console.log "done subscribing" + onLoadComplete() + @render('main') diff --git a/labs/meteor-client/app/packages.json b/labs/meteor-client/app/packages.json new file mode 100644 index 0000000000000000000000000000000000000000..077404aaa415171f6d7c021d41143c361ea058f4 --- /dev/null +++ b/labs/meteor-client/app/packages.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/labs/meteor-client/app/server/bbblogger.coffee b/labs/meteor-client/app/server/bbblogger.coffee new file mode 100755 index 0000000000000000000000000000000000000000..462d683c1ea6da63bb30e30a8837c4d12d847272 --- /dev/null +++ b/labs/meteor-client/app/server/bbblogger.coffee @@ -0,0 +1,17 @@ +### +bunyan = Meteor.require 'bunyan' + +logger = bunyan.createLogger({ + name: 'bbbnode', + streams: [ + { + level: 'debug', + stream: process.stdout, + }, + { + level: 'info', + path: Meteor.config.log.path + } + ] +}) +### diff --git a/labs/meteor-client/app/server/collection_methods/chat.coffee b/labs/meteor-client/app/server/collection_methods/chat.coffee new file mode 100755 index 0000000000000000000000000000000000000000..66727f023c344fed4010e1b4b7c427dfe4f7d388 --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/chat.coffee @@ -0,0 +1,76 @@ +Meteor.methods + # meetingId: the id of the meeting + # chatObject: the object including info on the chat message, including the text + # requesterUserId: the userId of the user sending chat + # requesterToken: the authToken of the requester + sendChatMessagetoServer: (meetingId, chatObject, requesterUserId, requesterToken) -> + chatType = chatObject.chat_type + recipient = chatObject.to_userid + eventName = null + action = -> + if chatType is "PUBLIC_CHAT" + eventName = "send_public_chat_message_request" + return 'chatPublic' + else + eventName = "send_private_chat_message_request" + if recipient is requesterUserId + return 'chatSelf' #not allowed + else + return 'chatPrivate' + + if isAllowedTo(action(), meetingId, requesterUserId, requesterToken) and chatObject.from_userid is requesterUserId + message = + header : + timestamp: new Date().getTime() + name: eventName + payload: + message: chatObject + meeting_id: meetingId + requester_id: chatObject.from_userid + + Meteor.log.info "publishing chat to redis" + publish Meteor.config.redis.channels.toBBBApps.chat, message + return + + deletePrivateChatMessages: (userId, contact_id) -> + # if authorized pass through + requester = Meteor.Users.findOne({userId: userId}) + contact = Meteor.Users.findOne({_id: contact_id}) + deletePrivateChatMessages(requester.userId, contact.userId) +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- +@addChatToCollection = (meetingId, messageObject) -> + # manually convert time from 1.408645053653E12 to 1408645053653 if necessary + # (this is the time_from that the Flash client outputs) + messageObject.from_time = (messageObject.from_time).toString().split('.').join("").split("E")[0] + + if messageObject.from_userid? and messageObject.to_userid? + entry = + meetingId: meetingId + message: + chat_type: messageObject.chat_type + message: messageObject.message + to_username: messageObject.to_username + from_tz_offset: messageObject.from_tz_offset + from_color: messageObject.from_color + to_userid: messageObject.to_userid + from_userid: messageObject.from_userid + from_time: messageObject.from_time + from_username: messageObject.from_username + from_lang: messageObject.from_lang + + id = Meteor.Chat.insert(entry) + Meteor.log.info "added chat id=[#{id}]:#{messageObject.message}." #" Chat.size is now #{Meteor.Chat.find({meetingId: meetingId}).count()}" + + +# called on server start and meeting end +@clearChatCollection = (meetingId) -> + if meetingId? + Meteor.Chat.remove({meetingId: meetingId}, Meteor.log.info "cleared Chat Collection (meetingId: #{meetingId}!") + else + Meteor.Chat.remove({}, Meteor.log.info "cleared Chat Collection (all meetings)!") + +# -------------------------------------------------------------------------------------------- +# end Private methods on server +# -------------------------------------------------------------------------------------------- diff --git a/labs/meteor-client/app/server/collection_methods/meetings.coffee b/labs/meteor-client/app/server/collection_methods/meetings.coffee new file mode 100755 index 0000000000000000000000000000000000000000..348d08bd74bd4ae5076e04b3ee024737b3c97a8f --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/meetings.coffee @@ -0,0 +1,47 @@ +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- +@addMeetingToCollection = (meetingId, name, intendedForRecording, voiceConf, duration) -> + #check if the meeting is already in the collection + unless Meteor.Meetings.findOne({meetingId: meetingId})? + currentlyBeingRecorded = false # defaut value + id = Meteor.Meetings.insert( + meetingId: meetingId, + meetingName: name, + intendedForRecording: intendedForRecording, + currentlyBeingRecorded: currentlyBeingRecorded, + voiceConf: voiceConf, + duration: duration) + Meteor.log.info "added meeting _id=[#{id}]:meetingId=[#{meetingId}]:name=[#{name}]:duration=[#{duration}]:voiceConf=[#{voiceConf}]." + + +@clearMeetingsCollection = (meetingId) -> + if meetingId? + Meteor.Meetings.remove({meetingId: meetingId}, Meteor.log.info "cleared Meetings Collection (meetingId: #{meetingId}!") + else + Meteor.Meetings.remove({}, Meteor.log.info "cleared Meetings Collection (all meetings)!") + + +@removeMeetingFromCollection = (meetingId) -> + if Meteor.Meetings.findOne({meetingId: meetingId})? + Meteor.log.info "end of meeting #{meetingId}. Clear the meeting data from all collections" + # delete all users in the meeting + clearUsersCollection(meetingId) + + # delete all slides in the meeting + clearSlidesCollection(meetingId) + + # delete all shapes in the meeting + clearShapesCollection(meetingId) + + # delete all presentations in the meeting + clearPresentationsCollection(meetingId) + + # delete all chat messages in the meeting + clearChatCollection(meetingId) + + # delete the meeting + clearMeetingsCollection(meetingId) +# -------------------------------------------------------------------------------------------- +# end Private methods on server +# -------------------------------------------------------------------------------------------- diff --git a/labs/meteor-client/app/server/collection_methods/presentations.coffee b/labs/meteor-client/app/server/collection_methods/presentations.coffee new file mode 100755 index 0000000000000000000000000000000000000000..036ed708aff293f790de2ecd256feba29665fd5d --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/presentations.coffee @@ -0,0 +1,38 @@ +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- +@addPresentationToCollection = (meetingId, presentationObject) -> + #check if the presentation is already in the collection + unless Meteor.Presentations.findOne({meetingId: meetingId, 'presentation.id': presentationObject.id})? + entry = + meetingId: meetingId + presentation: + id: presentationObject.id + name: presentationObject.name + current: presentationObject.current + + pointer: #initially we have no data about the cursor + x: 0.0 + y: 0.0 + + id = Meteor.Presentations.insert(entry) + #Meteor.log.info "presentation added id =[#{id}]:#{presentationObject.id} in #{meetingId}. Presentations.size is now #{Meteor.Presentations.find({meetingId: meetingId}).count()}" + +@removePresentationFromCollection = (meetingId, presentationId) -> + if meetingId? and presentationId? and Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId})? + id = Meteor.Presentations.findOne({meetingId: meetingId, "presentation.id": presentationId}) + if id? + Meteor.Presentations.remove(id._id) + Meteor.log.info "----removed presentation[" + presentationId + "] from " + meetingId + + +# called on server start and meeting end +@clearPresentationsCollection = (meetingId) -> + if meetingId? + Meteor.Presentations.remove({meetingId: meetingId}, Meteor.log.info "cleared Presentations Collection (meetingId: #{meetingId}!") + else + Meteor.Presentations.remove({}, Meteor.log.info "cleared Presentations Collection (all meetings)!") + +# -------------------------------------------------------------------------------------------- +# end Private methods on server +# -------------------------------------------------------------------------------------------- diff --git a/labs/meteor-client/app/server/collection_methods/shapes.coffee b/labs/meteor-client/app/server/collection_methods/shapes.coffee new file mode 100755 index 0000000000000000000000000000000000000000..2f73effb9d3ae8ac9bca7336a8a38a00cd437efd --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/shapes.coffee @@ -0,0 +1,97 @@ +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- +@addShapeToCollection = (meetingId, whiteboardId, shapeObject) -> + if shapeObject?.shape_type is "text" + Meteor.log.info "we are dealing with a text shape and the event is:#{shapeObject.status}" + + entry = + meetingId: meetingId + whiteboardId: whiteboardId + shape: + type: shapeObject.shape.type + textBoxHeight: shapeObject.shape.textBoxHeight + backgroundColor: shapeObject.shape.backgroundColor + fontColor: shapeObject.shape.fontColor + status: shapeObject.shape.status + dataPoints: shapeObject.shape.dataPoints + x: shapeObject.shape.x + textBoxWidth: shapeObject.shape.textBoxWidth + whiteboardId: shapeObject.shape.whiteboardId + fontSize: shapeObject.shape.fontSize + id: shapeObject.shape.id + y: shapeObject.shape.y + calcedFontSize: shapeObject.shape.calcedFontSize + text: shapeObject.shape.text + background: shapeObject.shape.background + + if shapeObject.status is "textEdited" or shapeObject.status is "textPublished" + # only keep the final version of the text shape + removeTempTextShape = (callback) -> + Meteor.Shapes.remove({'shape.id':shapeObject.shape.id}) + # for s in Meteor.Shapes.find({'shape.id':shapeObject.shape.id}).fetch() + # Meteor.log.info "there is this shape: #{s.shape.text}" + callback() + + removeTempTextShape( -> + # display as the prestenter is typing + id = Meteor.Shapes.insert(entry) + Meteor.log.info "#{shapeObject.status} substituting the temp shapes with the newer one" + ) + + else + # the mouse button was released - the drawing is complete + # TODO: pencil messages currently don't send draw_end and are labeled all as DRAW_START + if shapeObject?.status is "DRAW_END" or (shapeObject?.status is "DRAW_START" and shapeObject?.shape_type is "pencil") + entry = + meetingId: meetingId + whiteboardId: whiteboardId + shape: + wb_id: shapeObject.wb_id + shape_type: shapeObject.shape_type + status: shapeObject.status + id: shapeObject.id + shape: + type: shapeObject.shape.type + status: shapeObject.shape.status + points: shapeObject.shape.points + whiteboardId: shapeObject.shape.whiteboardId + id: shapeObject.shape.id + square: shapeObject.shape.square + transparency: shapeObject.shape.transparency + thickness: shapeObject.shape.thickness + color: shapeObject.shape.color + + id = Meteor.Shapes.insert(entry) + +@removeAllShapesFromSlide = (meetingId, whiteboardId) -> + Meteor.log.info "removeAllShapesFromSlide__" + whiteboardId + if meetingId? and whiteboardId? and Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId})? + shapesOnSlide = Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch() + Meteor.log.info "number of shapes:" + shapesOnSlide.length + for s in shapesOnSlide + Meteor.log.info "shape=" + s.shape.id + id = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": s.shape.id}) + if id? + Meteor.Shapes.remove(id._id) + Meteor.log.info "----removed shape[" + s.shape.id + "] from " + whiteboardId + Meteor.log.info "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).fetch().length + +@removeShapeFromSlide = (meetingId, whiteboardId, shapeId) -> + shapeToRemove = Meteor.Shapes.findOne({meetingId: meetingId, whiteboardId: whiteboardId, "shape.id": shapeId}) + if meetingId? and whiteboardId? and shapeId? and shapeToRemove? + Meteor.Shapes.remove(shapeToRemove._id) + Meteor.log.info "----removed shape[" + shapeId + "] from " + whiteboardId + Meteor.log.info "remaining shapes on the slide:" + Meteor.Shapes.find({meetingId: meetingId, whiteboardId: whiteboardId}).count() + + +# called on server start and meeting end +@clearShapesCollection = (meetingId) -> + if meetingId? + Meteor.Shapes.remove({meetingId: meetingId}, Meteor.log.info "cleared Shapes Collection (meetingId: #{meetingId}!") + else + Meteor.Shapes.remove({}, Meteor.log.info "cleared Shapes Collection (all meetings)!") + +# -------------------------------------------------------------------------------------------- +# end Private methods on server +# -------------------------------------------------------------------------------------------- diff --git a/labs/meteor-client/app/server/collection_methods/slides.coffee b/labs/meteor-client/app/server/collection_methods/slides.coffee new file mode 100755 index 0000000000000000000000000000000000000000..04128babbe5e937c55abf4b3ef9589753b5754dc --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/slides.coffee @@ -0,0 +1,52 @@ +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- +@displayThisSlide = (meetingId, newSlideId, slideObject) -> + presentationId = newSlideId.split("/")[0] # grab the presentationId part of the slideId + # change current to false for the old slide + Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"slide.current": false}}) + # for the new slide: remove the version which came with presentation_shared_message from the Collection + # to avoid using old data (this message contains everything we need for the new slide) + removeSlideFromCollection(meetingId, newSlideId) + # add the new slide to the collection + addSlideToCollection(meetingId, presentationId, slideObject) + + +@addSlideToCollection = (meetingId, presentationId, slideObject) -> + unless Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideObject.id})? + entry = + meetingId: meetingId + presentationId: presentationId + slide: + height_ratio: slideObject.height_ratio + y_offset: slideObject.y_offset + num: slideObject.num + x_offset: slideObject.x_offset + current: slideObject.current + png_uri: slideObject.png_uri + txt_uri: slideObject.txt_uri + id: slideObject.id + width_ratio: slideObject.width_ratio + swf_uri: slideObject.swf_uri + thumb_uri: slideObject.thumb_uri + + id = Meteor.Slides.insert(entry) + #Meteor.log.info "added slide id =[#{id}]:#{slideObject.id} in #{meetingId}. Now there are #{Meteor.Slides.find({meetingId: meetingId}).count()} slides in the meeting" + +@removeSlideFromCollection = (meetingId, slideId) -> + if meetingId? and slideId? and Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId})? + id = Meteor.Slides.findOne({meetingId: meetingId, "slide.id": slideId}) + if id? + Meteor.Slides.remove(id._id) + Meteor.log.info "----removed slide[" + slideId + "] from " + meetingId + +# called on server start and meeting end +@clearSlidesCollection = (meetingId) -> + if meetingId? + Meteor.Slides.remove({meetingId: meetingId}, Meteor.log.info "cleared Slides Collection (meetingId: #{meetingId}!") + else + Meteor.Slides.remove({}, Meteor.log.info "cleared Slides Collection (all meetings)!") + +# -------------------------------------------------------------------------------------------- +# end Private methods on server +# -------------------------------------------------------------------------------------------- diff --git a/labs/meteor-client/app/server/collection_methods/users.coffee b/labs/meteor-client/app/server/collection_methods/users.coffee new file mode 100755 index 0000000000000000000000000000000000000000..5843d9c56701c9622be1702a24009e268c32c56b --- /dev/null +++ b/labs/meteor-client/app/server/collection_methods/users.coffee @@ -0,0 +1,270 @@ +# -------------------------------------------------------------------------------------------- +# Public methods on server +# All these method must first authenticate the user before it calls the private function counterpart below +# which sends the request to bbbApps. If the method is modifying the media the current user is sharing, +# you should perform the request before sending the request to bbbApps. This allows the user request to be performed +# immediately, since they do not require permission for things such as muting themsevles. +# -------------------------------------------------------------------------------------------- +Meteor.methods + # meetingId: the meetingId of the meeting the user[s] is in + # toMuteUserId: the userId of the user to be [un]muted + # requesterUserId: the userId of the requester + # requesterToken: the authToken of the requester + # mutedBoolean: true for muting, false for unmuting + muteUser: (meetingId, toMuteUserId, requesterUserId, requesterToken, mutedBoolean) -> + action = -> + if mutedBoolean + if toMuteUserId is requesterUserId + return 'muteSelf' + else + return 'muteOther' + else + if toMuteUserId is requesterUserId + return 'unmuteSelf' + else + return 'unmuteOther' + + if isAllowedTo(action(), meetingId, requesterUserId, requesterToken) + message = + payload: + userid: toMuteUserId + meeting_id: meetingId + mute: mutedBoolean + requester_id: requesterUserId + header: + timestamp: new Date().getTime() + name: "mute_user_request" + version: "0.0.1" + + Meteor.log.info "publishing a user mute #{mutedBoolean} request for #{toMuteUserId}" + + publish Meteor.config.redis.channels.toBBBApps.voice, message + updateVoiceUser meetingId, {'web_userid': toMuteUserId, talking:false, muted:mutedBoolean} + return + + # meetingId: the meetingId which both users are in + # toLowerUserId: the userid of the user to have their hand lowered + # loweredByUserId: userId of person lowering + # loweredByToken: the authToken of the requestor + userLowerHand: (meetingId, toLowerUserId, loweredByUserId, loweredByToken) -> + action = -> + if toLowerUserId is loweredByUserId + return 'lowerOwnHand' + else + return 'lowerOthersHand' + + if isAllowedTo(action(), meetingId, loweredByUserId, loweredByToken) + message = + payload: + userid: toLowerUserId + meeting_id: meetingId + raise_hand: false + lowered_by: loweredByUserId + header: + timestamp: new Date().getTime() + name: "user_lowered_hand_message" + version: "0.0.1" + + # publish to pubsub + publish Meteor.config.redis.channels.toBBBApps.users, message + return + + # meetingId: the meetingId which both users are in + # toRaiseUserId: the userid of the user to have their hand lowered + # raisedByUserId: userId of person lowering + # raisedByToken: the authToken of the requestor + userRaiseHand: (meetingId, toRaiseUserId, raisedByUserId, raisedByToken) -> + action = -> + if toRaiseUserId is raisedByUserId + return 'raiseOwnHand' + else + return 'raiseOthersHand' + + if isAllowedTo(action(), meetingId, raisedByUserId, raisedByToken) + message = + payload: + userid: toRaiseUserId + meeting_id: meetingId + raise_hand: false + lowered_by: raisedByUserId + header: + timestamp: new Date().getTime() + name: "user_raised_hand_message" + version: "0.0.1" + + # publish to pubsub + publish Meteor.config.redis.channels.toBBBApps.users, message + return + + # meetingId: the meeting where the user is + # userId: the userid of the user logging out + # authToken: the authToken of the user + userLogout: (meetingId, userId, authToken) -> + if isAllowedTo('logoutSelf', meetingId, userId, authToken) + Meteor.log.info "a user is logging out from #{meetingId}:" + userId + requestUserLeaving meetingId, userId + +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- + +# Only callable from server +# Received information from BBB-Apps that a user left +# Need to update the collection +# params: meetingid, userid as defined in BBB-Apps +@markUserOffline = (meetingId, userId) -> + # mark the user as offline. remove from the collection on meeting_end #TODO + Meteor.log.info "marking user [#{userId}] as offline in meeting[#{meetingId}]" + Meteor.Users.update({'meetingId': meetingId, 'userId': userId}, {$set:{'user.connection_status':'offline'}}) + + +# Corresponds to a valid action on the HTML clientside +# After authorization, publish a user_leaving_request in redis +# params: meetingid, userid as defined in BBB-App +@requestUserLeaving = (meetingId, userId) -> + if Meteor.Users.findOne({'meetingId': meetingId, 'userId': userId})? + message = + payload: + meeting_id: meetingId + userid: userId + header: + timestamp: new Date().getTime() + name: "user_leaving_request" + version: "0.0.1" + + if userId? and meetingId? + Meteor.log.info "sending a user_leaving_request for #{meetingId}:#{userId}" + publish Meteor.config.redis.channels.toBBBApps.users, message + else + Meteor.log.info "did not have enough information to send a user_leaving_request" + +#update a voiceUser - a helper method +@updateVoiceUser = (meetingId, voiceUserObject) -> + u = Meteor.Users.findOne userId: voiceUserObject.web_userid + if u? + if voiceUserObject.talking? + Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.talking':voiceUserObject.talking}}) # talking + if voiceUserObject.joined? + Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.joined':voiceUserObject.joined}}) # joined + if voiceUserObject.locked? + Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.locked':voiceUserObject.locked}}) # locked + if voiceUserObject.muted? + Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.voiceUser.muted':voiceUserObject.muted}}) # muted + if voiceUserObject.listenOnly? + Meteor.Users.update({meetingId: meetingId ,userId: voiceUserObject.web_userid}, {$set: {'user.listenOnly':voiceUserObject.listenOnly}}) # muted + else + Meteor.log.info "ERROR! did not find such voiceUser!" + +@userJoined = (meetingId, user) -> + userId = user.userid + + u = Meteor.Users.findOne({userId:user.userid, meetingId: meetingId}) + # the collection already contains an entry for this user because + # we added a dummy user on register_user_message (to save authToken) + if u? + Meteor.log.info "UPDATING USER #{user.userid}, authToken=#{u.authToken}" + Meteor.Users.update({userId:user.userid, meetingId: meetingId}, {$set:{ + user: + userid: user.userid + presenter: user.presenter + name: user.name + _sort_name: user.name.toLowerCase() + phone_user: user.phone_user + raise_hand: user.raise_hand + has_stream: user.has_stream + role: user.role + listenOnly: user.listenOnly + extern_userid: user.extern_userid + permissions: user.permissions + locked: user.locked + time_of_joining: user.timeOfJoining + connection_status: "online" # TODO consider other default value + voiceUser: + web_userid: user.voiceUser.web_userid + callernum: user.voiceUser.callernum + userid: user.voiceUser.userid + talking: user.voiceUser.talking + joined: user.voiceUser.joined + callername: user.voiceUser.callername + locked: user.voiceUser.locked + muted: user.voiceUser.muted + webcam_stream: user.webcam_stream + }}) + + welcomeMessage = Meteor.config.defaultWelcomeMessage + .replace /%%CONFNAME%%/, Meteor.Meetings.findOne({meetingId: meetingId})?.meetingName + welcomeMessage = welcomeMessage + Meteor.config.defaultWelcomeMessageFooter + + # store the welcome message in chat for easy display on the client side + chatId = Meteor.Chat.upsert({'message.chat_type':"SYSTEM_MESSAGE", 'message.to_userid': userId, meetingId: meetingId}, + {$set:{ + meetingId: meetingId + 'message.chat_type': "SYSTEM_MESSAGE" + 'message.message': welcomeMessage + 'message.from_color': '0x3399FF' + 'message.to_userid': userId + 'message.from_userid': "SYSTEM_MESSAGE" + 'message.from_username': "" + 'message.from_time': user.timeOfJoining.toString() + }}) + Meteor.log.info "added a system message in chat for user #{userId}" + + else + # scenario: there are meetings running at the time when the meteor + # process starts. As a result we the get_users_reply message contains + # users for which we have not observed user_registered_message and + # hence we do not have the auth_token. There will be permission issues + # as the server collection does not have the auth_token of such users + # and cannot authorize their client side actions + Meteor.log.info "NOTE: got user_joined_message " + entry = + meetingId: meetingId + userId: userId + user: + userid: user.userid + presenter: user.presenter + name: user.name + _sort_name: user.name.toLowerCase() + phone_user: user.phone_user + raise_hand: user.raise_hand + has_stream: user.has_stream + role: user.role + listenOnly: user.listenOnly + extern_userid: user.extern_userid + permissions: user.permissions + locked: user.locked + time_of_joining: user.timeOfJoining + connection_status: "" # TODO consider other default value + voiceUser: + web_userid: user.voiceUser.web_userid + callernum: user.voiceUser.callernum + userid: user.voiceUser.userid + talking: user.voiceUser.talking + joined: user.voiceUser.joined + callername: user.voiceUser.callername + locked: user.voiceUser.locked + muted: user.voiceUser.muted + webcam_stream: user.webcam_stream + + id = Meteor.Users.insert(entry) + Meteor.log.info "joining user id=[#{id}]:#{user.name}. Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}" + +@createDummyUser = (meetingId, user) -> + if Meteor.Users.findOne({userId:user.userid, meetingId: meetingId})? + Meteor.log.info "ERROR!! CAN'T REGISTER AN EXISTSING USER" + else + entry = + meetingId: meetingId + userId: user.userid + authToken: user.authToken + + id = Meteor.Users.insert(entry) + Meteor.log.info "added user dummy user id=[#{id}]:#{user.name}. + Users.size is now #{Meteor.Users.find({meetingId: meetingId}).count()}" + +# called on server start and on meeting end +@clearUsersCollection = (meetingId) -> + if meetingId? + Meteor.Users.remove({meetingId: meetingId}, Meteor.log.info "cleared Users Collection (meetingId: #{meetingId}!") + else + Meteor.Users.remove({}, Meteor.log.info "cleared Users Collection (all meetings)!") diff --git a/labs/meteor-client/app/server/publish.coffee b/labs/meteor-client/app/server/publish.coffee new file mode 100755 index 0000000000000000000000000000000000000000..727c870d2f771d71c09d98a2699cfb60bd74e6b2 --- /dev/null +++ b/labs/meteor-client/app/server/publish.coffee @@ -0,0 +1,66 @@ +# Publish only the online users that are in the particular meetingId +# On the client side we pass the meetingId parameter +Meteor.publish 'users', (meetingId, userid, authToken) -> + Meteor.log.info "attempt publishing users for #{meetingId}, #{userid}, #{authToken}" + u = Meteor.Users.findOne({'userId': userid, 'meetingId': meetingId}) + if u? + Meteor.log.info "found it from the first time #{userid}" + if isAllowedTo('subscribeUsers', meetingId, userid, authToken) + Meteor.log.info "#{userid} was allowed to subscribe to 'users'" + username = u?.user?.name or "UNKNOWN" + + # offline -> online + if u.user?.connection_status isnt 'online' + Meteor.call "validateAuthToken", meetingId, userid, authToken + + Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "online"}}) + Meteor.log.info "username of the subscriber: " + username + ", connection_status becomes online" + + @_session.socket.on("close", Meteor.bindEnvironment(=> + Meteor.log.info "\na user lost connection: session.id=#{@_session.id} userId = #{userid}, username=#{username}, meeting=#{meetingId}" + Meteor.Users.update({'meetingId':meetingId, 'userId': userid}, {$set:{'user.connection_status': "offline"}}) + Meteor.log.info "username of the user losing connection: " + username + ", connection_status: becomes offline" + requestUserLeaving meetingId, userid + ) + ) + + #publish the users which are not offline + Meteor.Users.find( + {meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}}, + {fields:{'authToken': false} + }) + else + Meteor.log.warn "was not authorized to subscribe to 'users'" + + else #subscribing before the user was added to the collection + Meteor.call "validateAuthToken", meetingId, userid, authToken + Meteor.log.error "there was no such user #{userid} in #{meetingId}" + Meteor.Users.find( + {meetingId: meetingId, 'user.connection_status':{$in: ["online", ""]}}, + {fields:{'authToken': false} + }) + + +Meteor.publish 'chat', (meetingId, userid, authToken) -> + if isAllowedTo('subscribeChat', meetingId, userid, authToken) + Meteor.log.info "publishing chat for #{meetingId} #{userid} #{authToken}" + return Meteor.Chat.find({$or: [ + {'message.chat_type': 'PUBLIC_CHAT', 'meetingId': meetingId}, + {'message.from_userid': userid, 'meetingId': meetingId}, + {'message.to_userid': userid, 'meetingId': meetingId} + ]}) + +Meteor.publish 'shapes', (meetingId) -> + Meteor.Shapes.find({meetingId: meetingId}) + +Meteor.publish 'slides', (meetingId) -> + Meteor.log.info "publishing slides for #{meetingId}" + Meteor.Slides.find({meetingId: meetingId}) + +Meteor.publish 'meetings', (meetingId) -> + Meteor.log.info "publishing meetings for #{meetingId}" + Meteor.Meetings.find({meetingId: meetingId}) + +Meteor.publish 'presentations', (meetingId) -> + Meteor.log.info "publishing presentations for #{meetingId}" + Meteor.Presentations.find({meetingId: meetingId}) diff --git a/labs/meteor-client/app/server/redispubsub.coffee b/labs/meteor-client/app/server/redispubsub.coffee new file mode 100755 index 0000000000000000000000000000000000000000..d983db9816d2a1467e1ba74d296c15e46a680d3f --- /dev/null +++ b/labs/meteor-client/app/server/redispubsub.coffee @@ -0,0 +1,291 @@ +Meteor.methods + # + # I dont know if this is okay to be server side. We need to call it from the router, but I don't know if any harm can be caused + # by the client calling this + # + + # Construct and send a message to bbb-web to validate the user + validateAuthToken: (meetingId, userId, authToken) -> + Meteor.log.info "sending a validate_auth_token with", + userid: userId + authToken: authToken + meetingid: meetingId + + message = + "payload": + "auth_token": authToken + "userid": userId + "meeting_id": meetingId + "header": + "timestamp": new Date().getTime() + "reply_to": meetingId + "/" + userId + "name": "validate_auth_token" + + if authToken? and userId? and meetingId? + publish Meteor.config.redis.channels.toBBBApps.meeting, message + else + Meteor.log.info "did not have enough information to send a validate_auth_token message" + +class Meteor.RedisPubSub + constructor: (callback) -> + Meteor.log.info "constructor RedisPubSub" + + @pubClient = redis.createClient() + @subClient = redis.createClient() + + Meteor.log.info("Subscribing message on channel: #{Meteor.config.redis.channels.fromBBBApps}") + + @subClient.on "psubscribe", Meteor.bindEnvironment(@_onSubscribe) + @subClient.on "pmessage", Meteor.bindEnvironment(@_onMessage) + + @subClient.psubscribe(Meteor.config.redis.channels.fromBBBApps) + callback @ + + _onSubscribe: (channel, count) => + Meteor.log.info "Subscribed to #{channel}" + + #grab data about all active meetings on the server + message = + "header": + "name": "get_all_meetings_request" + "payload": {} # I need this, otherwise bbb-apps won't recognize the message + publish Meteor.config.redis.channels.toBBBApps.meeting, message + + _onMessage: (pattern, channel, jsonMsg) => + # TODO: this has to be in a try/catch block, otherwise the server will + # crash if the message has a bad format + + message = JSON.parse(jsonMsg) + correlationId = message.payload?.reply_to or message.header?.reply_to + meetingId = message.payload?.meeting_id + + ignoredEventTypes = [ + "keep_alive_reply" + "page_resized_message" + "presentation_page_resized_message" + "presentation_cursor_updated_message" # just because it's common. we handle it anyway + ] + + if message?.header? and message?.payload? + unless message.header.name in ignoredEventTypes + Meteor.log.info "eventType= #{message.header.name} ", + message: jsonMsg + + # handle voice events + if message.header.name in ['user_left_voice_message', 'user_joined_voice_message', 'user_voice_talking_message', 'user_voice_muted_message'] + voiceUser = message.payload.user?.voiceUser + updateVoiceUser meetingId, voiceUser + return + + # listen only + if message.header.name is 'user_listening_only' + updateVoiceUser meetingId, {'web_userid': message.payload.userid, 'listenOnly': message.payload.listen_only} + # most likely we don't need to ensure that the user's voiceUser's {talking, joined, muted, locked} are false by default #TODO? + return + + if message.header.name is "get_all_meetings_reply" + Meteor.log.info "Let's store some data for the running meetings so that when an HTML5 client joins everything is ready!" + listOfMeetings = message.payload.meetings + for meeting in listOfMeetings + # we currently do not have voice_conf or duration in this message. + addMeetingToCollection meeting.meetingID, meeting.meetingName, meeting.recorded, meeting.voiceBridge, meeting.duration + return + + if message.header.name is "get_users_reply" and message.payload.requester_id is "nodeJSapp" + unless Meteor.Meetings.findOne({MeetingId: message.payload.meeting_id})? + users = message.payload.users + for user in users + user.timeOfJoining = message.header.current_time # TODO this might need to be removed + userJoined meetingId, user + return + + if message.header.name is "validate_auth_token_reply" + return + + if message.header.name is "user_registered_message" + createDummyUser message.payload.meeting_id, message.payload.user + return + + if message.header.name is "user_joined_message" + user = message.payload.user + user.timeOfJoining = message.header.current_time + userJoined meetingId, user + return + + if message.header.name is "user_left_message" + userId = message.payload.user?.userid + if userId? and meetingId? + markUserOffline meetingId, userId + return + + if message.header.name is "disconnect_user_message" + Meteor.log.info "a user(#{message.payload.userid}) was kicked out from meeting(#{message.payload.meeting_id})" + return + + if message.header.name is "get_chat_history_reply" and message.payload.requester_id is "nodeJSapp" + unless Meteor.Meetings.findOne({MeetingId: message.payload.meeting_id})? + for chatMessage in message.payload.chat_history + addChatToCollection meetingId, chatMessage + return + + if message.header.name is "send_public_chat_message" or message.header.name is "send_private_chat_message" + messageObject = message.payload.message + # use current_time instead of message.from_time so that the chats from Flash and HTML5 have uniform times + messageObject.from_time = message.header.current_time + addChatToCollection meetingId, messageObject + return + + if message.header.name is "meeting_created_message" + meetingName = message.payload.name + intendedForRecording = message.payload.recorded + voiceConf = message.payload.voice_conf + duration = message.payload.duration + addMeetingToCollection meetingId, meetingName, intendedForRecording, voiceConf, duration + return + + if message.header.name is "presentation_shared_message" + presentationId = message.payload.presentation?.id + # change the currently displayed presentation to presentation.current = false + Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},{$set: {"presentation.current": false}}) + + #update(if already present) entirely the presentation with the fresh data + removePresentationFromCollection meetingId, presentationId + addPresentationToCollection meetingId, message.payload.presentation + + for slide in message.payload.presentation?.pages + addSlideToCollection meetingId, message.payload.presentation?.id, slide + if slide.current + displayThisSlide meetingId, slide.id, slide + return + + if message.header.name is "get_presentation_info_reply" and message.payload.requester_id is "nodeJSapp" + for presentation in message.payload.presentations + addPresentationToCollection meetingId, presentation + + for page in presentation.pages + #add the slide to the collection + addSlideToCollection meetingId, presentation.id, page + + #request for shapes + whiteboardId = "#{presentation.id}/#{page.num}" # d2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1404411622872/1 + #Meteor.log.info "the whiteboard_id here is:" + whiteboardId + + message = + "payload": + "meeting_id": meetingId + "requester_id": "nodeJSapp" + "whiteboard_id": whiteboardId + "header": + "timestamp": new Date().getTime() + "name": "get_whiteboard_shapes_request" + "version": "0.0.1" + + if whiteboardId? and meetingId? + publish Meteor.config.redis.channels.toBBBApps.whiteboard, message + else + Meteor.log.info "did not have enough information to send a user_leaving_request" + return + + if message.header.name is "presentation_page_changed_message" + newSlide = message.payload.page + displayThisSlide meetingId, newSlide?.id, newSlide + return + + if message.header.name is "get_whiteboard_shapes_reply" and message.payload.requester_id is "nodeJSapp" + for shape in message.payload.shapes + whiteboardId = shape.wb_id + addShapeToCollection meetingId, whiteboardId, shape + return + + if message.header.name is "send_whiteboard_shape_message" + shape = message.payload.shape + whiteboardId = shape?.wb_id + addShapeToCollection meetingId, whiteboardId, shape + return + + if message.header.name is "presentation_cursor_updated_message" + x = message.payload.x_percent + y = message.payload.y_percent + Meteor.Presentations.update({"presentation.current": true, meetingId: meetingId},{$set: {"pointer.x": x, "pointer.y": y}}) + return + + if message.header.name is "whiteboard_cleared_message" + whiteboardId = message.payload.whiteboard_id + removeAllShapesFromSlide meetingId, whiteboardId + return + + if message.header.name is "undo_whiteboard_request" + whiteboardId = message.payload.whiteboard_id + shapeId = message.payload.shape_id + removeShapeFromSlide meetingId, whiteboardId, shapeId + return + + if message.header.name is "presenter_assigned_message" + newPresenterId = message.payload.new_presenter_id + if newPresenterId? + # reset the previous presenter + Meteor.Users.update({"user.presenter": true, meetingId: meetingId},{$set: {"user.presenter": false}}) + # set the new presenter + Meteor.Users.update({"user.userid": newPresenterId, meetingId: meetingId},{$set: {"user.presenter": true}}) + return + + if message.header.name is "presentation_page_resized_message" + slideId = message.payload.page?.id + heightRatio = message.payload.page?.height_ratio + widthRatio = message.payload.page?.width_ratio + xOffset = message.payload.page?.x_offset + yOffset = message.payload.page?.y_offset + presentationId = slideId.split("/")[0] + Meteor.Slides.update({presentationId: presentationId, "slide.current": true}, {$set: {"slide.height_ratio": heightRatio, "slide.width_ratio": widthRatio, "slide.x_offset": xOffset, "slide.y_offset": yOffset}}) + return + + if message.header.name is "user_raised_hand_message" + userId = message.payload.userid + meetingId = message.payload.meeting_id + if userId? and meetingId? + last_raised = new Date() + Meteor.Users.update({"user.userid": userId},{$set: {"user.raise_hand": last_raised}}) + return + + if message.header.name is "user_lowered_hand_message" + userId = message.payload.userid + meetingId = message.payload.meeting_id + if userId? and meetingId? + Meteor.Users.update({"user.userid": userId, meetingId: meetingId},{$set: {"user.raise_hand": 0}}) + return + + if message.header.name is "recording_status_changed_message" + intendedForRecording = message.payload.recorded + currentlyBeingRecorded = message.payload.recording + Meteor.Meetings.update({meetingId: meetingId, intendedForRecording: intendedForRecording}, {$set: {currentlyBeingRecorded: currentlyBeingRecorded}}) + return + + if message.header.name in ["meeting_ended_message", "meeting_destroyed_event", + "end_and_kick_all_message", "disconnect_all_users_message"] + if Meteor.Meetings.findOne({meetingId: meetingId})? + Meteor.log.info "there are #{Meteor.Users.find({meetingId: meetingId}).count()} users in the meeting" + for user in Meteor.Users.find({meetingId: meetingId}).fetch() + markUserOffline meetingId, user.userId + #TODO should we clear the chat messages for that meeting?! + unless message.header.name is "disconnect_all_users_message" + removeMeetingFromCollection meetingId + return + +# -------------------------------------------------------------------------------------------- +# Private methods on server +# -------------------------------------------------------------------------------------------- + +# message should be an object +@publish = (channel, message) -> + Meteor.log.info "Publishing", + channel: channel + message: message + + if Meteor.redisPubSub? + Meteor.redisPubSub.pubClient.publish channel, JSON.stringify(message), (err, res) -> + if err + Meteor.log.info "error", + error: err + + else + Meteor.log.info "ERROR!! Meteor.redisPubSub was undefined" diff --git a/labs/meteor-client/app/server/server.coffee b/labs/meteor-client/app/server/server.coffee new file mode 100755 index 0000000000000000000000000000000000000000..125492a669988db89201196ae24a0bcd2a501425 --- /dev/null +++ b/labs/meteor-client/app/server/server.coffee @@ -0,0 +1,14 @@ +Meteor.startup -> + Meteor.log.info "server start" + + #remove all data + clearUsersCollection() + clearChatCollection() + clearMeetingsCollection() + clearShapesCollection() + clearSlidesCollection() + clearPresentationsCollection() + + # create create a PubSub connection, start listening + Meteor.redisPubSub = new Meteor.RedisPubSub(-> + Meteor.log.info "created pubsub") diff --git a/labs/meteor-client/app/server/user_permissions.coffee b/labs/meteor-client/app/server/user_permissions.coffee new file mode 100644 index 0000000000000000000000000000000000000000..b7e171b9f07f30db13dd89362651195feb669bf4 --- /dev/null +++ b/labs/meteor-client/app/server/user_permissions.coffee @@ -0,0 +1,36 @@ + +moderator = null +presenter = null +viewer = + # raising/lowering hand + raiseOwnHand : true + lowerOwnHand : true + + # muting + muteSelf : true + unmuteSelf : true + + logoutSelf : true + + #subscribing + subscribeUsers: true + subscribeChat: true + + #chat + chatPublic: true #should make this dynamically modifiable later on + chatPrivate: true #should make this dynamically modifiable later on + +@isAllowedTo = (action, meetingId, userId, authToken) -> + Meteor.log.info "in isAllowedTo: action-#{action}, userId=#{userId}, authToken=#{authToken}" + + user = Meteor.Users.findOne({meetingId:meetingId, userId: userId}) + if user? + # we check if the user is who he claims to be + if authToken is user.authToken + if user.user?.role is 'VIEWER' or user.user?.role is undefined + return viewer[action] or false + Meteor.log.error "in meetingId=#{meetingId} userId=#{userId} tried to perform #{action} without permission" + + "\n..while the authToken was #{user.authToken} and the user's object is #{JSON.stringify user}" + + # the current version of the HTML5 client represents only VIEWER users + return false diff --git a/labs/meteor-client/app/smart.json b/labs/meteor-client/app/smart.json new file mode 100644 index 0000000000000000000000000000000000000000..df361d73aa09c1c4eb0b0ff0ee12c5885b14c29b --- /dev/null +++ b/labs/meteor-client/app/smart.json @@ -0,0 +1,7 @@ +{ + "packages": { + "raphaeljs-package": { + "git": "https://github.com/tomconnors/raphaeljs-package.git" + } + } +} diff --git a/labs/meteor-client/app/smart.lock b/labs/meteor-client/app/smart.lock new file mode 100644 index 0000000000000000000000000000000000000000..28323ba08a86ea6a4584af9e30f9038eb0c9785b --- /dev/null +++ b/labs/meteor-client/app/smart.lock @@ -0,0 +1,18 @@ +{ + "meteor": {}, + "dependencies": { + "basePackages": { + "raphaeljs-package": { + "git": "https://github.com/tomconnors/raphaeljs-package.git", + "branch": "master" + } + }, + "packages": { + "raphaeljs-package": { + "git": "https://github.com/tomconnors/raphaeljs-package.git", + "branch": "master", + "commit": "85eaef3664ec063e4bcb81be1226d126d4d20539" + } + } + } +} diff --git a/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js b/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js new file mode 100755 index 0000000000000000000000000000000000000000..a766640de087f922d9fec07dd61dffb371c63e37 --- /dev/null +++ b/labs/meteor-client/app/tests/jasmine/client/integration/templates/usersList/usersListSpec.js @@ -0,0 +1,264 @@ +var emptyUsersCollection = function() { + Meteor.Users.find().map(function(item) { + Meteor.Users.remove({ _id: item._id }); + }); +} + +var renderUsersListTemplate = function() { + var div = document.createElement("div"); + var data = {}; + data.id = "users"; + data.name = "usersList"; + var comp = Blaze.renderWithData(Template.usersList, data); // loading data is optional + Blaze.insert(comp, div); + return div; +} + +// TODO: try to use Meteor methods instead +var removeUserFromCollection = function(id) { + Meteor.Users.find().map(function(item) { + if(item.userId == id) Meteor.Users.remove({ _id: item._id }); + }); +} + +// TODO: try to start with calling the app's methods instead of modifying the collection +xdescribe("usersList template", function () { + beforeEach(function () { + emptyUsersCollection(); + }); + + it("should have no users when we start with an empty Users collection", function () { + var div = renderUsersListTemplate(); + + expect($(div).find(".userNameEntry")[0]).not.toBeDefined(); + }); + + it("should not display presenter icon next to a non-presenter user", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + presenter: false + } + }; + Meteor.Users.insert(document1); + var div = renderUsersListTemplate(); + + expect($(div).find(".glyphicon-picture")[0]).not.toBeDefined(); + }); + + it("should display presenter icon next to the presenter's username", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + presenter: true + } + }; + Meteor.Users.insert(document1); + var div = renderUsersListTemplate(); + + expect($(div).find(".glyphicon-picture")[0]).toBeDefined(); + }); + + it("should display usernames correctly", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + name: "Maxim" + } + }; + Meteor.Users.insert(document1); + var div = renderUsersListTemplate(); + + expect($(div).find(".userNameEntry").html().trim()).toEqual("Maxim"); + }); + + it("should display all the users in chat (correct number)", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + name: "Maxim" + } + }; + var document2 = { + meetingId: "meeting001", + userId: "user002", + user: { + name: "Anton" + } + }; + var document3 = { + meetingId: "meeting001", + userId: "user003", + user: { + name: "Danny" + } + }; + + Meteor.Users.insert(document1); + Meteor.Users.insert(document2); + Meteor.Users.insert(document3); + var div = renderUsersListTemplate(); + + expect($(div).find(".userNameEntry").size()).toEqual(3); + }); + + it("should be able to reactively handle new and logged-out users (1 user -> 3 users -> 4 users -> 2 users -> 5 users)", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + name: "Maxim" + } + }; + var document2 = { + meetingId: "meeting001", + userId: "user002", + user: { + name: "Anton" + } + }; + var document3 = { + meetingId: "meeting001", + userId: "user003", + user: { + name: "Danny" + } + }; + var document4 = { + meetingId: "meeting001", + userId: "user004", + user: { + name: "Chad" + } + }; + var document5 = { + meetingId: "meeting001", + userId: "user005", + user: { + name: "Fardad" + } + }; + var document6 = { + meetingId: "meeting001", + userId: "user006", + user: { + name: "Adam" + } + }; + var document7 = { + meetingId: "meeting001", + userId: "user007", + user: { + name: "Gary" + } + }; + + Meteor.Users.insert(document1); + var div = renderUsersListTemplate(); + + expect($(div).find(".userNameEntry").size()).toEqual(1); + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + + Meteor.Users.insert(document2); + Meteor.Users.insert(document3); + + expect($(div).find(".userNameEntry").size()).toEqual(3); + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton"); + expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny"); + + Meteor.Users.insert(document4); + + expect($(div).find(".userNameEntry").size()).toEqual(4); + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton"); + expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny"); + expect($(div).find(".userNameEntry:eq(3)").html().trim()).toEqual("Chad"); + + removeUserFromCollection("user002"); + removeUserFromCollection("user004"); + + expect($(div).find(".userNameEntry").size()).toEqual(2); + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Danny"); + + Meteor.Users.insert(document5); + Meteor.Users.insert(document6); + Meteor.Users.insert(document7); + + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Danny"); + expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Fardad"); + expect($(div).find(".userNameEntry:eq(3)").html().trim()).toEqual("Adam"); + expect($(div).find(".userNameEntry:eq(4)").html().trim()).toEqual("Gary"); + }); + + it("should display usernames in the correct order", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + name: "Maxim" + } + }; + var document2 = { + meetingId: "meeting001", + userId: "user002", + user: { + name: "Anton" + } + }; + var document3 = { + meetingId: "meeting001", + userId: "user003", + user: { + name: "Danny" + } + }; + + Meteor.Users.insert(document1); + Meteor.Users.insert(document2); + Meteor.Users.insert(document3); + var div = renderUsersListTemplate(); + + expect($(div).find(".userNameEntry:eq(0)").html().trim()).toEqual("Maxim"); + expect($(div).find(".userNameEntry:eq(1)").html().trim()).toEqual("Anton"); + expect($(div).find(".userNameEntry:eq(2)").html().trim()).toEqual("Danny"); + }); + + it("should handle listen-only users properly", function () { + var document1 = { + meetingId: "meeting001", + userId: "user001", + user: { + name: "Maxim" + } + }; + var document2 = { + meetingId: "meeting001", + userId: "user002", + user: { + name: "Anton", + listenOnly: true + } + }; + var document3 = { + meetingId: "meeting001", + userId: "user003", + user: { + name: "Danny" + } + }; + + Meteor.Users.insert(document1); + Meteor.Users.insert(document2); + Meteor.Users.insert(document3); + var div = renderUsersListTemplate(); + + expect($(div).find(".glyphicon-headphones")).toBeDefined(); + }); +}); diff --git a/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js b/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js new file mode 100755 index 0000000000000000000000000000000000000000..db7a809486f5511e7cc854f6f79cded3ed39d974 --- /dev/null +++ b/labs/meteor-client/app/tests/jasmine/client/integration/templates/whiteboard/whiteboardSpec.js @@ -0,0 +1,95 @@ +var emptyMeetingsCollection = function() { + Meteor.Meetings.find().map(function(item) { + Meteor.Meetings.remove({ _id: item._id }); + }); +} + +var emptyPresentationsCollection = function() { + Meteor.Presentations.find().map(function(item) { + Meteor.Presentations.remove({ _id: item._id }); + }); +} + +var emptySlidesCollection = function() { + Meteor.Slides.find().map(function(item) { + Meteor.Slides.remove({ _id: item._id }); + }); +} + +var emptyShapesCollection = function() { + Meteor.Shapes.find().map(function(item) { + Meteor.Shapes.remove({ _id: item._id }); + }); +} + +var renderWhiteboardTemplate = function(title) { + var div = document.createElement("div"); + var data = {}; + data.id = "whiteboard"; + data.title = title; + data.name = "whiteboard"; + var comp = Blaze.renderWithData(Template.whiteboard, data); // loading data is optional + Blaze.insert(comp, div); + return div; +} + +xdescribe("whiteboard template", function () { + beforeEach(function () { + emptyMeetingsCollection(); + emptyPresentationsCollection(); + emptySlidesCollection(); + emptyShapesCollection(); + }); + + it("should contain a pencil icon inside the title entry", function () { + var div = renderWhiteboardTemplate("Whiteboard: default.pdf"); + + expect($(div).find(".title").find(".glyphicon-pencil")[0]).toBeDefined(); + }); + + it("should contain the correct title", function () { + var div = renderWhiteboardTemplate("Whiteboard: default.pdf"); + + expect($(div).find(".title:eq(0)").html()).toContain("Whiteboard: default.pdf"); + }); + + // TODO: finish the following + it("should be rendered successfully", function () { + var meeting1 = { + meetingId: "meeting001", + meetingName: "first meeting" + } + Meteor.Meetings.insert(meeting1); + + var presentation1 = { + meetingId: "meeting001", + presentation: { + id: "presentation001", + name: "default.pdf", + current: true + } + } + Meteor.Presentations.insert(presentation1); + + var slide1 = { + meetingId: "meeting001", + presentationId: "presentation001", + slide: { + id: "slide001", + num: 1, + current: true, + width_ratio: 100.0, + height_ratio: 100.0, + x_offset: 0.0, + y_offset: 0.0, + png_uri: "http://bigbluebutton.org/wp-content/uploads/2013/05/bbb-overview.png" + } + } + Meteor.Slides.insert(slide1); + + var div = document.createElement("DIV"); + Blaze.render(Template.main, div); + + expect($(div).find("#whiteboard-navbar")[0]).toBeDefined(); + }); +}); diff --git a/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js b/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js new file mode 100755 index 0000000000000000000000000000000000000000..4f7c913db8cbc8c7238efa9b7c609386300ec725 --- /dev/null +++ b/labs/meteor-client/app/tests/jasmine/server/unit/CollectionMethodsSpec.js @@ -0,0 +1,637 @@ +describe("Collections", function () { + beforeEach(function () { + MeteorStubs.install(); + }); + + afterEach(function () { + MeteorStubs.uninstall(); + }); + + //---------------------------------------------------------------------- + // publish.coffee + //---------------------------------------------------------------------- + + it("should all be correctly handled by remove() after calling clearCollections()", function () { + spyOn(Meteor.Users, "remove"); + spyOn(Meteor.Chat, "remove"); + spyOn(Meteor.Meetings, "remove"); + spyOn(Meteor.Shapes, "remove"); + spyOn(Meteor.Slides, "remove"); + spyOn(Meteor.Presentations, "remove"); + + clearUsersCollection(); + clearChatCollection(); + clearMeetingsCollection(); + clearShapesCollection(); + clearSlidesCollection(); + clearPresentationsCollection(); + + expect(Meteor.Users.remove).toHaveBeenCalled(); + expect(Meteor.Chat.remove).toHaveBeenCalled(); + expect(Meteor.Meetings.remove).toHaveBeenCalled(); + expect(Meteor.Shapes.remove).toHaveBeenCalled(); + expect(Meteor.Slides.remove).toHaveBeenCalled(); + expect(Meteor.Presentations.remove).toHaveBeenCalled(); + }); + + //---------------------------------------------------------------------- + // chat.coffee + //---------------------------------------------------------------------- + + it("should be handled correctly by insert() on calling addChatToCollection() in case of private chat", function () { + spyOn(Meteor.Users, "findOne").and.callFake(function(doc) { + if(doc.userId == "user001") return { userId: "user001" }; + else if(doc.userId == "user002") return { userUd: "user002" }; + }); + spyOn(Meteor.Chat, "insert"); + + addChatToCollection("meeting001", { + from_time: "123", + from_userid: "user001", + to_userid: "user002", + chat_type: "PRIVATE_CHAT", + message: "Hello!", + to_username: "Anton", + from_tz_offset: "240", + from_color: "0x000000", + from_username: "Maxim", + from_lang: "en" + }); + + expect(Meteor.Chat.insert).toHaveBeenCalledWith({ + meetingId: "meeting001", + message: { + chat_type: "PRIVATE_CHAT", + message: "Hello!", + to_username: "Anton", + from_tz_offset: "240", + from_color: "0x000000", + to_userid: "user002",//not "dbid002" + from_userid: "user001",//not "dbid001" + from_time: "123", + from_username: "Maxim", + from_lang: "en" + } + }); + }); + + it("should be handled correctly by insert() on calling addChatToCollection() in case of public chat", function () { + spyOn(Meteor.Users, "findOne").and.callFake(function(doc) { + if(doc.userId == "user001") return { _id: "dbid001" }; + else if(doc.userId == "user002") return { _id: "dbid002" }; + }); + spyOn(Meteor.Chat, "insert"); + + addChatToCollection("meeting001", { + from_time: "123", + from_userid: "user001", + to_userid: "public_chat_userid", + chat_type: "PUBLIC_CHAT", + message: "Hello!", + to_username: "public_chat_username", + from_tz_offset: "240", + from_color: "0x000000", + from_username: "Maxim", + from_lang: "en" + }); + + expect(Meteor.Chat.insert).toHaveBeenCalledWith({ + meetingId: "meeting001", + message: { + chat_type: "PUBLIC_CHAT", + message: "Hello!", + to_username: "public_chat_username", + from_tz_offset: "240", + from_color: "0x000000", + to_userid: "public_chat_userid", + from_userid: "user001", + from_time: "123", + from_username: "Maxim", + from_lang: "en" + } + }); + }); + + //---------------------------------------------------------------------- + // meetings.coffee + //---------------------------------------------------------------------- + + it("should not be updated on calling addMeetingToCollection() if the meeting is already in the collection", function () { + spyOn(Meteor.Meetings, "findOne").and.callFake(function(doc) { + if(doc.meetingId == "meeting001") return { meetingId: "meeting001" }; + else return undefined; + }); + spyOn(Meteor.Meetings, "insert"); + + addMeetingToCollection("meeting001", "Demo Meeting", false, "12345", "0"); + + expect(Meteor.Meetings.insert).not.toHaveBeenCalled(); + }); + + it("should be handled correctly by insert() on calling addMeetingToCollection() with a brand new meeting", function () { + spyOn(Meteor.Meetings, "findOne").and.returnValue(undefined);//collection is empty + spyOn(Meteor.Meetings, "insert"); + + addMeetingToCollection("meeting001", "Demo Meeting", false, "12345", "0"); + + expect(Meteor.Meetings.insert).toHaveBeenCalledWith({ + meetingId: "meeting001", + meetingName: "Demo Meeting", + intendedForRecording: false, + currentlyBeingRecorded: false,//default value + voiceConf: "12345", + duration: "0" + }); + }); + + it("should not be touched on calling removeMeetingFromCollection() if there is no wanted meeting in the collection", function () { + spyOn(Meteor.Meetings, "findOne").and.returnValue(undefined);//collection is empty + spyOn(Meteor.Meetings, "remove"); + + removeMeetingFromCollection("meeting001"); + + expect(Meteor.Meetings.remove).not.toHaveBeenCalled(); + }); + + //TODO: emulate a find() call + /*it("should be correctly updated after the removeMeetingFromCollection() call", function () { + spyOn(Meteor.Meetings, "findOne").and.callFake(function(doc) { + if(doc.meetingId == "meeting001") return { _id: "id001", meetingId: "meeting001" }; + else return undefined; + }); + + spyOn(Meteor.Meetings, "remove"); + + removeMeetingFromCollection("meeting001"); + + expect(Meteor.Meetings.remove).toHaveBeenCalled(); + });*/ + + //---------------------------------------------------------------------- + // shapes.coffee + //---------------------------------------------------------------------- + + // addShapeToCollection() + it('should be handled correctly by insert() on calling addShapeToCollection() with a text', function () { + spyOn(Meteor.Shapes, 'find').and.returnValue({ + count: function() { + return 1; + } + }); + spyOn(Meteor.Shapes, 'insert'); + + addShapeToCollection('meeting001', 'whiteboard001', { + shape_type: 'text', + status: 'textPublished', + shape: { + type: 'text', + textBoxHeight: 24.5, + backgroundColor: 16777215, + fontColor: 0, + status: 'textPublished', + dataPoints: '36.5,55.0', + x: 36.5, + textBoxWidth: 36.0, + whiteboardId: 'whiteboard001', + fontSize: 18, + id: 'shape001', + y: 55.0, + calcedFontSize: 3.6, + text: 'Hello World!', + background: true + } + }); + + expect(Meteor.Shapes.insert).toHaveBeenCalledWith({ + meetingId: 'meeting001', + whiteboardId: 'whiteboard001', + shape: { + type: 'text', + textBoxHeight: 24.5, + backgroundColor: 16777215, + fontColor: 0, + status: 'textPublished', + dataPoints: '36.5,55.0', + x: 36.5, + textBoxWidth: 36.0, + whiteboardId: 'whiteboard001', + fontSize: 18, + id: 'shape001', + y: 55.0, + calcedFontSize: 3.6, + text: 'Hello World!', + background: true + } + }); + }); + it('should be handled correctly by insert() on calling addShapeToCollection() with a finished standard shape', function () { + spyOn(Meteor.Shapes, 'find').and.returnValue({ + count: function() { + return 1; + } + }); + spyOn(Meteor.Shapes, 'insert'); + + addShapeToCollection('meeting001', 'whiteboard001', { + wb_id: 'whiteboard001', + shape_type: 'rectangle', + status: 'DRAW_END', + id: 'shape001', + shape: { + type: 'rectangle', + status: 'DRAW_END', + points: [60.0, 17.0, 73.0, 57.5], + whiteboardId: 'whiteboard001', + id: 'shape001', + square: false, + transparency: false, + thickness: 10, + color: 0 + } + }); + + expect(Meteor.Shapes.insert).toHaveBeenCalledWith({ + meetingId: 'meeting001', + whiteboardId: 'whiteboard001', + shape: { + wb_id: 'whiteboard001', + shape_type: 'rectangle', + status: 'DRAW_END', + id: 'shape001', + shape: { + type: 'rectangle', + status: 'DRAW_END', + points: [60.0, 17.0, 73.0, 57.5], + whiteboardId: 'whiteboard001', + id: 'shape001', + square: false, + transparency: false, + thickness: 10, + color: 0 + } + } + }); + }); + it('should be handled correctly by insert() on calling addShapeToCollection() with a pencil being used', function () { + spyOn(Meteor.Shapes, 'find').and.returnValue({ + count: function() { + return 1; + } + }); + spyOn(Meteor.Shapes, 'insert'); + + addShapeToCollection('meeting001', 'whiteboard001', { + wb_id: 'whiteboard001', + shape_type: 'pencil', + status: 'DRAW_START', + id: 'shape001', + shape: { + type: 'pencil', + status: 'DRAW_START', + points: [35.8, 63.6, 36.1, 63.4, 36.2, 63.2], + whiteboardId: 'whiteboard001', + id: 'shape001', + square: undefined, + transparency: false, + thickness: 10, + color: 0 + } + }); + + expect(Meteor.Shapes.insert).toHaveBeenCalledWith({ + meetingId: 'meeting001', + whiteboardId: 'whiteboard001', + shape: { + wb_id: 'whiteboard001', + shape_type: 'pencil', + status: 'DRAW_START', + id: 'shape001', + shape: { + type: 'pencil', + status: 'DRAW_START', + points: [35.8, 63.6, 36.1, 63.4, 36.2, 63.2], + whiteboardId: 'whiteboard001', + id: 'shape001', + square: undefined, + transparency: false, + thickness: 10, + color: 0 + } + } + }); + }); + + // removeAllShapesFromSlide() + it('should not be touched on calling removeAllShapesFromSlide() with undefined meetingId', function () { + spyOn(Meteor.Shapes, 'remove'); + removeAllShapesFromSlide(undefined, 'whiteboard001'); + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeAllShapesFromSlide() with undefined whiteboardId', function () { + spyOn(Meteor.Shapes, 'remove'); + removeAllShapesFromSlide('meeting001', undefined); + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeAllShapesFromSlide() if there is no shapes on the whiteboard', function () { + spyOn(Meteor.Shapes, 'find').and.returnValue(undefined); + spyOn(Meteor.Shapes, 'remove'); + removeAllShapesFromSlide('meeting001', 'whiteboard001'); + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should be cleared on calling removeAllShapesFromSlide() if there are shapes on the whiteboard', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001') + return { + fetch: function() { + return [{shape: {id: 'shape001'}}]; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001') + return { + _id: 'doc001' + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'remove'); + removeAllShapesFromSlide('meeting001', 'whiteboard001'); + expect(Meteor.Shapes.remove).toHaveBeenCalledWith('doc001'); + }); + + // removeShapeFromSlide() + it('should not be touched on calling removeShapeFromSlide() with undefined meetingId', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === undefined && doc.whiteboardId === 'whiteboard001') + return { + count: function() { + return 0; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === undefined && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001') + return { + _id: 'doc001' + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'remove'); + + removeShapeFromSlide(undefined, 'whiteboard001', 'shape001'); + + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeShapeFromSlide() with undefined whiteboardId', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === undefined) + return { + count: function() { + return 0; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === undefined && doc['shape.id'] === 'shape001') + return { + _id: 'doc001' + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'remove'); + + removeShapeFromSlide('meeting001', undefined, 'shape001'); + + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeShapeFromSlide() with undefined shapeId', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001') + return { + count: function() { + return 0; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === undefined) + return { + _id: 'doc001' + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'remove'); + + removeShapeFromSlide('meeting001', 'whiteboard001', undefined); + + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeShapeFromSlide() if there is no wanted shape on the whiteboard', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001') + return { + count: function() { + return 0; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001') + return undefined; + else return { + _id: 'doc001' + }; + }); + spyOn(Meteor.Shapes, 'remove'); + + removeShapeFromSlide('meeting001', 'whiteboard001', undefined); + + expect(Meteor.Shapes.remove).not.toHaveBeenCalled(); + }); + it('should be updated correctly on calling removeShapeFromSlide() with an existing shape', function () { + spyOn(Meteor.Shapes, 'find').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001') + return { + count: function() { + return 0; + } + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc.whiteboardId === 'whiteboard001' && doc['shape.id'] === 'shape001') + return { + _id: 'doc001' + }; + else return undefined; + }); + spyOn(Meteor.Shapes, 'remove'); + removeShapeFromSlide('meeting001', 'whiteboard001', 'shape001'); + expect(Meteor.Shapes.remove).toHaveBeenCalledWith('doc001'); + }); + + //---------------------------------------------------------------------- + // presentation.coffee + //---------------------------------------------------------------------- + + it("should be handled correctly by insert() on calling addPresentationToCollection()", function (){ + spyOn(Meteor.Presentations, "findOne").and.returnValue(undefined) + + spyOn(Meteor.Presentations, "insert"); + + addPresentationToCollection("meeting001", { + id: "presentation001", + name: "Presentation 001", + current: true + }); + + expect(Meteor.Presentations.insert).toHaveBeenCalledWith({ + meetingId: "meeting001", + presentation: { + id: "presentation001", + name: "Presentation 001", + current: true + }, + pointer: { + x: 0.0, + y: 0.0 + } + }); + }); + + it("should be handled correctly on calling addPresentationToCollection() when presentation is already in the collection", function (){ + spyOn(Meteor.Presentations, "findOne").and.returnValue({ _id: "dbid001" }); + + spyOn(Meteor.Presentations, "insert"); + + addPresentationToCollection("meeting001", { + id: "presentation001", + name: "Presentation 001", + current: true + }); + + expect(Meteor.Presentations.insert).not.toHaveBeenCalledWith({ + meetingId: "meeting001", + presentation: { + id: "presentation001", + name: "Presentation 001", + current: true + }, + pointer: { + x: 0.0, + y: 0.0 + } + }); + }); + + it("should be handled correctly by remove() on calling removePresentationFromCollection", function (){ + spyOn(Meteor.Presentations, "findOne").and.returnValue({ _id: "dbid001" }); + spyOn(Meteor.Presentations, "remove"); + + removePresentationFromCollection("meeting0001", "presentation001"); + + expect(Meteor.Presentations.remove).toHaveBeenCalled(); + }); + + it("should be handled correctly by remove() on calling removePresentationFromCollection", function (){ + spyOn(Meteor.Presentations, "findOne").and.returnValue(undefined); + spyOn(Meteor.Presentations, "remove"); + + removePresentationFromCollection("meeting0001", "presentation001"); + + expect(Meteor.Presentations.remove).not.toHaveBeenCalled(); + }); + + //---------------------------------------------------------------------- + // slides.coffee + //---------------------------------------------------------------------- + + // removeSlideFromCollection() + it('should not be touched on calling removeSlideFromCollection() with undefined meetingId', function () { + spyOn(Meteor.Slides, 'remove'); + removeSlideFromCollection(undefined, 'presentation001/2'); + expect(Meteor.Slides.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeSlideFromCollection() with undefined slideId', function () { + spyOn(Meteor.Slides, 'remove'); + removeSlideFromCollection('meeting001', undefined); + expect(Meteor.Slides.remove).not.toHaveBeenCalled(); + }); + it('should not be touched on calling removeSlideFromCollection() with a slide that does not exist', function () { + spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'slide001') + return undefined; + else return {meetingId: 'meeting001'}; + }); + spyOn(Meteor.Slides, 'remove'); + removeSlideFromCollection('meeting001', 'slide001'); + expect(Meteor.Slides.remove).not.toHaveBeenCalled(); + }); + it('should be handled correctly by remove() on calling removeSlideFromCollection() with an existing slide', function () { + spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'slide001') + return {_id: 'doc001'}; + else return undefined; + }); + spyOn(Meteor.Slides, 'remove'); + removeSlideFromCollection('meeting001', 'slide001'); + expect(Meteor.Slides.remove).toHaveBeenCalledWith('doc001'); + }); + + // addSlideToCollection() + it('should not be touched on calling addSlideToCollection() if the slide is already in the collection', function () { + spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'presentation001/2') + return {_id: 'doc001'}; + else return undefined; + }); + spyOn(Meteor.Slides, 'insert'); + addSlideToCollection('meeting001', 'presentation001', { + id: 'presentation001/2' + }); + expect(Meteor.Slides.insert).not.toHaveBeenCalled(); + }); + it('should be handled correctly by insert() on calling addSlideToCollection() with a brand new slide', function () { + spyOn(Meteor.Slides, 'findOne').and.callFake(function(doc) { + if(doc.meetingId === 'meeting001' && doc['slide.id'] === 'presentation001/2') + return undefined; + else return {_id: 'doc001'}; + }); + spyOn(Meteor.Slides, 'insert'); + addSlideToCollection('meeting001', 'presentation001', { + height_ratio: 100, + y_offset: 0, + num: 2, + x_offset: 0, + current: true, + png_uri: 'http://localhost/bigbluebutton/presentation/presentation001/png/2', + txt_uri: 'http://localhost/bigbluebutton/presentation/presentation001/textfiles/slide-2.txt', + id: 'presentation001/2', + width_ratio: 100, + swf_uri: 'http://localhost/bigbluebutton/presentation/presentation001/slide/2', + thumb_uri: 'http://localhost/bigbluebutton/presentation/presentation001/thumbnail/1', + }); + expect(Meteor.Slides.insert).toHaveBeenCalledWith({ + meetingId: 'meeting001', + presentationId: 'presentation001', + slide: { + height_ratio: 100, + y_offset: 0, + num: 2, + x_offset: 0, + current: true, + png_uri: 'http://localhost/bigbluebutton/presentation/presentation001/png/2', + txt_uri: 'http://localhost/bigbluebutton/presentation/presentation001/textfiles/slide-2.txt', + id: 'presentation001/2', + width_ratio: 100, + swf_uri: 'http://localhost/bigbluebutton/presentation/presentation001/slide/2', + thumb_uri: 'http://localhost/bigbluebutton/presentation/presentation001/thumbnail/1' + } + }); + }); +}); diff --git a/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js b/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js new file mode 100644 index 0000000000000000000000000000000000000000..c0f38a33d59c2513e6f8725e48b69997f7b642a9 --- /dev/null +++ b/labs/meteor-client/app/tests/jasmine/server/unit/config-stubs.js @@ -0,0 +1,11 @@ +/* + Stub the logger +*/ + +Logger = {}; +Logger.prototype = { + constructor: Logger +} +Logger.info = function() {}; +Meteor.log = Logger; + diff --git a/labs/meteor-client/grunt-instructions.txt b/labs/meteor-client/grunt-instructions.txt new file mode 100644 index 0000000000000000000000000000000000000000..90d57a5345088f9ecd952456e0761dc6395fa1c8 --- /dev/null +++ b/labs/meteor-client/grunt-instructions.txt @@ -0,0 +1,10 @@ +GRUNT + +Install all the dependencies (including grunt): +$ npm install + +Running Grunt: +-for the default: +$ grunt + + diff --git a/labs/meteor-client/log/.gitkeep b/labs/meteor-client/log/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/labs/meteor-client/package.json b/labs/meteor-client/package.json new file mode 100644 index 0000000000000000000000000000000000000000..65873d355a91fd03401c77e05639524182bf7436 --- /dev/null +++ b/labs/meteor-client/package.json @@ -0,0 +1,14 @@ +{ + "name": "BBB-HTML-CLIENT", + "scripts": { + "start": "meteor" + }, + "devDependencies": { + "coffeelint": "^1.6.0", + "grunt": "^0.4.5", + "grunt-coffeelint": "0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-jshint": "^0.10.0" + } +} +