var Chat = function ($, PushJS) { "use strict"; var me = { settings: { // Interval nach dem neue Nachrichten abgerufen werden (in Millisekunden) fetchMessageInterval: 2000, // Interval nach dem die Userliste aktualisiert wird (in Millisekunden) fetchUserlistInterval: 15000, // Zeit die zwischen zwei LazyLoading-Requests vergehen muss (in Millisekunden) lazyLoadingBufferTime: 250, // Zeit für die eine neue Nachricht als "NEU" markiert ist (in Millisekunden) newTimeout: 5000 }, selector: { // Nachrichtenbereich messageArea: '#message-area', // Eingabefeld für neue Nachrichten inputField: '#nachricht', // Container in dem die Benutzerliste angezeigt wird userList: '#userlist', // Container in dem die Raumliste angezeigt wird roomList: '#roomlist', // Bezeichner für den aktuellen Raum roomName: '#message-header', // Formular-Element chatForm: '#chatform', // Checkbox um Nachricht hervorzuheben prioCheckbox: '#prio', // InfoBox für Browser-Benachrichtigungen notificationInfo: '#notification-info', // Xentral-Sidebar wawiSidebar: '#sidebar' }, storage: { // User-ID des aktuellen Chat-Partners activeUserId: 0, // Eigene User-ID selfUserId: 0, // ID der neuesten empfangenen Nachricht newestMessageId: 0, // ID der ältesten empfangenen Nachricht oldestMessageId: null, // ID der neuesten Gelesen-Markierung newestReadmarkId: 0, requestBuffer: 0, // Gibt an ob die älteste Nachricht bereits geladen wurde requestEOF: false, // Array mit Nachrichten-IDs die als gelesen markiert werden sollen readMark: [], // Neuestes Datum; für Anzeige der Datumszeile (wenn zu neueren Nachrichten gescrollt wird) newestDateString: '', // Ältestes Datum; für Anzeige der Datumszeile (wenn zu älteren Nachrichten gescrollt wird) oldestDateString: '', // Datumswerte merken, die schonmal angezeigt wurden dateStrings: [], isStandaloneWindow: null, fetchMessageInterval: null, fetchUserlistInterval: null }, elem: { $window: null, $wrapper: null, $sidebarWrapper: null, $sidebarScroller: null, $messageWrapper: null, $messageScroller: null }, init: function () { me.storage.isStandaloneWindow = ($(me.selector.wawiSidebar).length === 0); me.elem.$window = $(window); me.elem.$wrapper = $('#chat-wrapper'); me.elem.$messageWrapper = $('#message-wrapper'); me.elem.$messageScroller = $('#message-scroller'); me.elem.$sidebarWrapper = $('#sidebar-wrapper'); me.elem.$sidebarScroller = $('#sidebar-scroller'); me.checkNotificationPermission(); me.resizeElements(); me.attachEvents(); // Chat mit "Öffentlich" me.switchToUser(0); if (me.storage.isStandaloneWindow) { var navigation = ''; $('#tabs').prepend(navigation); } }, /** * Registriert alle benötigten Events und Timer */ attachEvents: function () { // Formular-Absenden abfangen $(me.selector.chatForm).on('submit', function (event) { event.preventDefault(); me.sendMessage($(me.selector.inputField).val(), $(me.selector.prioCheckbox).prop('checked')); }); // Klick auf User $(document).on('click', me.selector.userList + ' li', function (e) { e.preventDefault(); me.switchToUser(parseInt($(this).data('user-id'))); }); // Klick auf Raum $(document).on('click', me.selector.roomList + ' li', function (e) { e.preventDefault(); me.switchToUser(parseInt($(this).data('user-id'))); }); // Bei Klick auf InfoBox für Benachrichtigungen > Erlaubnis einholen $(document).on('click', me.selector.notificationInfo, function () { me.requestNotificationPermission(); }); // Fenstergrößen-Änderungen abfangen // Vorallem der Nachrichtenbereich benötigt immer eine feste Breite; ansonsten // können überlange Nachrichten ohne Leerzeichen nicht umgebrochen werden. $(window).on('resize', function () { me.debounce(me.resizeElements, 100); }); }, /** * Startet die Intervale zum Abrufen der Nachrichten und Benutzerliste */ start: function () { // Alle 30 Sekunden die Userliste aktualisieren me.storage.fetchUserlistInterval = setInterval(function () { me.updateUserList(); }, me.settings.fetchUserlistInterval); // Alle 2 Sekunden die Nachrichten aktualisieren me.storage.fetchMessageInterval = setInterval(function () { me.fetchNewMessages(false); me.setMessagesAsRead(); }, me.settings.fetchMessageInterval); }, /** * Stop die Intervale zum Abrufen der Nachrichten und Benutzerliste */ stop: function () { clearInterval(me.storage.fetchMessageInterval); clearInterval(me.storage.fetchUserlistInterval); }, /** * Ist Berechtigung zum Senden von Browser-Benachrichtigungen vorhanden? * * @return {boolean} */ checkNotificationPermission: function () { var permissionStatus = PushJS.Permission.get(); // Falls Berechtigung nicht erteilt oder blockiert wurde > InfoBox anzeigen if (permissionStatus === PushJS.Permission.DEFAULT) { var content = '
' + '' + '' + ''+ '
Aktuell können keine Browser-Benachrichtigungen zugestellt werden. ' + 'Bitte Klicken Sie hier um die Nachrichten für Xentral zu erlauben.
'; $('#tabs-1').prepend(content); } }, /** * Browser-Dialog zum Erteilen der Berechtigung öffnen */ requestNotificationPermission: function () { PushJS.Permission.request( me.removeNotificationInfoBox, // Berechtigung erteilt me.removeNotificationInfoBox // Berechtigung verweigert ); }, /** * InfoBox zu Browser-Benachrichtigungen entfernen */ removeNotificationInfoBox: function () { $(me.selector.notificationInfo).remove(); me.resizeElements(); }, /** * Schickt eine neue Nachricht per AJAX ab * * @param {string} message * @param {boolean} priority */ sendMessage: function (message, priority) { if (typeof priority === 'undefined' || typeof priority !== 'boolean') { priority = false; } $.ajax({ type: 'POST', url: 'index.php?module=chat&action=list&cmd=sendmessage', data: { nachricht: message, empfaenger: me.storage.activeUserId, prio: (priority === true) ? '1' : '0' }, success: function () { me.emptyInput(); me.focusInput(); me.fetchNewMessages(true); } }); }, /** * Aktualisiert per AJAX die Userliste */ updateUserList: function () { $.ajax({ type: 'POST', dataType: 'json', url: 'index.php?module=chat&action=list&cmd=userlist', success: function (data) { me.renderUserList(data.users); me.renderRoomList(data.rooms) } }); }, /** * Rendert die JSON-Userliste zu HTML * * @param {array} data */ renderUserList: function (data) { var content = ''; $(me.selector.userList).html(content); }, /** * Rendert die JSON-Raumliste zu HTML * * @param {array} data */ renderRoomList: function (data) { var content = ''; $(me.selector.roomList).html(content); }, /** * Ruft neue Nachrichten per AJAX ab * * @param {boolean} scrollDown Soll nach dem Aktualisieren der Nachrichten zur neuesten Nachricht gescrollt werden? */ fetchNewMessages: function (scrollDown) { if (typeof scrollDown === 'undefined' || typeof scrollDown !== 'boolean') { scrollDown = false; } $.ajax({ type: 'POST', url: 'index.php?module=chat&action=list&cmd=messages', data: { user_id: me.storage.activeUserId, after_message_id: me.storage.newestMessageId, after_readmark_id: me.storage.newestReadmarkId }, success: function (data) { me.renderNewMessages(data.messages); me.renderMessagesAsRead(data.readmark); if (scrollDown === true) { me.scrollToLatestMessage(); } } }); }, /** * Nachrichten als "Gelesen" markieren * * renderNewMessages() füllt das readMark-Array */ setMessagesAsRead: function () { if (me.storage.readMark.length === 0) { return; } $.ajax({ type: 'POST', url: 'index.php?module=chat&action=list&cmd=markread', data: { messages: me.storage.readMark }, success: function (data) { // PHP liefert die Nachrichten-IDs der Gelesen-markierten Nachrichten zurück $(data).each(function (index, messageId) { messageId = parseInt(messageId); var pos = me.storage.readMark.indexOf(messageId); if (pos !== -1) { // Bestätigt-Gelesene Nachricht aus Array entfernen me.storage.readMark.splice(pos, 1); } }); } }); }, /** * Setzt bereits gerenderte Nachrichten auf "Gelesen" * * @param {array} data */ renderMessagesAsRead: function (data) { if (data.length === 0) { return; } // Im öffentlichen Chat gibt es keine Gelesen-Markierungen if (me.isPublicChat()) { return; } $(data).each(function (index, readmark) { if (readmark.id > me.storage.newestReadmarkId) { me.storage.newestReadmarkId = readmark.id; } // Nachricht als Gelesen markieren $(me.selector.messageArea) .find('#message-' + readmark.message) .removeClass('unread') .addClass('read'); }); }, /** * Ruft ältere Nachrichten per AJAX ab; wenn man nach oben scrollt */ fetchOldMessages: function () { // Die älteste Nachricht wurde bereits erreicht if (me.storage.requestEOF === true) { return; } // Request-Puffer ist aktiv; du musst noch warten if (me.storage.requestBuffer > window.performance.now()) { return; } // Puffer setzen; Nur ein Request alle 0,25 Sekunden // Notwendig weil Scroll-Event mehrmals pro Sekunde ausgelöst wird. me.storage.requestBuffer = window.performance.now() + me.settings.lazyLoadingBufferTime; $.ajax({ type: 'POST', url: 'index.php?module=chat&action=list&cmd=messages', data: { user_id: me.storage.activeUserId, before_message_id: me.storage.oldestMessageId }, success: function (data) { if (data.messages.length === 0) { // Ende wurde erreicht; es gibt keine älteren Nachrichten me.renderDateString(me.storage.oldestDateString, 'DESC', true); me.storage.requestEOF = true; return; } me.renderOldMessages(data.messages); } }); }, /** * Rendert die übergebene JSON-Nachrichtenliste zu HTML * * @param {array} data */ renderNewMessages: function (data) { var newMessages = false; // Abbruch wenn keine neuen Nachrichten im JSON sind if (data.length === 0) { return; } if (me.storage.oldestMessageId === null) { me.storage.oldestMessageId = data[0].id; } $(data).each(function (index, msg) { // Zeile mit Datum einfügen me.renderDateString(msg.date, 'ASC', false); // Nachricht in Nachrichtenbereich anfügen me.renderSingleMessage(msg, 'ASC'); newMessages = true; // Ungelesene Nachrichten für Gelesen-Markierung vormerken if (msg.read === false) { me.reserveMessageForReadMark(msg); } }); // Nach unten scrollen, falls eine neue Nachricht dabei war if (newMessages === true) { me.scrollToLatestMessage(); setTimeout(function () { me.enableScrollSpy(); }, 100); } }, /** * Rendert die übergebene JSON-Nachrichtenliste zu HTML * * @param {array} data */ renderOldMessages: function (data) { // Höhe des Nachrichtenbereich merken var oldHeight = $(me.selector.messageArea).height(); data = data.reverse(); $(data).each(function (index, msg) { me.renderDateString(msg.date, 'DESC', false); me.renderSingleMessage(msg, 'DESC'); // Ungelesene Nachrichten für Gelesen-Markierung vormerken if (msg.read === false) { me.reserveMessageForReadMark(msg); } }); // Scroll-Position vor dem Einfügen anzeigen var newHeight = $(me.selector.messageArea).height(); var scrollTop = (newHeight - oldHeight) - 20; me.elem.$messageScroller.scrollTop(scrollTop); }, /** * Nachricht für Gelesen-Markierung vormerken * * @param {object} msg */ reserveMessageForReadMark: function (msg) { if (msg.read === true) { return; } // Ungelesene Nachrichten für Gelesen-Markierung vormerken, wenn // ... Chat mit User if (me.isPrivateChat() && !me.isOwnMessage(msg.user)) { me.storage.readMark.push(msg.id); } // .. Chat mit Öffentlich if (me.isPublicChat()) { me.storage.readMark.push(msg.id); } }, /** * @param {object} msg * @param {string} direction [ASC|DESC] */ renderSingleMessage: function (msg, direction) { // Abbrechen, wenn Nachricht bereits vorhanden ist if ($('#message-' + msg.id).length > 0) { return; } // CSS-Klassen zusammenstellen var classes = 'message'; if (me.isPrivateChat()) { // Bei eigenen Nachrichten anzeigen ob Nachricht gelesen wurde if (me.isOwnMessage(msg.user)) { if (msg.read === false) { classes += ' unread'; } if (msg.read === true) { classes += ' read'; } } } // Fremde ungelesene Nachrichten als NEU markieren if (!me.isOwnMessage(msg.user)) { if (msg.read === false) { classes += ' new'; } } // Prio-Nachrichten markieren if (msg.prio === true) { classes += ' prio'; } // Nachrichten-HTML zusammenstellen var content = '
' + '
' + ' {{alt}}' + '
' + '
' + ' {{name}}' + ' {{time}}' + '
' + '
{{text}}
' + '
'; content = content.replace('{{id}}', msg.id); content = content.replace('{{user}}', msg.user); content = content.replace('{{alt}}', msg.name); content = content.replace('{{name}}', msg.name); content = content.replace('{{time}}', msg.time); content = content.replace('{{text}}', msg.text); content = content.replace('{{classes}}', classes); content = content.replace('{{elementId}}', 'message-' + msg.id); // Neue Nachrichten unten anhängen if (direction === 'ASC') { $(me.selector.messageArea).append(content); } // Ältere Nachrichten oben einfügen if (direction === 'DESC') { $(me.selector.messageArea).prepend(content); } // NEU-Markierung nach fünf Sekunden wieder entfernen setTimeout(function () { $('#message-' + msg.id + '.new').removeClass('new'); }, me.settings.newTimeout); // ID der ältesten und neuesten Nachricht wegspeichern if (msg.id > me.storage.newestMessageId) { me.storage.newestMessageId = msg.id } if (msg.id < me.storage.oldestMessageId) { me.storage.oldestMessageId = msg.id } }, /** * Zeile mit Datum in Nachrichtenbereich einfügen * * @param {string} dateString * @param {string} direction [ASC|DESC] * @param {boolean} force Datum immer ausgeben */ renderDateString: function (dateString, direction, force) { if (typeof direction === 'undefined' || typeof direction !== 'string') { direction = 'ASC'; } if (typeof force === 'undefined' || typeof force !== 'boolean') { force = false; } // Datum wurde bereits ausgegeben if (me.storage.dateStrings.indexOf(dateString) !== -1) { return; } // Merken welche Datumseinträge bereits angezeigt wurden me.storage.dateStrings.push(dateString); if (me.storage.newestDateString === '') { me.storage.newestDateString = dateString; } if (me.storage.oldestDateString === '') { me.storage.oldestDateString = dateString; } // Aufsteigend, wenn neue Nachrichten eingefügt werden if (direction === 'ASC') { if (me.storage.newestDateString !== dateString) { $(me.selector.messageArea).append( '
' + dateString + '
' ); me.storage.newestDateString = dateString; } } // Absteigend, wenn ältere Nachrichten eingefügt werden if (direction === 'DESC') { if (me.storage.oldestDateString !== dateString || force === true) { $(me.selector.messageArea).prepend( '
' + me.storage.oldestDateString + '
' ); me.storage.oldestDateString = dateString; } } }, /** * Scrollt zur neuesten Nachricht */ scrollToLatestMessage: function () { me.elem.$messageScroller.animate({ scrollTop: me.elem.$messageScroller.get(0).scrollHeight }, 100); }, /** * Wechselt den Chat-Partner * * @param {number} userId */ switchToUser: function (userId) { // Storage-Variablen zurücksetzen me.storage.activeUserId = userId; me.storage.newestMessageId = 0; me.storage.oldestMessageId = null; me.storage.oldestDateString = ''; me.storage.newestDateString = ''; me.storage.newestReadmarkId = 0; me.storage.readMark = []; me.storage.dateStrings = []; me.storage.requestBuffer = 0; me.storage.requestEOF = false; // Intervalle anhalten me.stop(); me.updateUserList(); me.emptyMessageArea(); me.fetchNewMessages(true); me.emptyInput(); me.focusInput(); // Intervalle nach zwei Sekunden starten. // Ansonsten kann der Nachrichtenempfang des ersten Intervalls und das // obige fetchNewMessages() zeitgleich passieren. Das führt zu leeren // Datumseinträgen. window.setTimeout(function () { me.start(); }, 2000); }, /** * Höhe und Breite von Elementen festlegen * * Methode wird beim Vergößern/-kleinern des Fensters aufgerufen. * * Vorallem der Nachrichtenbereich benötigt eine feste Breite, um überlangen Text * ohne Whitespace zuverlässig umbrechen zu können. */ resizeElements: function () { var windowHeight = me.elem.$window.height(); var wrapperWidth = me.elem.$wrapper.width(); var sidebarWidth = me.elem.$sidebarWrapper.outerWidth(); var $notificationInfo = $(me.selector.notificationInfo); var notificationHeight = ($notificationInfo.outerHeight() !== null) ? $notificationInfo.outerHeight() + 10 : 0; var newMessageWidth = 0; var newMessageHeight = 0; var newSidebarHeight = 0; if (me.storage.isStandaloneWindow) { newMessageWidth = wrapperWidth - sidebarWidth - 10; newMessageHeight = windowHeight - 200; newSidebarHeight = windowHeight - 90; } else { newMessageWidth = wrapperWidth - sidebarWidth - 10; newMessageHeight = windowHeight - 360; newSidebarHeight = windowHeight - 240; } newMessageHeight -= notificationHeight; newSidebarHeight -= notificationHeight; me.elem.$messageScroller.width(newMessageWidth); me.elem.$messageScroller.height(newMessageHeight); me.elem.$sidebarScroller.height(newSidebarHeight); }, /** * Ältere Nachrichten beim Nach-Oben-Scrollen abrufen */ enableScrollSpy: function () { // Ermitteln ob Nachrichtenbereich überhaupt einen Scrollbalken hat; // Es gibt keinen Scrollbalken bei wenigen oder keinen Nachrichten. var messageAreaHasScrollbar = (me.elem.$messageScroller.height() < $(me.selector.messageArea).height()); // ScrollSpy wird nicht benötigt wenn kein Scrollbalken vorhanden if (messageAreaHasScrollbar === false) { return; } // Ältere Nachrichten abrufen wenn oberer Nachrichtenbereich angezeigt wird me.elem.$messageScroller.on('scroll', function () { var scrollTop = $(this).scrollTop(); if (scrollTop < 50) { me.fetchOldMessages(); } }); }, /** * Scroll-Event auf Nachrichtenbereich entfernen */ disableScrollSpy: function () { me.elem.$messageScroller.off('scroll'); }, /** * Setzt den Focus auf das Eingabefeld */ focusInput: function () { $(me.selector.inputField).trigger('focus'); }, /** * Eingabefeld leeren */ emptyInput: function () { $(me.selector.inputField).val(''); //$(me.selector.prioCheckbox).prop('checked', ''); // Prio-Checkbox nicht zurücksetzen, soll manuell durch Benutzer passieren. }, /** * Leert den Nachrichtenbereich */ emptyMessageArea: function () { // Eventuell vorhandenen Scroll-Event entfernen me.disableScrollSpy(); // Nachrichtenbereich leeren $(me.selector.messageArea).empty(); }, /** * @return {bool} */ isPublicChat: function () { return me.storage.activeUserId === 0; }, /** * @return {bool} */ isPrivateChat: function () { return me.storage.activeUserId > 0; }, /** * @param {number} senderId * @return {bool} */ isOwnMessage: function (senderId) { return senderId === me.storage.selfUserId; }, /** * Puffer-Funktion um Events erst nach einer bestimmten Zeit auszuführen * * @param {function} callback * @param {number} delay */ debounce: function (callback, delay) { var context = this; var args = arguments; window.clearTimeout(me.storage.buffer); me.storage.buffer = window.setTimeout(function () { callback.apply(context, args); }, delay || 250); } }; return { init: me.init, start: me.start, stop: me.stop }; }(jQuery, Push); $(document).ready(function () { Chat.init(); });