Index: lams_build/lib/lams/lams.jar =================================================================== diff -u -rab2b6923490ce38dc647ea66d6768966fbc40cfd -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 Binary files differ Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_central/src/java/org/lamsfoundation/lams/web/PresenceServlet.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_central/src/java/org/lamsfoundation/lams/webservice/PresenceChatAction.java'. Fisheye: No comparison available. Pass `N' to diff? Index: lams_common/src/java/org/lamsfoundation/lams/commonContext.xml =================================================================== diff -u -r078ffcc5cce6214e179857ace04486fa04a7e3a2 -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_common/src/java/org/lamsfoundation/lams/commonContext.xml (.../commonContext.xml) (revision 078ffcc5cce6214e179857ace04486fa04a7e3a2) +++ lams_common/src/java/org/lamsfoundation/lams/commonContext.xml (.../commonContext.xml) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -226,11 +226,6 @@ - - - - - @@ -508,11 +503,6 @@ - - - - - Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/dao/IPresenceChatDAO.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/dao/hibernate/PresenceChatDAO.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/model/PresenceChatMessage.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/model/PresenceChatUser.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/service/IPresenceChatService.java'. Fisheye: No comparison available. Pass `N' to diff? Fisheye: Tag 9c603f82a3332d5e2e5b290bcc550f263c09a8f8 refers to a dead (removed) revision in file `lams_common/src/java/org/lamsfoundation/lams/presence/service/PresenceChatService.java'. Fisheye: No comparison available. Pass `N' to diff? Index: lams_learning/.classpath =================================================================== diff -u -r0fdf00ad8ffebc0cc6d79de96a216c08ce0d4cdf -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/.classpath (.../.classpath) (revision 0fdf00ad8ffebc0cc6d79de96a216c08ce0d4cdf) +++ lams_learning/.classpath (.../.classpath) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -18,6 +18,7 @@ + Index: lams_learning/build.xml =================================================================== diff -u -re59bc835a5ec91886980d67af70c0f05a0f7ae73 -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/build.xml (.../build.xml) (revision e59bc835a5ec91886980d67af70c0f05a0f7ae73) +++ lams_learning/build.xml (.../build.xml) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -3,5 +3,23 @@ + + + + + ${ant.project.name}: Copying additional Java classes to WAR + + \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml =================================================================== diff -u -ra6641bf9262a01d07740a517643f8fe187ec5b1f -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml (.../learningApplicationContext.xml) (revision a6641bf9262a01d07740a517643f8fe187ec5b1f) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/learningApplicationContext.xml (.../learningApplicationContext.xml) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -97,8 +97,33 @@ + + + + + + + + + + + + + + + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + PROPAGATION_REQUIRED + + + + + + + + Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/PresenceWebsocketServer.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,391 @@ +package org.lamsfoundation.lams.learning.presence; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.websocket.CloseReason; +import javax.websocket.CloseReason.CloseCodes; +import javax.websocket.OnClose; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.apache.tomcat.util.json.JSONArray; +import org.apache.tomcat.util.json.JSONException; +import org.apache.tomcat.util.json.JSONObject; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; +import org.lamsfoundation.lams.learning.presence.service.IPresenceChatService; +import org.lamsfoundation.lams.util.hibernate.HibernateSessionManager; +import org.lamsfoundation.lams.web.session.SessionManager; +import org.lamsfoundation.lams.web.util.AttributeNames; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +/** + * Receives, processes and sends Lesson Chat messages to Learners. + * + * @author Marcin Cieslak + */ +@ServerEndpoint("/presenceChatWebsocket") +public class PresenceWebsocketServer { + + /** + * Identifies a single connection. There can be more than one connection for the same user: multiple windows open or + * the same user in an another role. + */ + private static class Websocket { + private final Session session; + private final String nickName; + + private Websocket(Session session) { + this.session = session; + this.nickName = session.getRequestParameterMap().get("nickname").get(0); + } + } + + /** + * A singleton which updates Learners with messages and roster. + */ + private static class SendWorker extends Thread { + private boolean stopFlag = false; + // how ofter the thread runs + private static final long CHECK_INTERVAL = 2000; + // mapping lessonId -> timestamp when the check was last performed, so the thread does not run too often + private final Map lastSendTimes = new TreeMap(); + + @Override + public void run() { + while (!stopFlag) { + try { + // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually + // A new session needs to be created on each thread run as the session keeps stale Hibernate data (single transaction). + HibernateSessionManager.bindHibernateSessionToCurrentThread(true); + + // synchronize websockets as a new Learner entering chat could modify this collection + synchronized (PresenceWebsocketServer.websockets) { + Iterator>> entryIterator = PresenceWebsocketServer.websockets + .entrySet().iterator(); + // go through lessons and update registered learners with messages and roster + while (entryIterator.hasNext()) { + Entry> entry = entryIterator.next(); + Long lessonId = entry.getKey(); + Long lastSendTime = lastSendTimes.get(lessonId); + if ((lastSendTime == null) + || ((System.currentTimeMillis() - lastSendTime) >= SendWorker.CHECK_INTERVAL)) { + send(lessonId, null); + } + + // if all learners left the chat, remove the obsolete mapping + Set lessonWebsockets = entry.getValue(); + if (lessonWebsockets.isEmpty()) { + entryIterator.remove(); + PresenceWebsocketServer.rosters.remove(lessonId); + lastSendTimes.remove(lessonId); + } + } + } + + Thread.sleep(SendWorker.CHECK_INTERVAL); + } catch (InterruptedException e) { + PresenceWebsocketServer.log.warn("Stopping Presence Chat worker thread"); + stopFlag = true; + } catch (Exception e) { + // error caught, but carry on + PresenceWebsocketServer.log.error("Error in Presence Chat worker thread", e); + } + } + } + + /** + * Cheks for stale connections and feeds the opened ones with messages and roster. + */ + private void send(Long lessonId, String nickName) { + Long lastSendTime = lastSendTimes.get(lessonId); + // fetch messages a bit earlier than the last run, in case there was a lag somewhere + // JS code will filter out duplicates + lastSendTime = (lastSendTime == null) || (nickName != null) ? 0 : lastSendTime - 1000; + List messages = PresenceWebsocketServer.getPresenceChatService() + .getNewMessages(lessonId, new Date(lastSendTime)); + // update the timestamp, if this is a regular run + if (nickName == null) { + lastSendTimes.put(lessonId, System.currentTimeMillis()); + } + + Set lessonWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + Roster roster = PresenceWebsocketServer.rosters.get(lessonId); + JSONArray rosterJSON = roster.getRosterJSON(); + // synchronize websockets as a new Learner entering chat could modify this collection + synchronized (lessonWebsockets) { + Iterator websocketIterator = lessonWebsockets.iterator(); + while (websocketIterator.hasNext()) { + Websocket websocket = websocketIterator.next(); + // if this run is meant only for one learner, skip the others + if ((nickName != null) && !nickName.equals(websocket.nickName)) { + continue; + } + + // check whether the connection is not stale + if (!websocket.session.isOpen()) { + // remove the closed connection + websocketIterator.remove(); + + if (PresenceWebsocketServer.log.isDebugEnabled()) { + PresenceWebsocketServer.log.debug( + "User " + websocket.nickName + " left Presence Chat with lessonId: " + lessonId); + } + continue; + } + + // the connection is valid, carry on + JSONObject responseJSON = new JSONObject(); + + try { + // if it is just roster, skip messages + if (roster.imEnabled) { + JSONArray messagesJSON = PresenceWebsocketServer.filterMessages(messages, + websocket.nickName); + responseJSON.put("messages", messagesJSON); + } + responseJSON.put("roster", rosterJSON); + + // send the payload to the Learner's browser + websocket.session.getBasicRemote().sendText(responseJSON.toString()); + } catch (Exception e) { + PresenceWebsocketServer.log.error("Error while building message JSON", e); + } + } + } + } + } + + /** + * Keeps information of learners present in a lesson. Needs to work with DB so presence is visible in clustered + * environment. + */ + private static class Roster { + private final Long lessonId; + private final boolean imEnabled; + // timestamp when DB was last hit + private long lastDBCheckTime = 0; + + // Learners who are currently active + private final Set activeUsers = new TreeSet(); + + private Roster(Long lessonId, boolean imEnabled) { + this.lessonId = lessonId; + this.imEnabled = imEnabled; + } + + /** + * Checks which Learners + */ + private JSONArray getRosterJSON() { + Set localActiveUsers = new TreeSet(); + Set sessionWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + // find out who is active locally + for (Websocket websocket : sessionWebsockets) { + localActiveUsers.add(websocket.nickName); + } + + // is it time to sync with the DB yet? + long currentTime = System.currentTimeMillis(); + if ((currentTime - lastDBCheckTime) > IPresenceChatService.PRESENCE_IDLE_TIMEOUT) { + // store Learners active on this node + PresenceWebsocketServer.getPresenceChatService().updateUserPresence(lessonId, localActiveUsers); + + // read active Learners from all nodes + List storedActiveUsers = PresenceWebsocketServer.getPresenceChatService() + .getUsersActiveByLessonId(lessonId); + // refresh current collection + activeUsers.clear(); + for (PresenceChatUser activeUser : storedActiveUsers) { + activeUsers.add(activeUser.getNickname()); + } + + lastDBCheckTime = currentTime; + } else { + // add users active on this node; no duplicates - it is a set, not a list + activeUsers.addAll(localActiveUsers); + } + + return new JSONArray(activeUsers); + } + } + + private static Logger log = Logger.getLogger(PresenceWebsocketServer.class); + + private static IPresenceChatService presenceChatService; + + private static final SendWorker sendWorker = new SendWorker(); + private static final Map rosters = Collections.synchronizedMap(new TreeMap()); + private static final Map> websockets = Collections + .synchronizedMap(new TreeMap>()); + + static { + // run the singleton thread + PresenceWebsocketServer.sendWorker.start(); + } + + /** + * Registeres the Learner for processing by SendWorker. + */ + @OnOpen + public void registerUser(Session session) throws IOException { + Long lessonId = Long.valueOf(session.getRequestParameterMap().get(AttributeNames.PARAM_LESSON_ID).get(0)); + Set sessionWebsockets = PresenceWebsocketServer.websockets.get(lessonId); + if (sessionWebsockets == null) { + sessionWebsockets = Collections.synchronizedSet(new HashSet()); + PresenceWebsocketServer.websockets.put(lessonId, sessionWebsockets); + } + Websocket websocket = new Websocket(session); + sessionWebsockets.add(websocket); + + Roster roster = PresenceWebsocketServer.rosters.get(lessonId); + if (roster == null) { + boolean imEnabled = Boolean.valueOf(session.getRequestParameterMap().get("imEnabled").get(0)); + // build a new roster object + roster = new Roster(lessonId, imEnabled); + PresenceWebsocketServer.rosters.put(lessonId, roster); + } + + // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually + HibernateSessionManager.bindHibernateSessionToCurrentThread(false); + // update the chat window immediatelly + PresenceWebsocketServer.sendWorker.send(lessonId, websocket.nickName); + + if (PresenceWebsocketServer.log.isDebugEnabled()) { + PresenceWebsocketServer.log + .debug("User " + websocket.nickName + " entered Chat with toolSessionId: " + lessonId); + } + } + + /** + * If there was something wrong with the connection, put it into logs. + */ + @OnClose + public void unregisterUser(CloseReason reason) { + if (!(reason.getCloseCode().equals(CloseCodes.GOING_AWAY) + || reason.getCloseCode().equals(CloseCodes.NORMAL_CLOSURE))) { + PresenceWebsocketServer.log.warn("Abnormal Presence Chat websocket close. Code: " + reason.getCloseCode() + + ". Reason: " + reason.getReasonPhrase()); + } + } + + /** + * Receives a message sent by Learner via a websocket. + * + * @throws IOException + */ + @OnMessage + public void receiveRequest(String input, Session session) throws JSONException, IOException { + if (StringUtils.isBlank(input)) { + return; + } + JSONObject requestJSON = new JSONObject(input); + String requestType = requestJSON.getString("type"); + if (requestType.equals("message")) { + PresenceWebsocketServer.storeMessage(requestJSON, session); + } else if (requestType.equals("fetchConversation")) { + PresenceWebsocketServer.sendConversation(requestJSON, session); + } + } + + /** + * Stores a message sent by a Learner. + */ + private static void storeMessage(JSONObject requestJSON, Session session) throws JSONException { + String message = requestJSON.getString("message"); + if (StringUtils.isBlank(message)) { + return; + } + Long lessonId = requestJSON.getLong("lessonID"); + + // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually + HibernateSessionManager.bindHibernateSessionToCurrentThread(false); + + String from = session.getRequestParameterMap().get("nickname").get(0); + String to = requestJSON.getString("to"); + if (StringUtils.isBlank(to)) { + to = null; + } + + PresenceWebsocketServer.getPresenceChatService().createPresenceChatMessage(lessonId, from, to, new Date(), + message); + } + + /** + * Sends the whole message history between the current and the chosen learner. + */ + private static void sendConversation(JSONObject requestJSON, Session session) throws JSONException, IOException { + Long lessonId = requestJSON.getLong("lessonID"); + + // websocket communication bypasses standard HTTP filters, so Hibernate session needs to be initialised manually + HibernateSessionManager.bindHibernateSessionToCurrentThread(false); + + String to = requestJSON.getString("to"); + String from = session.getRequestParameterMap().get("nickname").get(0); + + List messages = PresenceWebsocketServer.getPresenceChatService() + .getMessagesByConversation(lessonId, from, to); + JSONArray messagesJSON = new JSONArray(); + + for (PresenceChatMessage message : messages) { + JSONObject messageJSON = PresenceWebsocketServer.buildMessageJSON(message); + messagesJSON.put(messageJSON); + } + + JSONObject responseJSON = new JSONObject(); + responseJSON.put("messages", messagesJSON); + // send the payload to the Learner's browser + session.getBasicRemote().sendText(responseJSON.toString()); + } + + /** + * Filteres messages meant for the given user (group or personal). + */ + private static JSONArray filterMessages(List messages, String nickName) throws JSONException { + JSONArray messagesJSON = new JSONArray(); + + for (PresenceChatMessage message : messages) { + String messageTo = message.getTo(); + if ((messageTo == null) || message.getTo().equals(nickName) || message.getFrom().equals(nickName)) { + JSONObject messageJSON = PresenceWebsocketServer.buildMessageJSON(message); + messagesJSON.put(messageJSON); + } + } + + return messagesJSON; + } + + private static JSONObject buildMessageJSON(PresenceChatMessage message) throws JSONException { + JSONObject messageJSON = new JSONObject(); + messageJSON.put("uid", message.getUid()); + messageJSON.put("from", message.getFrom()); + messageJSON.put("to", message.getTo()); + messageJSON.put("dateSent", message.getDateSent()); + messageJSON.put("message", message.getMessage()); + return messageJSON; + } + + private static IPresenceChatService getPresenceChatService() { + if (PresenceWebsocketServer.presenceChatService == null) { + WebApplicationContext ctx = WebApplicationContextUtils + .getWebApplicationContext(SessionManager.getServletContext()); + PresenceWebsocketServer.presenceChatService = (IPresenceChatService) ctx.getBean("presenceChatService"); + } + return PresenceWebsocketServer.presenceChatService; + } +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/IPresenceChatDAO.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,43 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.dao; + +import java.util.Date; +import java.util.List; + +import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; + +public interface IPresenceChatDAO { + + void insertOrUpdate(Object object); + + List getNewMessages(Long lessonId, Date lastCheck); + + List getMessagesByConversation(Long lessonId, String from, String to); + + List getUsersByLessonIdAndLastPresence(Long lessonId, Date oldestLastPresence); +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/hibernate/PresenceChatDAO.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/hibernate/PresenceChatDAO.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/dao/hibernate/PresenceChatDAO.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,77 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.dao.hibernate; + +import java.util.Date; +import java.util.List; + +import org.lamsfoundation.lams.dao.hibernate.LAMSBaseDAO; +import org.lamsfoundation.lams.learning.presence.dao.IPresenceChatDAO; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; +import org.springframework.stereotype.Repository; + +@Repository +public class PresenceChatDAO extends LAMSBaseDAO implements IPresenceChatDAO { + // main query for getting new messages + private static final String MESSAGE_NEW = "FROM " + PresenceChatMessage.class.getName() + + " msg WHERE msg.lessonId=:lessonId AND msg.dateSent > :lastCheck ORDER BY msg.dateSent ASC"; + + // main query for getting new messages + private static final String MESSAGE_BY_CONVERSATION = "FROM " + PresenceChatMessage.class.getName() + + " msg WHERE msg.lessonId=:lessonId AND ((msg.to = :to AND msg.from = :from) OR (msg.from = :to AND msg.to = :from)) " + + "ORDER BY msg.dateSent ASC"; + + private static final String USER_BY_LESSON_ID_AND_TIME = "from " + PresenceChatUser.class.getName() + " r" + + " where r.lessonId=? and r.lastPresence > ?"; + + @Override + public void insertOrUpdate(Object object) { + super.insertOrUpdate(object); + flush(); + } + + @Override + @SuppressWarnings("unchecked") + public List getNewMessages(Long lessonId, Date lastCheck) { + return (List) (doFindByNamedParam(PresenceChatDAO.MESSAGE_NEW, + new String[] { "lessonId", "lastCheck" }, new Object[] { lessonId, lastCheck })); + } + + @Override + @SuppressWarnings("unchecked") + public List getMessagesByConversation(Long lessonId, String from, String to) { + return (List) (doFindByNamedParam(PresenceChatDAO.MESSAGE_BY_CONVERSATION, + new String[] { "lessonId", "from", "to" }, new Object[] { lessonId, from, to })); + } + + @Override + @SuppressWarnings("unchecked") + public List getUsersByLessonIdAndLastPresence(Long lessonId, Date oldestLastPresence) { + return (List) this.doFind(PresenceChatDAO.USER_BY_LESSON_ID_AND_TIME, + new Object[] { lessonId, oldestLastPresence }); + } +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatMessage.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatMessage.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatMessage.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,126 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.model; + +import java.util.Date; + +/** + * @hibernate.class table="lams_presence_chat_msgs" + */ +public class PresenceChatMessage implements java.io.Serializable, Cloneable { + + private Long uid; + + private Long lessonId; + + private String from; + + private String to; + + private Date dateSent; + + private String message; + + public PresenceChatMessage() { + + } + + public PresenceChatMessage(Long lessonId, String from, String to, Date dateSent, String message) { + super(); + this.lessonId = lessonId; + this.from = from; + this.to = to; + this.dateSent = dateSent; + this.message = message; + } + + /** + * @hibernate.id generator-class="native" type="java.lang.Long" column="uid" + */ + public Long getUid() { + return uid; + } + + public void setUid(Long uid) { + this.uid = uid; + } + + /** + * @hibernate.property column="lesson_id" + */ + public Long getLessonId() { + return lessonId; + } + + public void setLessonId(Long lessonId) { + this.lessonId = lessonId; + } + + /** + * @hibernate.property column="from_user" + */ + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + + /** + * @hibernate.property column="to_user" + */ + public String getTo() { + return to; + } + + public void setTo(String to) { + this.to = to; + } + + /** + * @hibernate.property column="date_sent" + */ + public Date getDateSent() { + return dateSent; + } + + public void setDateSent(Date dateSent) { + this.dateSent = dateSent; + } + + /** + * @hibernate.property column="message" + */ + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + +} Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatUser.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatUser.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/model/PresenceChatUser.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,82 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.model; + +import java.util.Date; + +/** + * @hibernate.class table="lams_presence_user" + */ +public class PresenceChatUser implements java.io.Serializable, Cloneable { + + private String nickname; + + private Long lessonId; + + private Date lastPresence; + + public PresenceChatUser() { + } + + public PresenceChatUser(String nickname, Long lessonId, Date lastPresence) { + this.nickname = nickname; + this.lessonId = lessonId; + this.lastPresence = lastPresence; + } + + /** + * @hibernate.id generator-class="assigned" column="nickname" + */ + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + /** + * @hibernate.property column="lesson_id" + */ + public Long getLessonId() { + return lessonId; + } + + public void setLessonId(Long lessonId) { + this.lessonId = lessonId; + } + + /** + * @hibernate.property column="last_presence" + */ + public Date getLastPresence() { + return lastPresence; + } + + public void setLastPresence(Date lastPresence) { + this.lastPresence = lastPresence; + } +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/IPresenceChatService.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/IPresenceChatService.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/IPresenceChatService.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,47 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.service; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; + +public interface IPresenceChatService { + final long PRESENCE_IDLE_TIMEOUT = 15 * 1000; + + void createPresenceChatMessage(Long lessonId, String from, String to, Date dateSent, String message); + + List getNewMessages(Long lessonId, Date lastCheck); + + List getMessagesByConversation(Long lessonId, String from, String to); + + void updateUserPresence(Long lessonId, Set users); + + List getUsersActiveByLessonId(Long lessonId); +} \ No newline at end of file Index: lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/PresenceChatService.java =================================================================== diff -u --- lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/PresenceChatService.java (revision 0) +++ lams_learning/src/java/org/lamsfoundation/lams/learning/presence/service/PresenceChatService.java (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -0,0 +1,77 @@ +/**************************************************************** + * Copyright (C) 2005 LAMS Foundation (http://lamsfoundation.org) + * ============================================================= + * License Information: http://lamsfoundation.org/licensing/lams/2.0/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2.0 + * as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + * + * http://www.gnu.org/licenses/gpl.txt + * **************************************************************** + */ + +/* $Id$ */ + +package org.lamsfoundation.lams.learning.presence.service; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.lamsfoundation.lams.learning.presence.dao.IPresenceChatDAO; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatMessage; +import org.lamsfoundation.lams.learning.presence.model.PresenceChatUser; + +public class PresenceChatService implements IPresenceChatService { + + private IPresenceChatDAO presenceChatDAO; + + @Override + public void createPresenceChatMessage(Long lessonId, String from, String to, Date dateSent, String message) { + PresenceChatMessage presenceChatMessage = new PresenceChatMessage(lessonId, from, to, dateSent, message); + presenceChatDAO.insertOrUpdate(presenceChatMessage); + } + + /** + * Stores information when users with given UIDs were last seen in their Chat session. + */ + @Override + public void updateUserPresence(Long lessonId, Set users) { + Date now = new Date(); + for (String nickname : users) { + PresenceChatUser rosterEntry = new PresenceChatUser(nickname, lessonId, now); + presenceChatDAO.insertOrUpdate(rosterEntry); + } + } + + @Override + public List getUsersActiveByLessonId(Long lessonId) { + Date oldestLastPresence = new Date(System.currentTimeMillis() - IPresenceChatService.PRESENCE_IDLE_TIMEOUT); + return presenceChatDAO.getUsersByLessonIdAndLastPresence(lessonId, oldestLastPresence); + } + + @Override + public List getNewMessages(Long lessonId, Date lastCheck) { + return presenceChatDAO.getNewMessages(lessonId, lastCheck); + } + + @Override + public List getMessagesByConversation(Long lessonId, String from, String to) { + return presenceChatDAO.getMessagesByConversation(lessonId, from, to); + } + + public void setPresenceChatDAO(IPresenceChatDAO presenceChatDAO) { + this.presenceChatDAO = presenceChatDAO; + } +} \ No newline at end of file Index: lams_learning/web/WEB-INF/tags/Head.tag =================================================================== diff -u -rab2b6923490ce38dc647ea66d6768966fbc40cfd -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/web/WEB-INF/tags/Head.tag (.../Head.tag) (revision ab2b6923490ce38dc647ea66d6768966fbc40cfd) +++ lams_learning/web/WEB-INF/tags/Head.tag (.../Head.tag) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -32,7 +32,6 @@ <%@ tag body-content="scriptless"%> - Index: lams_learning/web/css/presence.css =================================================================== diff -u -rd1915c892bc5ca61b4c5065d7fc1a188fdc0b1f2 -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/web/css/presence.css (.../presence.css) (revision d1915c892bc5ca61b4c5065d7fc1a188fdc0b1f2) +++ lams_learning/web/css/presence.css (.../presence.css) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -1,15 +1,5 @@ -/* -************************************************* -LAMS 2.2 -Date modified: 29/04/2009 - -************************************************* -Presence stylesheet -********************************************** */ - -#presenceUserListings { - margin: 5px; - padding: 2px; +.startHidden { + display: none; } #presenceChat { @@ -20,15 +10,6 @@ z-index: 10; } -#presenceChatWarning { - position: absolute; - width: 400px; - right: 18px; - z-index: 10; - padding: 10px 10px 10px 40px; - margin: 0px; -} - #presenceChatTabs { position: absolute; bottom: 0px; @@ -37,70 +18,54 @@ height: 260px; } +#presenceChatTabs li a { + border-bottom: none; +} + +#presenceChatTabs li .ui-icon-close { + margin-top: 5px; + cursor: pointer; +} + #presenceChatRoster { position: absolute; right: 0pt; bottom: 0pt; width: 25%; height: 100%; + background-color: #FFFFFF; + border: #AAAAAA thin solid; } +#presenceUserCount { + height: 23px; + border-bottom: #AAAAAA thin solid; + text-align: center; + padding-top: 7px; + cursor: pointer; +} + #presenceUserListings{ + padding: 5px; height: 255px; overflow-x: hidden; overflow-y: scroll; } -#minMaxIcon{ - position:absolute; - top: 0px; - right:0px; -} - -#tabsHolder { - float: left; - width: 90%; - overflow:hidden; -} - -#leftSliderDiv { - float: left; - width: 5%; - text-align: left; -} - -#rightSliderDiv { - float: left; - width: 5%; - text-align: right; -} - -.smallImage { - width: 16px; - height: 16px; - border: none; -} - -.startHidden { - display: none; -} - .presenceListing { - width:"100%"; + cursor: pointer; } .presenceListing:hover { - width:"100%"; background-color: #CBDBFF; - cursor: pointer; } .presenceListingNewMessage { background-color: #88FF99; } -.presenceTabNewMessage { - text-decoration: underline; +.presenceTabNewMessage a { + text-decoration: underline !important; } .presenceMessage { @@ -111,26 +76,15 @@ font-weight: bold; } -.presenceMessageEmote { - font-style: italic; - padding-bottom: 4px; -} - -.chatPanel { - width: 95%; - height: 220px; -} - .messageArea { width: 100%; - height: 80%; + height: 190px; overflow-x: hidden; overflow-y: scroll; } .sendArea { - width: 100%; - height: 20%; + width: 95%; } .messageInput { @@ -140,20 +94,4 @@ .sendButton { float: left; -} - -table.tabLabelTable { - width: auto; -} - -.ui-tabs .ui-tabs-nav li.ui-tabs-active a{ - cursor: pointer; -} - -#presenceChatRoster li { - width: 100%; -} - -#presenceChatRoster li a{ - width: 83%; } \ No newline at end of file Index: lams_learning/web/includes/javascript/presence.js =================================================================== diff -u -rd1915c892bc5ca61b4c5065d7fc1a188fdc0b1f2 -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/web/includes/javascript/presence.js (.../presence.js) (revision d1915c892bc5ca61b4c5065d7fc1a188fdc0b1f2) +++ lams_learning/web/includes/javascript/presence.js (.../presence.js) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -1,111 +1,174 @@ -var windowHeight; -var pollInProgress = false; +$(document).ready(function (){ + presenceChat = $("#presenceChat"); + rosterDiv = $("#presenceUserListings"); -var roster = { - // association nick -> localId, the latter being just some ID made in this script - users : {}, - maxUserLocalId : 0, - // map "tab" -> "last message in tab", so we don't fetch all messages every time, only new ones - lastMessageUids : [], - - // when user clicked another user in roster - handleUserClicked : function(localId) { - var nick = null; - $.each(roster.users, function(key, value){ - if (value == localId) { - nick = key; - return false; + // if presence IM is enabled + if (presenceEnabled) { + // make visible + presenceChat.removeClass("startHidden"); + + // create chat tabs + presenceChatTabs = $("#presenceChatTabs").tabs({ + 'activate' : function(event, ui) { + // remove visual indicators (underline) of new message + var nick = getUserFromTabIndex(presenceChatTabs.tabs('option','active')), + tag = nickToTag(nick), + tabLabel = tagToTabLabel(tag); + $("#" + tabLabel).removeClass('presenceTabNewMessage'); } }); - - if(nick != nickname){ - var tag = nickToTag(nick); - var tabLabel = tagToTabLabel(tag); - var tab = $('#' + tabLabel); - // if the clicked user's tab is open - if(tab.length){ - // make the sender's tab label unbold - tab.html(nick); - } else { - // if the clicked user's tab is not already open, create it and select it - addTab(nick, tag); + } +}); + + +var windowHeight = null, + roster = { + // association nick -> localId, the latter being just some ID made in this script + users : {}, + maxUserLocalId : 0, + lastMessageUids : {}, + + // when user clicked another user in roster + handleUserClicked : function(localId) { + var nick = null; + // find the nick based on the ID + $.each(roster.users, function(key, value){ + if (value == localId) { + nick = key; + return false; + } + }); + + if (nick != nickname){ + var tag = nickToTag(nick), + tabLabel = tagToTabLabel(tag), + tab = $('#' + tabLabel); + // if the clicked learner's tab is open + if (tab.length == 0){ + // if the clicked user's tab is not already open, create it and select it + addTab(nick, tag); + } + // select the added tab + presenceChatTabs.tabs( "option", "active", $('li', presenceChatTabs).length - 1); } + }, + + updateDisplay : function(users) { + // repopulate user objects array + var rosterUsers = {}; + $.each(users, function(index, nick){ + var localId = roster.users[nick]; + rosterUsers[nick] = localId ? localId : ++roster.maxUserLocalId; + }); + this.users = rosterUsers; - // select the added tab - presenceChatTabs.tabs('select' , tag); + // add HTML entries for each user + rosterDiv.empty(); + jQuery.each(this.users, function(nick, localId){ + var tag = nickToTag(nick), + listingName = tagToListing(tag), + listing = $("#" + listingName); + // if no listing in roster exists + if (listing.length == 0){ + // create listing div + var listingDiv = $('
' + + createPresenceListing(nick, tag) + + '
'); + + // add the listing div + rosterDiv.append(listingDiv); + } else { + // remove and append at the right place (from sort) + rosterDiv.append(listing.remove()); + } + }); + + // update presenceTabLabel + $("#presenceUserCount").html(labelUsers + " (" + users.length + ")"); } }, + + // init the connection with server using server URL but with different protocol + websocket = new WebSocket(APP_URL.replace('http', 'ws') + 'presenceChatWebsocket?lessonID=' + lessonId + + '&imEnabled=' + presenceImEnabled + '&nickname=' + encodeURIComponent(nickname)); - updateDisplay : function(users) { - // sort alphabetically - users.sort(); - // repopulate user objects array - var rosterUsers = {}; - $.each(users, function(index, nick){ - var localId = roster.users[nick]; - rosterUsers[nick] = localId ? localId : ++roster.maxUserLocalId; - }); - this.users = rosterUsers; - - jQuery.each(this.users, function(nick, localId){ - var tag = nickToTag(nick); - var listingName = tagToListing(tag); - var listing = $("#" + listingName); - // if no listing in roster exists - if (listing.length == 0){ - // create listing div - var listingDiv = $('
' - + createPresenceListing(nick, tag) - + '
'); +// when the server pushes new messages and roster to the learner's browser +websocket.onmessage = function(e){ + // create JSON object + var input = JSON.parse(e.data); + if (input.roster) { + roster.updateDisplay(input.roster); + } + + if (input.messages) { + var activeNick = getUserFromTabIndex(presenceChatTabs.tabs('option','active')), + selectedTabTag = nickToTag(activeNick); + + jQuery.each(input.messages, function(){ + // which tab are we talking about? + var from = this.to ? (this.from == nickname ? this.to : this.from) : groupChatInfo.nick, + lastMessageUid = roster.lastMessageUids[from] || 0; + + // are the messages new? + if (this.uid > lastMessageUid) { + var tag = nickToTag(from); + if (tag != selectedTabTag) { + var tab = $("#" + tagToTabLabel(tag)); + if (tab.length == 0) { + // no tab opened yet, create it + tab = addTab(from, tag); + } - // add the listing div - rosterDiv.append(listingDiv); - } else { - // remove and append at the right place (from sort) - rosterDiv.append(listing.remove()); - } - }); - - // update presenceTabLabel - var presenceTabLabelDiv = $("#presence_tabLabel"); - presenceTabLabelDiv.html(labelUsers + " (" + users.length + ")"); - } -} + // notify of new message + tab.addClass('presenceTabNewMessage'); + if (tag != groupChatInfo.tag) { + $("#" + tagToListing(tag)).addClass('presenceListingNewMessage'); + } + } + + roster.lastMessageUids[from] = this.uid; + var messageArea = $("#" + (nickToMessageArea(from))); + messageArea.append(generateMessageHTML(this.from, this.message, this.dateSent)); + messageArea.scrollTop(messageArea.prop('scrollHeight')); + } + }); + } + + // remove conversation tabs with learners who are gone + $('li a', presenceChatTabs).each(function() { + var nick = $(this).text(); + if (nick != groupChatInfo.nick && !roster.users[nick]) { + var tag = $(this).attr('href'); + $(tag).remove(); + $(this).parent().remove(); + presenceChatTabs.tabs('refresh'); + } + }); +}; /* ******* HTML write Functions ******* */ function createPrivateTabLabel(nick, tag){ - return '' + - '' + - '' + - '' + - '' + - '' + - '
' + nick + '
'; + return '' + nick + 'Remove Tab'; } function createPrivateTabContent(nick, tag){ - return '

' + - '
' + - '' + - '' + - '
'; + return '
' + + '
' + + '' + + '' + + '
'; } function createPresenceListing(nick, tag){ - return '' + - '' + - '' + - '' + - '' + - '
' + nick + '
'; + return '
' + nick + '
'; } /* ******* Helper Functions ******* */ function generateMessageHTML(nick, message, date) { - var fromElem = $('
(' + date.substring(11, 19) + ') ' + nick + '
'); - var msgElem = $('
' + message + '
'); + var fromElem = $('
(' + date.substring(11, 19) + ') ' + nick + '
'), + msgElem = $('
' + message + '
'); return completeElem = $('
').append(fromElem).append(msgElem); } @@ -128,22 +191,29 @@ 'top' : windowHeight + "px" }); } - - $("#presenceChatWarning").css({ - 'top' : windowHeight - 10 + "px" - }); } function getUserFromTabIndex(tabIndex) { - return $(".ui-tabs-label")[tabIndex].innerHTML; + return tabIndex == 0 ? groupChatInfo.nick : $('li a', presenceChatTabs)[tabIndex].innerHTML; } +// Adds a new tab and fetches conversation history function addTab(nick, tag) { - // add a tab with the the nick specified - presenceChatTabs.tabs('add', '#' + tag, - createPrivateTabLabel(nick, tag)); - // add the content - $("#" + tag).html(createPrivateTabContent(nick, tag)); + var tab = $('
  • ').attr('id', tag + '_tabLabel').html(createPrivateTabLabel(nick, tag)).appendTo($('ul', presenceChatTabs)); + $('
    ').attr('id', tag).html(createPrivateTabContent(nick, tag)).appendTo(presenceChatTabs); + presenceChatTabs.tabs('refresh'); + + // fetch all messages from the start + roster.lastMessageUids[nick] = null; + var data = { + 'type' : 'fetchConversation', + 'lessonID' : lessonId, + 'to' : nick + }; + + websocket.send(JSON.stringify(data)); + + return tab; } function nickToTag(nick) { @@ -163,10 +233,10 @@ } function getTime() { - var currentTime = new Date(); - var hours = currentTime.getHours(); - var minutes = currentTime.getMinutes(); - var seconds = currentTime.getSeconds(); + var currentTime = new Date(), + hours = currentTime.getHours(), + minutes = currentTime.getMinutes(), + seconds = currentTime.getSeconds(); if (hours < 10) { hours = "0" + hours; @@ -186,123 +256,35 @@ /* ******* Main chat functions ******* */ function sendMessage(receiver) { - var tag = nickToTag(receiver); - var messageInput = $('#' + tag + '_messageInput'); - var message = messageInput.val(); + var tag = nickToTag(receiver), + messageInput = $('#' + tag + '_messageInput'), + message = messageInput.val(); if (!message || message == '') { return false; // do not send empty messages. } messageInput.val(''); messageInput.focus(); - $.ajax({ - url : actionUrl, - data : {'method' : 'sendMessage', - 'lessonID' : lessonId, - 'from' : nickname, - 'to' : tag == groupChatInfo.tag ? null : receiver, - 'message' : message - }, - cache : false, - complete : updateChat - }); + + var data = { + 'type' : 'message', + 'lessonID' : lessonId, + 'to' : tag == groupChatInfo.tag ? '' : receiver, + 'message' : message + }; + + websocket.send(JSON.stringify(data)); } -function updateChat(){ - // skip another attempt if previous did not return yet (slow server?) - if (!pollInProgress) { - pollInProgress = true; - var from = null; - var selected = null; - var lastMessageUid = null; - var getMessages = presenceShown && presenceImEnabled; - if (getMessages) { - selected = presenceChatTabs.tabs('option','active'); - from = getUserFromTabIndex(selected); - if (groupChatInfo.nick == from) { - from = null; - } - lastMessageUid = roster.lastMessageUids[from ? from : 'group']; - } - - $.ajax({ - url : actionUrl, - data : {'method' : 'getChatContent', - 'lessonID' : lessonId, - 'getMessages' : getMessages, - 'lastMessageUid' : lastMessageUid, - 'to' : nickname, - 'from' : from - }, - cache : false, - dataType : 'json', - complete : function(){ - pollInProgress = false; - }, - success : function (result) { - roster.updateDisplay(result.roster); - - // real new messages for the opentab - if (result.messages) { - var messageArea = $("#" + (nickToMessageArea(from ? from : groupChatInfo.nick))); - var lastMessageUid = null; - jQuery.each(result.messages, function(){ - messageArea.append(generateMessageHTML(this.from, this.message, this.dateSent)); - lastMessageUid = this.uid; - }); - // store last message uid and get new messages starting from this one - roster.lastMessageUids[from ? from : 'group'] = lastMessageUid; - messageArea.scrollTop(messageArea.prop('scrollHeight')); - } - - // check if other users wrote something new - if (result.newConversations) { - var selectedTabTag = nickToTag(getUserFromTabIndex(selected)); - jQuery.each(result.newConversations, function(index, nick){ - var tag = nick == 'group' ? groupChatInfo.tag : nickToTag(nick); - if (tag != selectedTabTag) { - var tab = $("#" + tagToTabLabel(tag)); - if (tab.length == 0) { - addTab(this, tag); - tab = $("#" + tagToTabLabel(tag)); - } - - // notify of new message - tab.addClass('presenceTabNewMessage'); - if (tag != groupChatInfo.tag) { - $("#" + tagToListing(tag)).addClass('presenceListingNewMessage'); - } - } - }); - } - } - }); - } -} - - /* ******* Click handlers ******* */ - -function handleCloseTab(label){ - var tabLabelsLocal = $(".ui-tabs-label"); - for (var i = 0; i < tabLabelsLocal.length; i++){ - if(tabLabelsLocal[i].innerHTML == label){ - presenceChatTabs.tabs('remove' , i); - roster.lastMessageUids[label] = null; - } - } +function handleCloseTab(tag){ + $('#' + tag + '_tabLabel').remove(); + $('#' + tag).remove(); + presenceChatTabs.tabs('refresh'); } -function handleLeftScrollClick(){ - presenceChatTabs.tabs('scrollLeft'); -} - -function handleRightScrollClick(){ - presenceChatTabs.tabs('scrollRight'); -} - function handlePresenceClick() { if (presenceShown) { presenceChat.animate({ Index: lams_learning/web/presenceChat.jsp =================================================================== diff -u -r2673317d66ead2aa551d93dcb5f5e9afab5f532e -r9c603f82a3332d5e2e5b290bcc550f263c09a8f8 --- lams_learning/web/presenceChat.jsp (.../presenceChat.jsp) (revision 2673317d66ead2aa551d93dcb5f5e9afab5f532e) +++ lams_learning/web/presenceChat.jsp (.../presenceChat.jsp) (revision 9c603f82a3332d5e2e5b290bcc550f263c09a8f8) @@ -1,138 +1,52 @@ <%-- css --%> - + <%-- javascript --%> - - + <%-- initial html / presence.js adds on html into here --%>
    <%-- only pop the message box if im is enabled --%> - -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - - " class="sendButton" - onclick="javascript:sendMessage(groupChatInfo.nick)" /> + +
    +
      +
    • + +
    • +
    +
    +
    +
    + + " class="sendButton" + onclick="javascript:sendMessage(groupChatInfo.nick)" />
    -
    -
    -
    - -
    +
    -
    <%-- always pop the roster --%> -
    - -
    -
    +
    +
    +
    -
    - -<%-- floating div shown when IE6 is being used --%> -
    -
    \ No newline at end of file