You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2018/02/25 15:01:59 UTC

[openmeetings] 04/04: [OPENMEETINGS-1791] quick polls seems to work

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git

commit 202b74f41a90ff64c57d5f15a99f555212353f5e
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Sun Feb 25 21:46:42 2018 +0700

    [OPENMEETINGS-1791] quick polls seems to work
---
 .../openmeetings/db/util/ws/RoomMessage.java       |   1 +
 openmeetings-web/pom.xml                           |   3 +
 .../openmeetings/web/app/QuickPollManager.java     | 110 ++++
 .../apache/openmeetings/web/room/RoomPanel.html    |   9 +
 .../apache/openmeetings/web/room/RoomPanel.java    |  14 +-
 .../openmeetings/web/room/menu/PollsSubMenu.java   | 174 ++++++
 .../openmeetings/web/room/menu/RoomMenuPanel.java  |  96 +---
 .../org/apache/openmeetings/web/room/room-base.js  | 598 ++-------------------
 .../openmeetings/web/room/sidebar/RoomSidebar.java |   2 +-
 .../apache/openmeetings/web/room/video-manager.js  | 169 ++++++
 .../org/apache/openmeetings/web/room/video-util.js | 111 ++++
 .../java/org/apache/openmeetings/web/room/video.js | 268 +++++++++
 openmeetings-web/src/main/webapp/css/wb.css        |  26 +
 13 files changed, 949 insertions(+), 632 deletions(-)

diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
index 1e4af48..3c57781 100644
--- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
+++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/util/ws/RoomMessage.java
@@ -58,6 +58,7 @@ public class RoomMessage implements IWebSocketPushMessage {
 		, audioActivity //user speaks
 		, mute
 		, exclusive
+		, quickPollUpdated
 	}
 	private final Date timestamp;
 	private final String uid;
diff --git a/openmeetings-web/pom.xml b/openmeetings-web/pom.xml
index 404788e..0a77370 100644
--- a/openmeetings-web/pom.xml
+++ b/openmeetings-web/pom.xml
@@ -213,6 +213,9 @@
 							<jsSourceDir>../java/org/apache/openmeetings/web/room</jsSourceDir>
 							<jsSourceFiles>
 								<jsSourceFile>jquery.dialogextend.js</jsSourceFile>
+								<jsSourceFile>video-util.js</jsSourceFile>
+								<jsSourceFile>video.js</jsSourceFile>
+								<jsSourceFile>video-manager.js</jsSourceFile>
 								<jsSourceFile>room-base.js</jsSourceFile>
 							</jsSourceFiles>
 							<jsFinalFile>room.js</jsFinalFile>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/QuickPollManager.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/QuickPollManager.java
new file mode 100644
index 0000000..b143603
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/QuickPollManager.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.app;
+
+import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKey;
+import static org.apache.openmeetings.web.app.Application.getHazelcast;
+import static org.apache.openmeetings.web.app.WebSession.getUserId;
+import static org.red5.logging.Red5LoggerFactory.getLogger;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.openmeetings.core.util.WebSocketHelper;
+import org.apache.openmeetings.db.entity.basic.Client;
+import org.apache.openmeetings.db.entity.room.Room;
+import org.apache.openmeetings.db.util.ws.RoomMessage.Type;
+import org.apache.openmeetings.db.util.ws.TextRoomMessage;
+import org.slf4j.Logger;
+import org.springframework.stereotype.Component;
+
+import com.github.openjson.JSONObject;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.IMap;
+
+@Component
+public class QuickPollManager {
+	private static final Logger log = getLogger(QuickPollManager.class, getWebAppRootKey());
+	private static final String QPOLLS_KEY = "QPOLLS_KEY";
+	private HazelcastInstance hazelcast;
+
+	@PostConstruct
+	private void init() {
+		this.hazelcast = getHazelcast();
+	}
+
+	private IMap<Long, Map<Long, Boolean>> map() {
+		return hazelcast.getMap(QPOLLS_KEY);
+	}
+
+	public boolean isStarted(Long roomId) {
+		return map().containsKey(roomId);
+	}
+
+	public void start(Client c) {
+		Long roomId = c.getRoomId();
+		if (!c.hasRight(Room.Right.presenter) || isStarted(roomId)) {
+			return;
+		}
+		log.debug("Starting quick poll, room: {}", roomId);
+		IMap<Long, Map<Long, Boolean>> polls = map();
+		polls.lock(roomId);
+		polls.putIfAbsent(roomId, new ConcurrentHashMap<Long, Boolean>());
+		polls.unlock(roomId);
+		WebSocketHelper.sendRoom(new TextRoomMessage(roomId, c, Type.quickPollUpdated, c.getUid()));
+	}
+
+	public void vote(Client c, boolean vote) {
+		Long roomId = c.getRoomId();
+		IMap<Long, Map<Long, Boolean>> polls = map();
+		polls.lock(roomId);
+		if (polls.containsKey(roomId)) {
+			Map<Long, Boolean> votes = map().get(roomId);
+			if (!votes.containsKey(c.getUserId())) {
+				votes.put(c.getUserId(), vote);
+				polls.put(roomId,  votes);
+				WebSocketHelper.sendRoom(new TextRoomMessage(roomId, c, Type.quickPollUpdated, c.getUid()));
+			}
+		}
+		polls.unlock(roomId);
+	}
+
+	public void close(Client c) {
+		Long roomId = c.getRoomId();
+		if (!c.hasRight(Room.Right.presenter) || !isStarted(roomId)) {
+			return;
+		}
+		map().remove(roomId);
+		WebSocketHelper.sendRoom(new TextRoomMessage(roomId, c, Type.quickPollUpdated, c.getUid()));
+	}
+
+	public JSONObject toJson(Long roomId) {
+		boolean started = isStarted(roomId);
+		JSONObject o = new JSONObject().put("started", started);
+		if (started) {
+			Map<Long, Boolean> votes = map().get(roomId);
+			o.put("voted", votes.containsKey(getUserId()));
+			o.put("pros", votes.entrySet().stream().filter(e -> e.getValue()).count())
+				.put("cons", votes.entrySet().stream().filter(e -> !e.getValue()).count());
+		}
+		return o;
+	}
+}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.html b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.html
index 2b7a90a..d6c38c8 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.html
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.html
@@ -75,6 +75,15 @@
 		<div id="clipboard-dialog" wicket:message="title:1121,data-btn-ok:54">
 			<p><span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span><span class="text"></span></p>
 		</div>
+		<div id="quick-vote-template">
+			<div class="close clickable"><wicket:message key="85" /></div>
+			<div class="control pro">
+				<span class="badge">0</span>
+			</div>
+			<div class="control con">
+				<span class="badge">0</span>
+			</div>
+		</div>
 	</div>
 </wicket:panel>
 </html>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
index 29ddfc4..2da8a6c 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/RoomPanel.java
@@ -58,6 +58,7 @@ import org.apache.openmeetings.db.util.ws.RoomMessage.Type;
 import org.apache.openmeetings.db.util.ws.TextRoomMessage;
 import org.apache.openmeetings.util.NullStringer;
 import org.apache.openmeetings.web.app.ClientManager;
+import org.apache.openmeetings.web.app.QuickPollManager;
 import org.apache.openmeetings.web.app.StreamClientManager;
 import org.apache.openmeetings.web.app.WebSession;
 import org.apache.openmeetings.web.common.BasePanel;
@@ -139,7 +140,8 @@ public class RoomPanel extends BasePanel {
 			}
 			StringBuilder sb = new StringBuilder("Room.init(").append(options.toString(new NullStringer())).append(");")
 					.append(wb.getInitScript())
-					.append("Room.setSize();");
+					.append("Room.setSize();")
+					.append(getQuickPollJs());
 			target.appendJavaScript(sb);
 			WebSocketHelper.sendRoom(new RoomMessage(r.getId(), _c, RoomMessage.Type.roomEnter));
 			// play video from other participants
@@ -618,12 +620,22 @@ public class RoomPanel extends BasePanel {
 						handler.appendJavaScript(String.format("if (typeof(VideoManager) !== 'undefined') {VideoManager.exclusive('%s');}", uid));
 					}
 						break;
+					case quickPollUpdated:
+					{
+						menu.update(handler);
+						handler.appendJavaScript(getQuickPollJs());
+					}
+						break;
 				}
 			}
 		}
 		super.onEvent(event);
 	}
 
+	private String getQuickPollJs() {
+		return String.format("Room.quickPoll(%s);", getBean(QuickPollManager.class).toJson(r.getId()));
+	}
+
 	private void updateInterviewRecordingButtons(IPartialPageRequestHandler handler) {
 		Client _c = getClient();
 		if (interview && _c.hasRight(Right.moderator)) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/PollsSubMenu.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/PollsSubMenu.java
new file mode 100644
index 0000000..15bdcc7
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/PollsSubMenu.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") +  you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openmeetings.web.room.menu;
+
+import static org.apache.openmeetings.util.OpenmeetingsVariables.getWebAppRootKey;
+import static org.apache.openmeetings.web.app.Application.getBean;
+import static org.apache.openmeetings.web.app.WebSession.getUserId;
+import static org.apache.openmeetings.web.room.sidebar.RoomSidebar.PARAM_ACTION;
+import static org.apache.openmeetings.web.util.CallbackFunctionHelper.getNamedFunction;
+import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit;
+
+import java.io.Serializable;
+
+import org.apache.openmeetings.db.dao.room.PollDao;
+import org.apache.openmeetings.db.entity.basic.Client;
+import org.apache.openmeetings.db.entity.room.Room;
+import org.apache.openmeetings.db.entity.room.Room.RoomElement;
+import org.apache.openmeetings.db.entity.room.RoomPoll;
+import org.apache.openmeetings.web.app.QuickPollManager;
+import org.apache.openmeetings.web.common.menu.RoomMenuItem;
+import org.apache.openmeetings.web.room.RoomPanel;
+import org.apache.openmeetings.web.room.poll.CreatePollDialog;
+import org.apache.openmeetings.web.room.poll.PollResultsDialog;
+import org.apache.openmeetings.web.room.poll.VoteDialog;
+import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.PriorityHeaderItem;
+import org.red5.logging.Red5LoggerFactory;
+import org.slf4j.Logger;
+
+public class PollsSubMenu implements Serializable {
+	private static final long serialVersionUID = 1L;
+	private static final Logger log = Red5LoggerFactory.getLogger(PollsSubMenu.class, getWebAppRootKey());
+	private static final String FUNC_QPOLL_ACTION = "quickPollAction";
+	private static final String PARAM_VOTE = "vote";
+	private static final String ACTION_CLOSE = "close";
+	private final RoomPanel room;
+	private final RoomMenuPanel mp;
+	private final CreatePollDialog createPoll;
+	private final VoteDialog vote;
+	private final PollResultsDialog pollResults;
+	private RoomMenuItem pollsMenu;
+	private RoomMenuItem pollQuickMenuItem;
+	private RoomMenuItem pollCreateMenuItem;
+	private RoomMenuItem pollVoteMenuItem;
+	private RoomMenuItem pollResultMenuItem;
+	private final AbstractDefaultAjaxBehavior quickPollAction = new AbstractDefaultAjaxBehavior() {
+		private static final long serialVersionUID = 1L;
+
+		@Override
+		protected void respond(AjaxRequestTarget target) {
+			try {
+				String action = mp.getRequest().getRequestParameters().getParameterValue(PARAM_ACTION).toString();
+				QuickPollManager qm = getBean(QuickPollManager.class);
+				Client c = room.getClient();
+				if (ACTION_CLOSE.equals(action)) {
+					qm.close(c);
+				} else if (PARAM_VOTE.equals(action)) {
+					boolean vote = mp.getRequest().getRequestParameters().getParameterValue(PARAM_VOTE).toBoolean();
+					qm.vote(c, vote);
+				}
+			} catch (Exception e) {
+				log.error("Unexpected exception while toggle 'quickPollAction'", e);
+			}
+		}
+	};
+
+	public PollsSubMenu(final RoomPanel room, final RoomMenuPanel mp) {
+		this.room = room;
+		this.mp = mp;
+		mp.add(createPoll = new CreatePollDialog("createPoll", room.getRoom().getId()));
+		mp.add(vote = new VoteDialog("vote"));
+		mp.add(pollResults = new PollResultsDialog("pollResults", room.getRoom().getId()));
+	}
+
+	public void init() {
+		pollsMenu = new RoomMenuItem(mp.getString("menu.polls"), null, false);
+		pollQuickMenuItem = new RoomMenuItem(mp.getString("menu.polls.quick.title"), mp.getString("menu.polls.quick.descr"), false) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				getBean(QuickPollManager.class).start(room.getClient());
+			}
+		};
+		pollCreateMenuItem = new RoomMenuItem(mp.getString("24"), mp.getString("1483"), false) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				createPoll.updateModel(target);
+				createPoll.open(target);
+			}
+		};
+		pollVoteMenuItem = new RoomMenuItem(mp.getString("32"), mp.getString("1485"), false) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				RoomPoll rp = getBean(PollDao.class).getByRoom(room.getRoom().getId());
+				if (rp != null) {
+					vote.updateModel(target, rp);
+					vote.open(target);
+				}
+			}
+		};
+		pollResultMenuItem = new RoomMenuItem(mp.getString("37"), mp.getString("1484"), false) {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void onClick(AjaxRequestTarget target) {
+				pollResults.updateModel(target, room.getClient().hasRight(Room.Right.moderator));
+				pollResults.open(target);
+			}
+		};
+		mp.add(quickPollAction);
+	}
+
+	RoomMenuItem getMenu() {
+		pollsMenu.setTop(true);
+		pollsMenu.getItems().add(pollQuickMenuItem);
+		pollsMenu.getItems().add(pollCreateMenuItem);
+		pollsMenu.getItems().add(pollResultMenuItem);
+		pollsMenu.getItems().add(pollVoteMenuItem);
+		return pollsMenu;
+	}
+
+	public void update(final boolean moder, final boolean notExternalUser, final Room r) {
+		PollDao pollDao = getBean(PollDao.class);
+		boolean pollExists = pollDao.hasPoll(r.getId());
+		pollsMenu.setEnabled((moder && !r.isHidden(RoomElement.ActionMenu)) || (!moder && r.isAllowUserQuestions()));
+		pollQuickMenuItem.setEnabled(room.getClient().hasRight(Room.Right.presenter) && !getBean(QuickPollManager.class).isStarted(r.getId()));
+		pollCreateMenuItem.setEnabled(moder);
+		pollVoteMenuItem.setEnabled(pollExists && notExternalUser && !pollDao.hasVoted(r.getId(), getUserId()));
+		pollResultMenuItem.setEnabled(pollExists || !pollDao.getArchived(r.getId()).isEmpty());
+	}
+
+	public void updatePoll(IPartialPageRequestHandler handler, Long createdBy) {
+		RoomPoll rp = getBean(PollDao.class).getByRoom(room.getRoom().getId());
+		if (rp != null) {
+			vote.updateModel(handler, rp);
+		} else {
+			vote.close(handler, null);
+		}
+		if (createdBy != null && !getUserId().equals(createdBy)) {
+			vote.open(handler);
+		}
+		if (pollResults.isOpened()) {
+			pollResults.updateModel(handler, room.getClient().hasRight(Room.Right.moderator));
+		}
+	}
+
+	public void renderHead(IHeaderResponse response) {
+		response.render(new PriorityHeaderItem(getNamedFunction(FUNC_QPOLL_ACTION, quickPollAction, explicit(PARAM_ACTION), explicit(PARAM_VOTE))));
+	}
+}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
index d3aa885..e3a2f1a 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/menu/RoomMenuPanel.java
@@ -27,7 +27,6 @@ import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_REDIRECT
 import static org.apache.openmeetings.util.OpenmeetingsVariables.getBaseUrl;
 import static org.apache.openmeetings.util.OpenmeetingsVariables.isSipEnabled;
 import static org.apache.openmeetings.web.app.Application.getBean;
-import static org.apache.openmeetings.web.app.WebSession.getUserId;
 import static org.apache.openmeetings.web.util.GroupLogoResourceReference.getUrl;
 import static org.apache.openmeetings.web.util.OmUrlFragment.ROOMS_PUBLIC;
 
@@ -37,11 +36,9 @@ import java.util.List;
 import org.apache.commons.lang3.time.FastDateFormat;
 import org.apache.openmeetings.core.util.WebSocketHelper;
 import org.apache.openmeetings.db.dao.basic.ConfigurationDao;
-import org.apache.openmeetings.db.dao.room.PollDao;
 import org.apache.openmeetings.db.entity.basic.Client;
 import org.apache.openmeetings.db.entity.room.Room;
 import org.apache.openmeetings.db.entity.room.Room.RoomElement;
-import org.apache.openmeetings.db.entity.room.RoomPoll;
 import org.apache.openmeetings.db.entity.user.Group;
 import org.apache.openmeetings.db.entity.user.User;
 import org.apache.openmeetings.db.util.ws.RoomMessage.Type;
@@ -55,12 +52,10 @@ import org.apache.openmeetings.web.common.menu.MenuPanel;
 import org.apache.openmeetings.web.common.menu.RoomMenuItem;
 import org.apache.openmeetings.web.room.OmRedirectTimerBehavior;
 import org.apache.openmeetings.web.room.RoomPanel;
-import org.apache.openmeetings.web.room.poll.CreatePollDialog;
-import org.apache.openmeetings.web.room.poll.PollResultsDialog;
-import org.apache.openmeetings.web.room.poll.VoteDialog;
 import org.apache.wicket.AttributeModifier;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
+import org.apache.wicket.markup.head.IHeaderResponse;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.Model;
@@ -72,9 +67,6 @@ import com.googlecode.wicket.jquery.ui.widget.menu.IMenuItem;
 public class RoomMenuPanel extends Panel {
 	private static final long serialVersionUID = 1L;
 	private final InvitationDialog invite;
-	private final CreatePollDialog createPoll;
-	private final VoteDialog vote;
-	private final PollResultsDialog pollResults;
 	private final SipDialerDialog sipDialer;
 	private MenuPanel menuPanel;
 	private final StartSharingButton shareBtn;
@@ -100,16 +92,11 @@ public class RoomMenuPanel extends Panel {
 	private RoomMenuItem exitMenuItem;
 	private RoomMenuItem filesMenu;
 	private RoomMenuItem actionsMenu;
-	private RoomMenuItem pollsMenu;
 	private RoomMenuItem inviteMenuItem;
 	private RoomMenuItem shareMenuItem;
 	private RoomMenuItem applyModerMenuItem;
 	private RoomMenuItem applyWbMenuItem;
 	private RoomMenuItem applyAvMenuItem;
-	private RoomMenuItem pollQuickMenuItem;
-	private RoomMenuItem pollCreateMenuItem;
-	private RoomMenuItem pollVoteMenuItem;
-	private RoomMenuItem pollResultMenuItem;
 	private RoomMenuItem sipDialerMenuItem;
 	private RoomMenuItem downloadPngMenuItem;
 	private RoomMenuItem downloadJpgMenuItem;
@@ -122,6 +109,7 @@ public class RoomMenuPanel extends Panel {
 			return getUrl(getRequestCycle(), getGroup().getId());
 		}
 	};
+	private final PollsSubMenu pollsSubMenu;
 
 	public RoomMenuPanel(String id, final RoomPanel room) {
 		super(id);
@@ -137,10 +125,8 @@ public class RoomMenuPanel extends Panel {
 		RoomInvitationForm rif = new RoomInvitationForm("form", room.getRoom().getId());
 		add(invite = new InvitationDialog("invite", rif));
 		rif.setDialog(invite);
-		add(createPoll = new CreatePollDialog("createPoll", room.getRoom().getId()));
-		add(vote = new VoteDialog("vote"));
-		add(pollResults = new PollResultsDialog("pollResults", room.getRoom().getId()));
 		add(sipDialer = new SipDialerDialog("sipDialer", room));
+		pollsSubMenu = new PollsSubMenu(room, this);
 	}
 
 	private Group getGroup() {
@@ -162,8 +148,7 @@ public class RoomMenuPanel extends Panel {
 		};
 		filesMenu = new RoomMenuItem(getString("245"), null, false);
 		actionsMenu = new RoomMenuItem(getString("635"), null, false);
-		pollsMenu = new RoomMenuItem(getString("menu.polls"), null, false);
-
+		pollsSubMenu.init();
 		inviteMenuItem = new RoomMenuItem(getString("213"), getString("1489"), false) {
 			private static final long serialVersionUID = 1L;
 
@@ -205,45 +190,6 @@ public class RoomMenuPanel extends Panel {
 				room.requestRight(Room.Right.video, target);
 			}
 		};
-		pollQuickMenuItem = new RoomMenuItem(getString("menu.polls.quick.title"), getString("menu.polls.quick.descr"), false) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void onClick(AjaxRequestTarget target) {
-				//createPoll.updateModel(target);
-				//createPoll.open(target);
-			}
-		};
-		pollCreateMenuItem = new RoomMenuItem(getString("24"), getString("1483"), false) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void onClick(AjaxRequestTarget target) {
-				createPoll.updateModel(target);
-				createPoll.open(target);
-			}
-		};
-		pollVoteMenuItem = new RoomMenuItem(getString("32"), getString("1485"), false) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void onClick(AjaxRequestTarget target) {
-				RoomPoll rp = getBean(PollDao.class).getByRoom(room.getRoom().getId());
-				if (rp != null) {
-					vote.updateModel(target, rp);
-					vote.open(target);
-				}
-			}
-		};
-		pollResultMenuItem = new RoomMenuItem(getString("37"), getString("1484"), false) {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void onClick(AjaxRequestTarget target) {
-				pollResults.updateModel(target, room.getClient().hasRight(Room.Right.moderator));
-				pollResults.open(target);
-			}
-		};
 		sipDialerMenuItem = new RoomMenuItem(getString("1447"), getString("1488"), false) {
 			private static final long serialVersionUID = 1L;
 
@@ -300,6 +246,12 @@ public class RoomMenuPanel extends Panel {
 		super.onInitialize();
 	}
 
+	@Override
+	public void renderHead(IHeaderResponse response) {
+		super.renderHead(response);
+		pollsSubMenu.renderHead(response);
+	}
+
 	private List<IMenuItem> getMenu() {
 		List<IMenuItem> menu = new ArrayList<>();
 		exitMenuItem.setEnabled(false);
@@ -327,12 +279,7 @@ public class RoomMenuPanel extends Panel {
 		actionsMenu.getItems().add(downloadPdfMenuItem);
 		menu.add(actionsMenu);
 
-		pollsMenu.setTop(true);
-		pollsMenu.getItems().add(pollQuickMenuItem);
-		pollsMenu.getItems().add(pollCreateMenuItem);
-		pollsMenu.getItems().add(pollResultMenuItem);
-		pollsMenu.getItems().add(pollVoteMenuItem);
-		menu.add(pollsMenu);
+		menu.add(pollsSubMenu.getMenu());
 		return menu;
 	}
 
@@ -345,25 +292,19 @@ public class RoomMenuPanel extends Panel {
 		downloadPngMenuItem.setEnabled(!isInterview);
 		downloadJpgMenuItem.setEnabled(!isInterview);
 		downloadPdfMenuItem.setEnabled(!isInterview);
-		PollDao pollDao = getBean(PollDao.class);
-		boolean pollExists = pollDao.hasPoll(r.getId());
 		User u = room.getClient().getUser();
 		boolean notExternalUser = u.getType() != User.Type.contact;
 		exitMenuItem.setEnabled(notExternalUser);
 		filesMenu.setEnabled(!isInterview && room.getSidebar().isShowFiles());
 		boolean moder = room.getClient().hasRight(Room.Right.moderator);
+		pollsSubMenu.update(moder, notExternalUser, r);
 		actionsMenu.setEnabled((moder && !r.isHidden(RoomElement.ActionMenu)) || (!moder && r.isAllowUserQuestions()));
-		pollsMenu.setEnabled((moder && !r.isHidden(RoomElement.ActionMenu)) || (!moder && r.isAllowUserQuestions()));
 		inviteMenuItem.setEnabled(notExternalUser && moder);
 		boolean shareVisible = room.screenShareAllowed();
 		shareMenuItem.setEnabled(shareVisible);
 		applyModerMenuItem.setEnabled(!moder);
 		applyWbMenuItem.setEnabled(!room.getClient().hasRight(Room.Right.whiteBoard));
 		applyAvMenuItem.setEnabled(!room.getClient().hasRight(Room.Right.audio) || !room.getClient().hasRight(Room.Right.video));
-		pollQuickMenuItem.setEnabled(moder);
-		pollCreateMenuItem.setEnabled(moder);
-		pollVoteMenuItem.setEnabled(pollExists && notExternalUser && !pollDao.hasVoted(r.getId(), getUserId()));
-		pollResultMenuItem.setEnabled(pollExists || !pollDao.getArchived(r.getId()).isEmpty());
 		sipDialerMenuItem.setEnabled(r.isSipEnabled() && isSipEnabled());
 		menuPanel.update(handler);
 		StringBuilder roomClass = new StringBuilder("room name");
@@ -392,18 +333,7 @@ public class RoomMenuPanel extends Panel {
 	}
 
 	public void updatePoll(IPartialPageRequestHandler handler, Long createdBy) {
-		RoomPoll rp = getBean(PollDao.class).getByRoom(room.getRoom().getId());
-		if (rp != null) {
-			vote.updateModel(handler, rp);
-		} else {
-			vote.close(handler, null);
-		}
-		if (createdBy != null && !getUserId().equals(createdBy)) {
-			vote.open(handler);
-		}
-		if (pollResults.isOpened()) {
-			pollResults.updateModel(handler, room.getClient().hasRight(Room.Right.moderator));
-		}
+		pollsSubMenu.updatePoll(handler, createdBy);
 		update(handler);
 	}
 
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/room-base.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/room-base.js
index a150835..d89c851 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/room-base.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/room-base.js
@@ -1,549 +1,4 @@
 /* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
-const WB_AREA_SEL = '.room.wb.area';
-const WBA_WB_SEL = '.room.wb.area .ui-tabs-panel.ui-corner-bottom.ui-widget-content:visible';
-var WBA_SEL = WB_AREA_SEL;
-const VID_SEL = '.video.user-video';
-var VideoUtil = (function() {
-	const self = {};
-	function _getVid(uid) {
-		return "video" + uid;
-	}
-	function _isSharing(c) {
-		return 'sharing' === c.type && c.screenActivities.indexOf('sharing') > -1;
-	}
-	function _isRecording(c) {
-		return 'sharing' === c.type
-			&& c.screenActivities.indexOf('recording') > -1
-			&& c.screenActivities.indexOf('sharing') < 0;
-	}
-	function _hasAudio(c) {
-		return c.activities.indexOf('broadcastA') > -1;
-	}
-	function _hasVideo(c) {
-		return c.activities.indexOf('broadcastV') > -1;
-	}
-	function _getRects(sel, excl) {
-		const list = [], elems = $(sel);
-		for (let i = 0; i < elems.length; ++i) {
-			if (excl !== $(elems[i]).attr('aria-describedby')) {
-				list.push(_getRect(elems[i]));
-			}
-		}
-		return list;
-	}
-	function _getRect(e) {
-		const win = $(e), winoff = win.offset();
-		return {left: winoff.left
-			, top: winoff.top
-			, right: winoff.left + win.width()
-			, bottom: winoff.top + win.height()};
-	}
-	function _getPos(list, w, h) {
-		if (Room.getOptions().interview) {
-			return {left: 0, top: 0};
-		}
-		const wba = $(WBA_SEL), woffset = wba.offset()
-			, offsetX = 20, offsetY = 10
-			, area = {left: woffset.left, top: woffset.top, right: woffset.left + wba.width(), bottom: woffset.top + wba.height()};
-		const rectNew = {
-				_left: area.left
-				, _top: area.top
-				, right: area.left + w
-				, bottom: area.top + h
-				, get left() {
-					return this._left
-				}
-				, set left(l) {
-					this._left = l;
-					this.right = l + w;
-				}
-				, get top() {
-					return this._top
-				}
-				, set top(t) {
-					this._top = t;
-					this.bottom = t + h;
-				}
-			};
-		let minY = area.bottom, posFound;
-		do {
-			posFound = true
-			for (let i = 0; i < list.length; ++i) {
-				const rect = list[i];
-				minY = Math.min(minY, rect.bottom);
-
-				if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) {
-					rectNew.left = rect.right + offsetX;
-					posFound = false;
-				}
-				if (rectNew.right >= area.right) {
-					rectNew.left = area.left;
-					rectNew.top = minY + offsetY;
-					posFound = false;
-				}
-				if (rectNew.bottom >= area.bottom) {
-					rectNew.top = area.top;
-					posFound = true;
-					break;
-				}
-			}
-		} while (!posFound);
-		return {left: rectNew.left, top: rectNew.top};
-	}
-	function _arrange() {
-		const list = [], elems = $(VID_SEL);
-		for (let i = 0; i < elems.length; ++i) {
-			const v = $(elems[i]);
-			v.css(_getPos(list, v.width(), v.height()));
-			list.push(_getRect(v));
-		}
-	}
-
-	self.getVid = _getVid;
-	self.isSharing = _isSharing;
-	self.isRecording = _isRecording;
-	self.hasAudio = _hasAudio;
-	self.hasVideo = _hasVideo;
-	self.getRects = _getRects;
-	self.getPos = _getPos;
-	self.arrange = _arrange;
-	return self;
-})();
-var Video = (function() {
-	const self = {};
-	let c, v, vc, t, f, swf, size, vol, slider, handle
-		, lastVolume = 50;
-
-	function _getName() {
-		return c.user.firstName + ' ' + c.user.lastName;
-	}
-	function _resizeDlg(_ww, _hh) {
-		const interview = Room.getOptions().interview;
-		const _w = interview ? 320 : _ww, _h = interview ? 260 : _hh;
-		const h = _h + t.height() + 2 + (f.is(":visible") ? f.height() : 0);
-		v.dialog("option", "width", _w).dialog("option", "height", h);
-		_resize(_w, _h);
-		return h;
-	}
-	function _securityMode(on) {
-		if (Room.getOptions().interview) {
-			return;
-		}
-		if (on) {
-			v.dialog("option", "position", {my: "center", at: "center", of: WBA_SEL});
-		} else {
-			const h = _resizeDlg(size.width, size.height);
-			v.dialog("widget").css(VideoUtil.getPos(VideoUtil.getRects(VID_SEL, VideoUtil.getVid(c.uid)), c.width, h));
-		}
-	}
-	function _resize(w, h) {
-		vc.width(w).height(h);
-		swf.attr('width', w).attr('height', h);
-	}
-	function _handleMicStatus(state) {
-		if (!f.is(":visible")) {
-			return;
-		}
-		if (state) {
-			f.find('.off').hide();
-			f.find('.on').show();
-			f.addClass('ui-state-highlight');
-			t.addClass('ui-state-highlight');
-		} else {
-			f.find('.off').show();
-			f.find('.on').hide();
-			f.removeClass('ui-state-highlight');
-			t.removeClass('ui-state-highlight');
-		}
-	}
-	function _handleVolume(val) {
-		handle.text(val);
-		const ico = vol.find('.ui-icon');
-		if (val > 0 && ico.hasClass('ui-icon-volume-off')) {
-			ico.toggleClass('ui-icon-volume-off ui-icon-volume-on');
-			vol.removeClass('ui-state-error');
-			_handleMicStatus(true);
-		} else if (val === 0 && ico.hasClass('ui-icon-volume-on')) {
-			ico.toggleClass('ui-icon-volume-on ui-icon-volume-off');
-			vol.addClass('ui-state-error');
-			_handleMicStatus(false);
-		}
-		if (typeof(swf[0].setVolume) === 'function') {
-			swf[0].setVolume(val);
-		}
-	}
-	function _mute(mute) {
-		if (!slider) {
-			return;
-		}
-		if (mute) {
-			const val = slider.slider("option", "value");
-			if (val > 0) {
-				lastVolume = val;
-			}
-			slider.slider("option", "value", 0);
-			_handleVolume(0);
-		} else {
-			slider.slider("option", "value", lastVolume);
-			_handleVolume(lastVolume);
-		}
-	}
-	function _init(_c, _pos) {
-		c = _c;
-		size = {width: c.width, height: c.height};
-		const _id = VideoUtil.getVid(c.uid)
-			, name = _getName()
-			, _w = c.self ? Math.max(300, c.width) : c.width
-			, _h = c.self ? Math.max(200, c.height) : c.height
-			, opts = Room.getOptions();
-		{ //scope
-			const cont = opts.interview ? $('.pod.pod-' + c.pod) : $('.room.box');
-			cont.append(OmUtil.tmpl('#user-video', _id).attr('title', name)
-					.attr('data-client-uid', c.type + c.cuid).data(self));
-		}
-		v = $('#' + _id);
-		v.dialog({
-			classes: {
-				'ui-dialog': 'ui-corner-all video user-video' + (opts.showMicStatus ? ' mic-status' : '')
-				, 'ui-dialog-titlebar': 'ui-corner-all' + (opts.showMicStatus ? ' ui-state-highlight' : '')
-			}
-			, width: _w
-			, minWidth: 40
-			, minHeight: 50
-			, autoOpen: true
-			, appendTo: opts.interview ? '.pod.pod-' + c.pod : '.room.box'
-			, draggable: !opts.interview
-			, resizable: !opts.interview
-			, modal: false
-			, resizeStop: function(event, ui) {
-				const w = ui.size.width - 2
-					, h = ui.size.height - t.height() - 4 - (f.is(":visible") ? f.height() : 0);
-				_resize(w, h);
-				swf[0].vidResize(w, h);
-			}
-			, close: function() {
-				VideoManager.close(c.uid, true);
-			}
-		}).dialogExtend({
-			icons: {
-				'collapse': 'ui-icon-minus'
-			}
-			, closable: VideoUtil.isSharing(c)
-			, collapsable: true
-			, dblclick: "collapse"
-		});
-		t = v.parent().find('.ui-dialog-titlebar').attr('title', name);
-		f = v.find('.footer');
-		if (!VideoUtil.isSharing(c)) {
-			v.parent().find('.ui-dialog-titlebar-buttonpane')
-				.append($('#video-volume-btn').children().clone())
-				.append($('#video-refresh-btn').children().clone());
-			const volume = v.parent().find('.dropdown-menu.video.volume');
-			slider = v.parent().find('.slider');
-			if (opts.interview) {
-				v.parent().find('.ui-dialog-titlebar-collapse').hide();
-			}
-			vol = v.parent().find('.ui-dialog-titlebar-volume')
-				.on('mouseenter', function(e) {
-					e.stopImmediatePropagation();
-					volume.toggle();
-				})
-				.click(function(e) {
-					e.stopImmediatePropagation();
-					const muted = $(this).find('.ui-icon').hasClass('ui-icon-volume-off');
-					roomAction('mute', JSON.stringify({uid: c.cuid, mute: !muted}));
-					_mute(!muted);
-					volume.hide();
-					return false;
-				}).dblclick(function(e) {
-					e.stopImmediatePropagation();
-					return false;
-				});
-			v.parent().find('.ui-dialog-titlebar-refresh')
-				.click(function(e) {
-					e.stopImmediatePropagation();
-					_refresh();
-					return false;
-				}).dblclick(function(e) {
-					e.stopImmediatePropagation();
-					return false;
-				});
-			volume.on('mouseleave', function() {
-				$(this).hide();
-			});
-			handle = v.parent().find('.slider .handle');
-			slider.slider({
-				orientation: 'vertical'
-				, range: 'min'
-				, min: 0
-				, max: 100
-				, value: lastVolume
-				, create: function() {
-					handle.text($(this).slider("value"));
-				}
-				, slide: function(event, ui) {
-					_handleVolume(ui.value);
-				}
-			});
-			const hasAudio = VideoUtil.hasAudio(c);
-			_handleMicStatus(hasAudio);
-			if (!hasAudio) {
-				vol.hide();
-			}
-		}
-		vc = v.find('.video');
-		vc.width(_w).height(_h);
-		//broadcast
-		const o = Room.getOptions();
-		if (c.self) {
-			o.cam = c.cam;
-			o.mic = c.mic;
-			o.mode = 'broadcast';
-		} else {
-			o.mode = 'play';
-		}
-		o.av = c.activities.join();
-		o.rights = o.rights.join();
-		o.width = c.width;
-		o.height = c.height;
-		o.sid = c.sid;
-		o.uid = c.uid;
-		o.cuid = c.cuid;
-		o.userId = c.user.id;
-		o.broadcastId = c.broadcastId;
-		o.type = c.type;
-		delete o.keycode;
-		swf = initSwf(vc, 'main.swf', _id + '-swf', o);
-		swf.attr('width', _w).attr('height', _h);
-		v.dialog("widget").css(_pos);
-	}
-	function _update(_c) {
-		const opts = Room.getOptions();
-		c.screenActivities = _c.screenActivities;
-		c.activities = _c.activities;
-		c.user.firstName = _c.user.firstName;
-		c.user.lastName = _c.user.lastName;
-		const hasAudio = VideoUtil.hasAudio(c);
-		_handleMicStatus(hasAudio);
-		if (hasAudio) {
-			vol.show();
-		} else {
-			vol.hide();
-			v.parent().find('.dropdown-menu.video.volume').hide();
-		}
-		if (opts.interview && c.pod !== _c.pod) {
-			c.pod = _c.pod;
-			v.dialog('option', 'appendTo', '.pod.pod-' + c.pod);
-		}
-		const name = _getName();
-		v.dialog('option', 'title', name).parent().find('.ui-dialog-titlebar').attr('title', name);
-		if (typeof(swf[0].update) === 'function') {
-			c.self ? swf[0].update() : swf[0].update(c);
-		}
-	}
-	function _refresh(_opts) {
-		if (typeof(swf[0].refresh) === 'function') {
-			const opts = _opts || {};
-			if (!Room.getOptions().interview && !isNaN(opts.width)) {
-				_resizeDlg(opts.width, opts.height);
-			}
-			try {
-				swf[0].refresh(opts);
-			} catch (e) {
-				//swf might throw
-			}
-		}
-	}
-	function _setRights(_r) {
-		if (typeof(swf[0].setRights) === 'function') {
-			swf[0].setRights(_r);
-		}
-	}
-	function _cleanup() {
-		if (typeof(swf[0].cleanup) === 'function') {
-			swf[0].cleanup();
-		}
-	}
-
-	self.update = _update;
-	self.refresh = _refresh;
-	self.mute = _mute;
-	self.isMuted = function() { return 0 === slider.slider("option", "value"); };
-	self.init = _init;
-	self.securityMode = _securityMode;
-	self.client = function() { return c; };
-	self.setRights = _setRights;
-	self.cleanup = _cleanup;
-	return self;
-});
-var VideoManager = (function() {
-	const self = {};
-	let share, inited = false;
-
-	function _init() {
-		if ($(WB_AREA_SEL + ' .wb-area .tabs').length > 0) {
-			WBA_SEL = WBA_WB_SEL;
-		}
-		VideoSettings.init(Room.getOptions());
-		share = $('.room.box').find('.icon.shared.ui-button');
-		inited = true;
-	}
-	function _update(c) {
-		if (!inited) {
-			return;
-		}
-		for (let i = 0; i < c.streams.length; ++i) {
-			const cl = JSON.parse(JSON.stringify(c)), s = c.streams[i];
-			delete cl.streams;
-			$.extend(cl, s);
-			if (cl.self && VideoUtil.isSharing(cl) || VideoUtil.isRecording(cl)) {
-				continue;
-			}
-			const _id = VideoUtil.getVid(cl.uid)
-				, av = VideoUtil.hasAudio(cl) || VideoUtil.hasVideo(cl)
-				, v = $('#' + _id);
-			if (av && v.length !== 1 && !!cl.self) {
-				Video().init(cl, VideoUtil.getPos(VideoUtil.getRects(VID_SEL), cl.width, cl.height + 25));
-			} else if (av && v.length === 1) {
-				v.data().update(cl);
-			} else if (!av && v.length === 1) {
-				_closeV(v);
-			}
-		}
-		if (c.uid === Room.getOptions().uid) {
-			Room.setRights(c.rights);
-			const windows = $(VID_SEL + ' .ui-dialog-content');
-			for (let i = 0; i < windows.length; ++i) {
-				const w = $(windows[i]);
-				w.data().setRights(c.rights);
-			}
-
-		}
-		if (c.streams.length === 0) {
-			// check for non inited video window
-			const v = $('#' + VideoUtil.getVid(c.uid));
-			if (v.length === 1) {
-				_closeV(v);
-			}
-		}
-	}
-	function _closeV(v) {
-		if (v.dialog('instance') !== undefined) {
-			v.dialog('destroy');
-		}
-		v.remove();
-	}
-	function _play(c) {
-		if (!inited) {
-			return;
-		}
-		if (VideoUtil.isSharing(c)) {
-			_highlight(share
-					.attr('title', share.data('user') + ' ' + c.user.firstName + ' ' + c.user.lastName + ' ' + share.data('text'))
-					.data('uid', c.uid)
-					.show(), 10);
-			share.tooltip().off('click').click(function() {
-				const v = $('#' + VideoUtil.getVid(c.uid))
-				if (v.length !== 1) {
-					Video().init(c, $(WBA_SEL).offset());
-				} else {
-					v.dialog('open');
-				}
-			});
-		} else if ('sharing' !== c.type) {
-			Video().init(c, VideoUtil.getPos(VideoUtil.getRects(VID_SEL), c.width, c.height + 25));
-		}
-	}
-	function _close(uid, showShareBtn) {
-		const _id = VideoUtil.getVid(uid), v = $('#' + _id);
-		if (v.length === 1) {
-			_closeV(v);
-		}
-		if (!showShareBtn && uid === share.data('uid')) {
-			share.off('click').hide();
-		}
-	}
-	function _highlight(el, count) {
-		if (count < 0) {
-			return;
-		}
-		el.addClass('ui-state-highlight', 2000, function() {
-			el.removeClass('ui-state-highlight', 2000, function() {
-				_highlight(el, --count);
-			});
-		});
-	}
-	function _find(uid) {
-		return $(VID_SEL + ' div[data-client-uid="room' + uid + '"]');
-	}
-	function _micActivity(uid, active) {
-		const u = $('#user' + uid + ' .audio-activity.ui-icon')
-			, v = _find(uid).parent();
-		if (active) {
-			u.addClass("speaking");
-			v.addClass('user-speaks')
-		} else {
-			u.removeClass("speaking");
-			v.removeClass('user-speaks')
-		}
-	}
-	function _refresh(uid, opts) {
-		const v = _find(uid);
-		if (v.length > 0) {
-			v.data().refresh(opts);
-		}
-	}
-	function _mute(uid, mute) {
-		const v = _find(uid);
-		if (v.length > 0) {
-			v.data().mute(mute);
-		}
-	}
-	function _clickExclusive(uid) {
-		const s = VideoSettings.load();
-		if (false !== s.video.confirmExclusive) {
-			const dlg = $('#exclusive-confirm');
-			dlg.dialog({
-				buttons: [
-					{
-						text: dlg.data('btn-ok')
-						, click: function() {
-							s.video.confirmExclusive = !$('#exclusive-confirm-dont-show').prop('checked');
-							VideoSettings.save();
-							roomAction('exclusive', uid);
-							$(this).dialog('close');
-						}
-					}
-					, {
-						text: dlg.data('btn-cancel')
-						, click: function() {
-							$(this).dialog('close');
-						}
-					}
-				]
-			})
-		}
-	}
-	function _exclusive(uid) {
-		const windows = $(VID_SEL + ' .ui-dialog-content');
-		for (let i = 0; i < windows.length; ++i) {
-			const w = $(windows[i]);
-			w.data().mute('room' + uid !== w.data('client-uid'));
-		}
-	}
-
-	self.init = _init;
-	self.update = _update;
-	self.play = _play;
-	self.close = _close;
-	self.securityMode = function(uid, on) { $('#' + VideoUtil.getVid(uid)).data().securityMode(on); };
-	self.micActivity = _micActivity;
-	self.refresh = _refresh;
-	self.mute = _mute;
-	self.clickExclusive = _clickExclusive;
-	self.exclusive = _exclusive;
-	return self;
-})();
 var Room = (function() {
 	const self = {}, sbSide = Settings.isRtl ? 'right' : 'left';
 	let options, menuHeight, chat, sb, dock, activities;
@@ -748,15 +203,64 @@ var Room = (function() {
 			]
 		});
 	}
+	function _setQuickPollRights() {
+		const close = $('#quick-vote .close');
+		if (close.length === 1) {
+			close.off();
+			if (options.rights.includes('superModerator') || options.rights.includes('moderator') || options.rights.includes('presenter')) {
+				close.show().click(function() {
+					quickPollAction('close');
+				});
+			} else {
+				close.hide();
+			}
+		}
+	}
+	function _quickPoll(obj) {
+		if (obj.started) {
+			let qv = $('#quick-vote');
+			if (qv.length === 0) {
+				const wbArea = $('.room.wb.area');
+				qv = OmUtil.tmpl('#quick-vote-template', 'quick-vote');
+				wbArea.append(qv);
+			}
+			const pro = qv.find('.control.pro')
+				con = qv.find('.control.con');
+			if (obj.voted) {
+				pro.removeClass('clickable').off();
+				con.removeClass('clickable').off();
+			} else {
+				pro.addClass('clickable').off().click(function() {
+					quickPollAction('vote', true);
+				});
+				con.addClass('clickable').off().click(function() {
+					quickPollAction('vote', false);
+				});
+			}
+			pro.find('.badge').text(obj.pros);
+			con.find('.badge').text(obj.cons);
+			_setQuickPollRights();
+		} else {
+			const qv = $('#quick-vote');
+			if (qv.length === 1) {
+				qv.remove();
+			}
+		}
+		OmUtil.tmpl('#quick-vote-template', 'quick-vote');
+	}
 
 	self.init = _init;
 	self.getMenuHeight = function() { return menuHeight; };
 	self.getOptions = function() { return typeof(options) === 'object' ? JSON.parse(JSON.stringify(options)) : {}; };
-	self.setRights = function(_r) { return options.rights = _r; };
+	self.setRights = function(_r) {
+		options.rights = _r;
+		_setQuickPollRights();
+	};
 	self.setSize = _setSize;
 	self.load = _load;
 	self.unload = _unload;
 	self.showClipboard = _showClipboard;
+	self.quickPoll = _quickPoll;
 	return self;
 })();
 function startPrivateChat(el) {
@@ -773,7 +277,7 @@ function sipBtnEraseClick() {
 	const txt = $('.sip-number')
 		, t = txt.val();
 	if (!!t) {
-		txt.val(t.substring(0, t.length -1));
+		txt.val(t.substring(0, t.length - 1));
 	}
 }
 function sipGetKey(evt) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/sidebar/RoomSidebar.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/sidebar/RoomSidebar.java
index 923d97a..c16b630 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/sidebar/RoomSidebar.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/sidebar/RoomSidebar.java
@@ -148,7 +148,7 @@ public class RoomSidebar extends Panel {
 					default:
 				}
 			} catch (Exception e) {
-				log.error("Unexpected exception while toggle 'action'", e);
+				log.error("Unexpected exception while toggle 'roomAction'", e);
 			}
 		}
 	};
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-manager.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-manager.js
new file mode 100644
index 0000000..66c7e61
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-manager.js
@@ -0,0 +1,169 @@
+/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
+var VideoManager = (function() {
+	const self = {};
+	let share, inited = false;
+
+	function _init() {
+		if ($(WB_AREA_SEL + ' .wb-area .tabs').length > 0) {
+			WBA_SEL = WBA_WB_SEL;
+		}
+		VideoSettings.init(Room.getOptions());
+		share = $('.room.box').find('.icon.shared.ui-button');
+		inited = true;
+	}
+	function _update(c) {
+		if (!inited) {
+			return;
+		}
+		for (let i = 0; i < c.streams.length; ++i) {
+			const cl = JSON.parse(JSON.stringify(c)), s = c.streams[i];
+			delete cl.streams;
+			$.extend(cl, s);
+			if (cl.self && VideoUtil.isSharing(cl) || VideoUtil.isRecording(cl)) {
+				continue;
+			}
+			const _id = VideoUtil.getVid(cl.uid)
+				, av = VideoUtil.hasAudio(cl) || VideoUtil.hasVideo(cl)
+				, v = $('#' + _id);
+			if (av && v.length !== 1 && !!cl.self) {
+				Video().init(cl, VideoUtil.getPos(VideoUtil.getRects(VID_SEL), cl.width, cl.height + 25));
+			} else if (av && v.length === 1) {
+				v.data().update(cl);
+			} else if (!av && v.length === 1) {
+				_closeV(v);
+			}
+		}
+		if (c.uid === Room.getOptions().uid) {
+			Room.setRights(c.rights);
+			const windows = $(VID_SEL + ' .ui-dialog-content');
+			for (let i = 0; i < windows.length; ++i) {
+				const w = $(windows[i]);
+				w.data().setRights(c.rights);
+			}
+
+		}
+		if (c.streams.length === 0) {
+			// check for non inited video window
+			const v = $('#' + VideoUtil.getVid(c.uid));
+			if (v.length === 1) {
+				_closeV(v);
+			}
+		}
+	}
+	function _closeV(v) {
+		if (v.dialog('instance') !== undefined) {
+			v.dialog('destroy');
+		}
+		v.remove();
+	}
+	function _play(c) {
+		if (!inited) {
+			return;
+		}
+		if (VideoUtil.isSharing(c)) {
+			_highlight(share
+					.attr('title', share.data('user') + ' ' + c.user.firstName + ' ' + c.user.lastName + ' ' + share.data('text'))
+					.data('uid', c.uid)
+					.show(), 10);
+			share.tooltip().off('click').click(function() {
+				const v = $('#' + VideoUtil.getVid(c.uid))
+				if (v.length !== 1) {
+					Video().init(c, $(WBA_SEL).offset());
+				} else {
+					v.dialog('open');
+				}
+			});
+		} else if ('sharing' !== c.type) {
+			Video().init(c, VideoUtil.getPos(VideoUtil.getRects(VID_SEL), c.width, c.height + 25));
+		}
+	}
+	function _close(uid, showShareBtn) {
+		const _id = VideoUtil.getVid(uid), v = $('#' + _id);
+		if (v.length === 1) {
+			_closeV(v);
+		}
+		if (!showShareBtn && uid === share.data('uid')) {
+			share.off('click').hide();
+		}
+	}
+	function _highlight(el, count) {
+		if (count < 0) {
+			return;
+		}
+		el.addClass('ui-state-highlight', 2000, function() {
+			el.removeClass('ui-state-highlight', 2000, function() {
+				_highlight(el, --count);
+			});
+		});
+	}
+	function _find(uid) {
+		return $(VID_SEL + ' div[data-client-uid="room' + uid + '"]');
+	}
+	function _micActivity(uid, active) {
+		const u = $('#user' + uid + ' .audio-activity.ui-icon')
+			, v = _find(uid).parent();
+		if (active) {
+			u.addClass("speaking");
+			v.addClass('user-speaks')
+		} else {
+			u.removeClass("speaking");
+			v.removeClass('user-speaks')
+		}
+	}
+	function _refresh(uid, opts) {
+		const v = _find(uid);
+		if (v.length > 0) {
+			v.data().refresh(opts);
+		}
+	}
+	function _mute(uid, mute) {
+		const v = _find(uid);
+		if (v.length > 0) {
+			v.data().mute(mute);
+		}
+	}
+	function _clickExclusive(uid) {
+		const s = VideoSettings.load();
+		if (false !== s.video.confirmExclusive) {
+			const dlg = $('#exclusive-confirm');
+			dlg.dialog({
+				buttons: [
+					{
+						text: dlg.data('btn-ok')
+						, click: function() {
+							s.video.confirmExclusive = !$('#exclusive-confirm-dont-show').prop('checked');
+							VideoSettings.save();
+							roomAction('exclusive', uid);
+							$(this).dialog('close');
+						}
+					}
+					, {
+						text: dlg.data('btn-cancel')
+						, click: function() {
+							$(this).dialog('close');
+						}
+					}
+				]
+			})
+		}
+	}
+	function _exclusive(uid) {
+		const windows = $(VID_SEL + ' .ui-dialog-content');
+		for (let i = 0; i < windows.length; ++i) {
+			const w = $(windows[i]);
+			w.data().mute('room' + uid !== w.data('client-uid'));
+		}
+	}
+
+	self.init = _init;
+	self.update = _update;
+	self.play = _play;
+	self.close = _close;
+	self.securityMode = function(uid, on) { $('#' + VideoUtil.getVid(uid)).data().securityMode(on); };
+	self.micActivity = _micActivity;
+	self.refresh = _refresh;
+	self.mute = _mute;
+	self.clickExclusive = _clickExclusive;
+	self.exclusive = _exclusive;
+	return self;
+})();
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-util.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-util.js
new file mode 100644
index 0000000..5f831ff
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video-util.js
@@ -0,0 +1,111 @@
+/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
+const WB_AREA_SEL = '.room.wb.area';
+const WBA_WB_SEL = '.room.wb.area .ui-tabs-panel.ui-corner-bottom.ui-widget-content:visible';
+var WBA_SEL = WB_AREA_SEL;
+const VID_SEL = '.video.user-video';
+var VideoUtil = (function() {
+	const self = {};
+	function _getVid(uid) {
+		return "video" + uid;
+	}
+	function _isSharing(c) {
+		return 'sharing' === c.type && c.screenActivities.indexOf('sharing') > -1;
+	}
+	function _isRecording(c) {
+		return 'sharing' === c.type
+			&& c.screenActivities.indexOf('recording') > -1
+			&& c.screenActivities.indexOf('sharing') < 0;
+	}
+	function _hasAudio(c) {
+		return c.activities.indexOf('broadcastA') > -1;
+	}
+	function _hasVideo(c) {
+		return c.activities.indexOf('broadcastV') > -1;
+	}
+	function _getRects(sel, excl) {
+		const list = [], elems = $(sel);
+		for (let i = 0; i < elems.length; ++i) {
+			if (excl !== $(elems[i]).attr('aria-describedby')) {
+				list.push(_getRect(elems[i]));
+			}
+		}
+		return list;
+	}
+	function _getRect(e) {
+		const win = $(e), winoff = win.offset();
+		return {left: winoff.left
+			, top: winoff.top
+			, right: winoff.left + win.width()
+			, bottom: winoff.top + win.height()};
+	}
+	function _getPos(list, w, h) {
+		if (Room.getOptions().interview) {
+			return {left: 0, top: 0};
+		}
+		const wba = $(WBA_SEL), woffset = wba.offset()
+			, offsetX = 20, offsetY = 10
+			, area = {left: woffset.left, top: woffset.top, right: woffset.left + wba.width(), bottom: woffset.top + wba.height()};
+		const rectNew = {
+				_left: area.left
+				, _top: area.top
+				, right: area.left + w
+				, bottom: area.top + h
+				, get left() {
+					return this._left
+				}
+				, set left(l) {
+					this._left = l;
+					this.right = l + w;
+				}
+				, get top() {
+					return this._top
+				}
+				, set top(t) {
+					this._top = t;
+					this.bottom = t + h;
+				}
+			};
+		let minY = area.bottom, posFound;
+		do {
+			posFound = true
+			for (let i = 0; i < list.length; ++i) {
+				const rect = list[i];
+				minY = Math.min(minY, rect.bottom);
+
+				if (rectNew.left < rect.right && rectNew.right > rect.left && rectNew.top < rect.bottom && rectNew.bottom > rect.top) {
+					rectNew.left = rect.right + offsetX;
+					posFound = false;
+				}
+				if (rectNew.right >= area.right) {
+					rectNew.left = area.left;
+					rectNew.top = minY + offsetY;
+					posFound = false;
+				}
+				if (rectNew.bottom >= area.bottom) {
+					rectNew.top = area.top;
+					posFound = true;
+					break;
+				}
+			}
+		} while (!posFound);
+		return {left: rectNew.left, top: rectNew.top};
+	}
+	function _arrange() {
+		const list = [], elems = $(VID_SEL);
+		for (let i = 0; i < elems.length; ++i) {
+			const v = $(elems[i]);
+			v.css(_getPos(list, v.width(), v.height()));
+			list.push(_getRect(v));
+		}
+	}
+
+	self.getVid = _getVid;
+	self.isSharing = _isSharing;
+	self.isRecording = _isRecording;
+	self.hasAudio = _hasAudio;
+	self.hasVideo = _hasVideo;
+	self.getRects = _getRects;
+	self.getPos = _getPos;
+	self.arrange = _arrange;
+	return self;
+})();
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video.js
new file mode 100644
index 0000000..4322f35
--- /dev/null
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/video.js
@@ -0,0 +1,268 @@
+/* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
+var Video = (function() {
+	const self = {};
+	let c, v, vc, t, f, swf, size, vol, slider, handle
+		, lastVolume = 50;
+
+	function _getName() {
+		return c.user.firstName + ' ' + c.user.lastName;
+	}
+	function _resizeDlg(_ww, _hh) {
+		const interview = Room.getOptions().interview;
+		const _w = interview ? 320 : _ww, _h = interview ? 260 : _hh;
+		const h = _h + t.height() + 2 + (f.is(":visible") ? f.height() : 0);
+		v.dialog("option", "width", _w).dialog("option", "height", h);
+		_resize(_w, _h);
+		return h;
+	}
+	function _securityMode(on) {
+		if (Room.getOptions().interview) {
+			return;
+		}
+		if (on) {
+			v.dialog("option", "position", {my: "center", at: "center", of: WBA_SEL});
+		} else {
+			const h = _resizeDlg(size.width, size.height);
+			v.dialog("widget").css(VideoUtil.getPos(VideoUtil.getRects(VID_SEL, VideoUtil.getVid(c.uid)), c.width, h));
+		}
+	}
+	function _resize(w, h) {
+		vc.width(w).height(h);
+		swf.attr('width', w).attr('height', h);
+	}
+	function _handleMicStatus(state) {
+		if (!f.is(":visible")) {
+			return;
+		}
+		if (state) {
+			f.find('.off').hide();
+			f.find('.on').show();
+			f.addClass('ui-state-highlight');
+			t.addClass('ui-state-highlight');
+		} else {
+			f.find('.off').show();
+			f.find('.on').hide();
+			f.removeClass('ui-state-highlight');
+			t.removeClass('ui-state-highlight');
+		}
+	}
+	function _handleVolume(val) {
+		handle.text(val);
+		const ico = vol.find('.ui-icon');
+		if (val > 0 && ico.hasClass('ui-icon-volume-off')) {
+			ico.toggleClass('ui-icon-volume-off ui-icon-volume-on');
+			vol.removeClass('ui-state-error');
+			_handleMicStatus(true);
+		} else if (val === 0 && ico.hasClass('ui-icon-volume-on')) {
+			ico.toggleClass('ui-icon-volume-on ui-icon-volume-off');
+			vol.addClass('ui-state-error');
+			_handleMicStatus(false);
+		}
+		if (typeof(swf[0].setVolume) === 'function') {
+			swf[0].setVolume(val);
+		}
+	}
+	function _mute(mute) {
+		if (!slider) {
+			return;
+		}
+		if (mute) {
+			const val = slider.slider("option", "value");
+			if (val > 0) {
+				lastVolume = val;
+			}
+			slider.slider("option", "value", 0);
+			_handleVolume(0);
+		} else {
+			slider.slider("option", "value", lastVolume);
+			_handleVolume(lastVolume);
+		}
+	}
+	function _init(_c, _pos) {
+		c = _c;
+		size = {width: c.width, height: c.height};
+		const _id = VideoUtil.getVid(c.uid)
+			, name = _getName()
+			, _w = c.self ? Math.max(300, c.width) : c.width
+			, _h = c.self ? Math.max(200, c.height) : c.height
+			, opts = Room.getOptions();
+		{ //scope
+			const cont = opts.interview ? $('.pod.pod-' + c.pod) : $('.room.box');
+			cont.append(OmUtil.tmpl('#user-video', _id).attr('title', name)
+					.attr('data-client-uid', c.type + c.cuid).data(self));
+		}
+		v = $('#' + _id);
+		v.dialog({
+			classes: {
+				'ui-dialog': 'ui-corner-all video user-video' + (opts.showMicStatus ? ' mic-status' : '')
+				, 'ui-dialog-titlebar': 'ui-corner-all' + (opts.showMicStatus ? ' ui-state-highlight' : '')
+			}
+			, width: _w
+			, minWidth: 40
+			, minHeight: 50
+			, autoOpen: true
+			, appendTo: opts.interview ? '.pod.pod-' + c.pod : '.room.box'
+			, draggable: !opts.interview
+			, resizable: !opts.interview
+			, modal: false
+			, resizeStop: function(event, ui) {
+				const w = ui.size.width - 2
+					, h = ui.size.height - t.height() - 4 - (f.is(":visible") ? f.height() : 0);
+				_resize(w, h);
+				swf[0].vidResize(w, h);
+			}
+			, close: function() {
+				VideoManager.close(c.uid, true);
+			}
+		}).dialogExtend({
+			icons: {
+				'collapse': 'ui-icon-minus'
+			}
+			, closable: VideoUtil.isSharing(c)
+			, collapsable: true
+			, dblclick: "collapse"
+		});
+		t = v.parent().find('.ui-dialog-titlebar').attr('title', name);
+		f = v.find('.footer');
+		if (!VideoUtil.isSharing(c)) {
+			v.parent().find('.ui-dialog-titlebar-buttonpane')
+				.append($('#video-volume-btn').children().clone())
+				.append($('#video-refresh-btn').children().clone());
+			const volume = v.parent().find('.dropdown-menu.video.volume');
+			slider = v.parent().find('.slider');
+			if (opts.interview) {
+				v.parent().find('.ui-dialog-titlebar-collapse').hide();
+			}
+			vol = v.parent().find('.ui-dialog-titlebar-volume')
+				.on('mouseenter', function(e) {
+					e.stopImmediatePropagation();
+					volume.toggle();
+				})
+				.click(function(e) {
+					e.stopImmediatePropagation();
+					const muted = $(this).find('.ui-icon').hasClass('ui-icon-volume-off');
+					roomAction('mute', JSON.stringify({uid: c.cuid, mute: !muted}));
+					_mute(!muted);
+					volume.hide();
+					return false;
+				}).dblclick(function(e) {
+					e.stopImmediatePropagation();
+					return false;
+				});
+			v.parent().find('.ui-dialog-titlebar-refresh')
+				.click(function(e) {
+					e.stopImmediatePropagation();
+					_refresh();
+					return false;
+				}).dblclick(function(e) {
+					e.stopImmediatePropagation();
+					return false;
+				});
+			volume.on('mouseleave', function() {
+				$(this).hide();
+			});
+			handle = v.parent().find('.slider .handle');
+			slider.slider({
+				orientation: 'vertical'
+				, range: 'min'
+				, min: 0
+				, max: 100
+				, value: lastVolume
+				, create: function() {
+					handle.text($(this).slider("value"));
+				}
+				, slide: function(event, ui) {
+					_handleVolume(ui.value);
+				}
+			});
+			const hasAudio = VideoUtil.hasAudio(c);
+			_handleMicStatus(hasAudio);
+			if (!hasAudio) {
+				vol.hide();
+			}
+		}
+		vc = v.find('.video');
+		vc.width(_w).height(_h);
+		//broadcast
+		const o = Room.getOptions();
+		if (c.self) {
+			o.cam = c.cam;
+			o.mic = c.mic;
+			o.mode = 'broadcast';
+		} else {
+			o.mode = 'play';
+		}
+		o.av = c.activities.join();
+		o.rights = o.rights.join();
+		o.width = c.width;
+		o.height = c.height;
+		o.sid = c.sid;
+		o.uid = c.uid;
+		o.cuid = c.cuid;
+		o.userId = c.user.id;
+		o.broadcastId = c.broadcastId;
+		o.type = c.type;
+		delete o.keycode;
+		swf = initSwf(vc, 'main.swf', _id + '-swf', o);
+		swf.attr('width', _w).attr('height', _h);
+		v.dialog("widget").css(_pos);
+	}
+	function _update(_c) {
+		const opts = Room.getOptions();
+		c.screenActivities = _c.screenActivities;
+		c.activities = _c.activities;
+		c.user.firstName = _c.user.firstName;
+		c.user.lastName = _c.user.lastName;
+		const hasAudio = VideoUtil.hasAudio(c);
+		_handleMicStatus(hasAudio);
+		if (hasAudio) {
+			vol.show();
+		} else {
+			vol.hide();
+			v.parent().find('.dropdown-menu.video.volume').hide();
+		}
+		if (opts.interview && c.pod !== _c.pod) {
+			c.pod = _c.pod;
+			v.dialog('option', 'appendTo', '.pod.pod-' + c.pod);
+		}
+		const name = _getName();
+		v.dialog('option', 'title', name).parent().find('.ui-dialog-titlebar').attr('title', name);
+		if (typeof(swf[0].update) === 'function') {
+			c.self ? swf[0].update() : swf[0].update(c);
+		}
+	}
+	function _refresh(_opts) {
+		if (typeof(swf[0].refresh) === 'function') {
+			const opts = _opts || {};
+			if (!Room.getOptions().interview && !isNaN(opts.width)) {
+				_resizeDlg(opts.width, opts.height);
+			}
+			try {
+				swf[0].refresh(opts);
+			} catch (e) {
+				//swf might throw
+			}
+		}
+	}
+	function _setRights(_r) {
+		if (typeof(swf[0].setRights) === 'function') {
+			swf[0].setRights(_r);
+		}
+	}
+	function _cleanup() {
+		if (typeof(swf[0].cleanup) === 'function') {
+			swf[0].cleanup();
+		}
+	}
+
+	self.update = _update;
+	self.refresh = _refresh;
+	self.mute = _mute;
+	self.isMuted = function() { return 0 === slider.slider("option", "value"); };
+	self.init = _init;
+	self.securityMode = _securityMode;
+	self.client = function() { return c; };
+	self.setRights = _setRights;
+	self.cleanup = _cleanup;
+	return self;
+});
diff --git a/openmeetings-web/src/main/webapp/css/wb.css b/openmeetings-web/src/main/webapp/css/wb.css
index 2f80575..fff1226 100644
--- a/openmeetings-web/src/main/webapp/css/wb.css
+++ b/openmeetings-web/src/main/webapp/css/wb.css
@@ -291,3 +291,29 @@
 .wb-area .wb-zoom button.up {
 	background-image: url(images/page_up.png);
 }
+#quick-vote {
+	position: absolute;
+	right: 40px;
+	bottom: 40px;
+	padding: 5px;
+	background-color: aquamarine;
+}
+#quick-vote .control {
+	display: inline-block;
+	width: 40px;
+	height: 40px;
+	background-repeat: no-repeat;
+	background-size: 35px;
+	position: relative;
+}
+#quick-vote .control.pro {
+	background-image: url(images/add.png);
+}
+#quick-vote .control.con {
+	background-image: url(images/cancel.png);
+}
+#quick-vote .control .badge {
+	position: absolute;
+	right: 0;
+	bottom: 0;
+}

-- 
To stop receiving notification emails like this one, please contact
solomax@apache.org.