diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/Poll.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/Poll.scala index eece8ec2d3..dbeaff5336 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/Poll.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/Poll.scala @@ -18,8 +18,8 @@ object PollFactory { private def processYesNoPollType(qType: String): Question = { val answers = new Array[Answer](2) - answers(0) = new Answer(0, "N", Some("No")) - answers(1) = new Answer(1, "Y", Some("Yes")) + answers(0) = new Answer(0, "No", Some("No")) + answers(1) = new Answer(1, "Yes", Some("Yes")) new Question(0, PollType.YesNoPollType, false, None, answers) } @@ -27,8 +27,8 @@ object PollFactory { private def processTrueFalsePollType(qType: String): Question = { val answers = new Array[Answer](2) - answers(0) = new Answer(0, "F", Some("False")) - answers(1) = new Answer(1, "T", Some("True")) + answers(0) = new Answer(0, "False", Some("False")) + answers(1) = new Answer(1, "True", Some("True")) new Question(0, PollType.TrueFalsePollType, false, None, answers) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PollApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PollApp.scala index 6ec1757ae9..94b0624748 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PollApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/PollApp.scala @@ -68,6 +68,9 @@ trait PollApp { val shape = new scala.collection.mutable.HashMap[String, Object]() shape += "num_respondents" -> new Integer(result.numRespondents) shape += "num_responders" -> new Integer(result.numResponders) + shape += "type" -> "poll_result" + shape += "id" -> result.id + shape += "status" -> "DRAW_END" val answers = new ArrayBuffer[java.util.HashMap[String, Object]]; result.answers.foreach(ans => { diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala index fdf51f5a1a..ad4658db17 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersApp.scala @@ -228,6 +228,7 @@ trait UsersApp { } usersModel.removeUser(msg.userId) + usersModel.removeRegUser(msg.userId) log.info("Ejecting user from meeting: mid=[" + mProps.meetingID + "]uid=[" + msg.userId + "]") outGW.send(new UserEjectedFromMeeting(mProps.meetingID, mProps.recorded, msg.userId, msg.ejectedBy)) @@ -273,7 +274,20 @@ trait UsersApp { def handleUserJoin(msg: UserJoining): Unit = { val regUser = usersModel.getRegisteredUserWithToken(msg.authToken) regUser foreach { ru => - val vu = new VoiceUser(msg.userID, msg.userID, ru.name, ru.name, false, false, false, false) + // if there was a phoneUser with the same userID, reuse the VoiceUser value object + val vu = usersModel.getUser(msg.userID) match { + case Some(u) => { + if (u.voiceUser.joined) { + u.voiceUser.copy() + } else { + new VoiceUser(msg.userID, msg.userID, ru.name, ru.name, false, false, false, false) + } + } + case None => { + new VoiceUser(msg.userID, msg.userID, ru.name, ru.name, false, false, false, false) + } + } + val uvo = new UserVO(msg.userID, ru.externId, ru.name, ru.role, raiseHand = false, presenter = false, hasStream = false, locked = getInitialLockStatus(ru.role), @@ -289,7 +303,7 @@ trait UsersApp { outGW.send(new MeetingState(mProps.meetingID, mProps.recorded, uvo.userID, meetingModel.getPermissions(), meetingModel.isMeetingMuted())) // Become presenter if the only moderator - if (usersModel.numModerators == 1) { + if ((usersModel.numModerators == 1) || (usersModel.noPresenter())) { if (ru.role == Role.MODERATOR) { assignNewPresenter(msg.userID, ru.name, msg.userID) } @@ -312,14 +326,18 @@ trait UsersApp { /* The current presenter has left the meeting. Find a moderator and make * him presenter. This way, if there is a moderator in the meeting, there - * will always be a presenter. + * will always be a presenter. */ val moderator = usersModel.findAModerator() moderator.foreach { mod => log.info("Presenter left meeting: mid=[" + mProps.meetingID + "] uid=[" + u.userID + "]. Making user=[" + mod.userID + "] presenter.") assignNewPresenter(mod.userID, mod.name, mod.userID) } - + } + // add VoiceUser again to the list as a phone user since we still didn't get the event from FreeSWITCH + val vu = u.voiceUser + if (vu.joined) { + this.context.self ! (new UserJoinedVoiceConfMessage(mProps.voiceBridge, vu.userId, msg.userID, vu.callerName, vu.callerNum, vu.muted, vu.talking)); } } @@ -338,12 +356,16 @@ trait UsersApp { log.info("Voice user=[" + msg.voiceUserId + "] is already in conf=[" + mProps.voiceBridge + "]. Must be duplicate message.") } case None => { - // No current web user. This means that the user called in through - // the phone. We need to generate a new user as we are not able - // to match with a web user. - val webUserId = usersModel.generateWebUserId + val webUserId = if (msg.userId != msg.callerIdName) { + msg.userId + } else { + // No current web user. This means that the user called in through + // the phone. We need to generate a new user as we are not able + // to match with a web user. + usersModel.generateWebUserId + } val vu = new VoiceUser(msg.voiceUserId, webUserId, msg.callerIdName, msg.callerIdNum, - true, false, false, false) + true, false, msg.muted, msg.talking) val sessionId = "PHONE-" + webUserId; diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala index b06c56eea7..9fe3bdd34d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/UsersModel.scala @@ -96,6 +96,10 @@ class UsersModel { uservos.values find (u => u.role == MODERATOR) } + def noPresenter(): Boolean = { + !getCurrentPresenter().isDefined + } + def getCurrentPresenter(): Option[UserVO] = { uservos.values find (u => u.presenter == true) } @@ -127,4 +131,17 @@ class UsersModel { def getViewers(): Array[UserVO] = { uservos.values filter (u => u.role == VIEWER) toArray } + + def getRegisteredUserWithUserID(userID: String): Option[RegisteredUser] = { + regUsers.values find (ru => userID contains ru.id) + } + + def removeRegUser(userID: String) { + getRegisteredUserWithUserID(userID) match { + case Some(ru) => { + regUsers -= ru.authToken + } + case None => + } + } } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/BigBlueButtonApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/BigBlueButtonApplication.java index effb9a71a1..0bb6eac33c 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/BigBlueButtonApplication.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/BigBlueButtonApplication.java @@ -42,6 +42,8 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { private ConnectionInvokerService connInvokerService; private MessagePublisher red5InGW; + private final UserConnectionMapper userConnections = new UserConnectionMapper(); + private final String APP = "BBB"; private final String CONN = "RED5-"; @@ -166,6 +168,8 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { log.info("User joining bbb-apps: data={}", logStr); + userConnections.addUserConnection(userId, connId); + return super.roomConnect(connection, params); } @@ -214,10 +218,15 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { Gson gson = new Gson(); String logStr = gson.toJson(logData); - log.info("User leaving bbb-apps: data={}", logStr); - - red5InGW.userLeft(bbbSession.getRoom(), getBbbSession().getInternalUserID(), sessionId); - + boolean removeUser = userConnections.userDisconnected(userId, connId); + + if (removeUser) { + log.info("User leaving bbb-apps: data={}", logStr); + red5InGW.userLeft(bbbSession.getRoom(), getBbbSession().getInternalUserID(), sessionId); + } else { + log.info("User not leaving bbb-apps but just disconnected: data={}", logStr); + } + super.roomDisconnect(conn); } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/UserConnectionMapper.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/UserConnectionMapper.java new file mode 100755 index 0000000000..7c115a8c78 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/red5/UserConnectionMapper.java @@ -0,0 +1,70 @@ +package org.bigbluebutton.red5; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * This class maintains the connections mapping of a user. + * This tracks the connections for a user to manage auto-reconnects. + * @author ralam + * + */ +public class UserConnectionMapper { + + private ConcurrentMap users = new ConcurrentHashMap(8, 0.9f, 1);; + + /** + * Adds a connection for a user. + * @param userId + * @param connId + */ + public synchronized void addUserConnection(String userId, String connId) { + if (users.containsKey(userId)) { + UserConnection user = users.get(userId); + user.add(connId); + } else { + UserConnection user = new UserConnection(); + user.add(connId); + users.put(userId, user); + } + } + + /** + * Removed a connection for a user. Returns true if the user doesn't have any + * connection left and thus can be removed. + * @param userId + * @param connId + * @return boolean - no more connections + */ + public synchronized boolean userDisconnected(String userId, String connId) { + if (users.containsKey(userId)) { + UserConnection user = users.get(userId); + user.remove(connId); + if (user.isEmpty()) { + users.remove(userId); + return true; + } + return false; + } + + return true; + } + + private class UserConnection { + private final Set connections = new HashSet(); + + public void add(String connId) { + connections.add(connId); + } + + public void remove(String connId) { + connections.remove(connId); + } + + public boolean isEmpty() { + return connections.isEmpty(); + } + } +} diff --git a/bigbluebutton-client/branding/default/style/css/BBBDefault.css b/bigbluebutton-client/branding/default/style/css/BBBDefault.css index 1d325e9072..41f50a4d42 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBDefault.css +++ b/bigbluebutton-client/branding/default/style/css/BBBDefault.css @@ -855,6 +855,7 @@ Alert { successImage: Embed(source='assets/images/status_success.png'); warningImage: Embed(source='assets/images/status_warning.png'); failImage: Embed(source='assets/images/status_fail.png'); + refreshImage: Embed(source='assets/images/status_refresh.png'); } .warningButtonStyle { @@ -871,3 +872,8 @@ Alert { fontSize: 12; paddingTop: 0; } + +.statusTimeStyle { + fontSize: 10; + paddingTop: 0; +} diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt b/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt index 9cbdc68e81..952a5f9763 100755 --- a/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt @@ -35,4 +35,7 @@ purchase a royalty-free license. I'm unavailable for custom icon design work. But your suggestions are always welcome! -==================== \ No newline at end of file +==================== +Some of the client icons were generated using the following online tool: + +http://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=clipart&foreground.space.trim=1&foreground.space.pad=0.15&foreground.clipart=res%2Fclipart%2Ficons%2Fnavigation_refresh.svg&foreColor=4b4b4b%2C0&crop=0&backgroundShape=none&backColor=ffffff%2C100&effects=none diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/status_refresh.png b/bigbluebutton-client/branding/default/style/css/assets/images/status_refresh.png new file mode 100644 index 0000000000..77d6a8bd12 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/status_refresh.png differ diff --git a/bigbluebutton-client/libs/as3commons-logging-2.7.swc b/bigbluebutton-client/libs/as3commons-logging-2.7.swc new file mode 100644 index 0000000000..d02d2f8171 Binary files /dev/null and b/bigbluebutton-client/libs/as3commons-logging-2.7.swc differ diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties index 87fe5b7232..f31270ce3e 100755 --- a/bigbluebutton-client/locale/en_US/bbbResources.properties +++ b/bigbluebutton-client/locale/en_US/bbbResources.properties @@ -235,6 +235,7 @@ bbb.chat.minimizeBtn.accessibilityName = Minimize the Chat Window bbb.chat.maximizeRestoreBtn.accessibilityName = Maximize the Chat Window bbb.chat.closeBtn.accessibilityName = Close the Chat Window bbb.chat.chatTabs.accessibleNotice = New messages in this tab. +bbb.chat.chatMessage.systemMessage = System bbb.publishVideo.changeCameraBtn.labelText = Change Webcam bbb.publishVideo.changeCameraBtn.toolTip = Open the change webcam dialog box bbb.publishVideo.cmbResolution.tooltip = Select a webcam resolution @@ -344,6 +345,13 @@ bbb.logout.confirm.title = Confirm Logout bbb.logout.confirm.message = Are you sure you want to log out? bbb.logout.confirm.yes = Yes bbb.logout.confirm.no = No +bbb.connection.failure=Network failure +bbb.connection.reconnecting=Reconnecting +bbb.connection.reestablished=Connection reestablished +bbb.connection.bigbluebutton=BigBlueButton +bbb.connection.sip=SIP +bbb.connection.video=Video +bbb.connection.deskshare=Deskshare bbb.notes.title = Notes bbb.notes.cmpColorPicker.toolTip = Text Color bbb.notes.saveBtn = Save @@ -496,10 +504,10 @@ bbb.polling.closeButton.label = Close bbb.polling.pollModal.title = Poll bbb.polling.respondersLabel.novotes = No Users Responded bbb.polling.respondersLabel.text = {0} Users Responded -bbb.polling.answer.Y = Yes -bbb.polling.answer.N = No -bbb.polling.answer.T = True -bbb.polling.answer.F = False +bbb.polling.answer.Yes = Yes +bbb.polling.answer.No = No +bbb.polling.answer.True = True +bbb.polling.answer.False = False bbb.polling.answer.A = A bbb.polling.answer.B = B bbb.polling.answer.C = C @@ -507,6 +515,8 @@ bbb.polling.answer.D = D bbb.polling.answer.E = E bbb.polling.answer.F = F bbb.polling.answer.G = G +bbb.polling.results.accessible.header = Poll Results. +bbb.polling.results.accessible.answer = Answer {0} had {1} votes. bbb.publishVideo.startPublishBtn.labelText = Start Sharing bbb.publishVideo.changeCameraBtn.labelText = Change Webcam Settings diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template index 04957d6c39..f98b1f7f90 100755 --- a/bigbluebutton-client/resources/config.xml.template +++ b/bigbluebutton-client/resources/config.xml.template @@ -16,6 +16,7 @@ showLogoutWindow="true" showLayoutTools="true" confirmLogout="true" showRecordingNotification="true"/> + diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html index 87d1161447..aa97d4a197 100755 --- a/bigbluebutton-client/resources/prod/BigBlueButton.html +++ b/bigbluebutton-client/resources/prod/BigBlueButton.html @@ -74,6 +74,7 @@ +