OpenXE/classes/Modules/Chat/www/js/chat.js
2021-05-21 08:49:41 +02:00

864 lines
30 KiB
JavaScript

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 = '<div id="submenu-wrapper" class="clearfix"><div class="back">';
navigation += '<a href="index.php?module=welcome&action=start"><svg xmlns="http://www.w3.org/2000/svg" width="7px" height="12px" viewBox="0 0 131.99 218.57"><path fill="none" stroke="#fff" stroke-miterlimit="10" stroke-width="33" d="M120.26 11.65l-97 97.21 97 98.11"/></svg></a>';
navigation += '</div><a href="index.php?module=welcome&action=start" style="display:block;padding: 7px 20px;">Zurück zur Startseite</a>';
navigation += '</div>';
$('#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 =
'<div class="info" id="notification-info">' +
'<table width="100%" border="0" cellpadding="0" cellspacing="0"><tr>' +
'<td>Aktuell können keine Browser-Benachrichtigungen zugestellt werden. ' +
'Bitte Klicken Sie hier um die Nachrichten für Xentral zu erlauben.</td>' +
'<td align="right"><input type="button" id="notification-button" value="Benachrichtigungen erlauben"></td>'+
'</tr></table></div>';
$('#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 = '<ul>';
$(data).each(function (index, user) {
user.id = parseInt(user.id);
// Sich selbst nicht in Userliste anzeigen
if (user.self === true) {
me.storage.selfUserId = user.id;
return;
}
if (user.id === me.storage.activeUserId) {
var roomname = '<img src="index.php?module=ajax&action=profilbild&id=' + user.id + '" alt="' + user.name + '">';
roomname += '<span>Mit ' + user.name + '</span>';
$(me.selector.roomName).html(roomname).removeClass('public');
}
var classes = (user.online === true) ? 'online' : '';
classes += (user.id === me.storage.activeUserId) ? ' active' : '';
content += '<li class="' + classes + '" data-user-id="' + user.id + '">';
content += '<span class="name">' + user.name + '</span> ';
if (user.unread > 0) {
content += '<span class="' + (user.unread > 0 ? 'unread' : '') + '">(' + user.unread + ')</span>';
}
content += '</li>';
});
content += '</ul>';
$(me.selector.userList).html(content);
},
/**
* Rendert die JSON-Raumliste zu HTML
*
* @param {array} data
*/
renderRoomList: function (data) {
var content = '<ul>';
$(data).each(function (index, room) {
var classes = (room.id === me.storage.activeUserId) ? ' active' : '';
if (room.id === me.storage.activeUserId) {
$(me.selector.roomName).html('<span>' + room.name + '</span>').addClass('public');
}
content += '<li class="' + classes + '" data-user-id="' + room.id + '">';
content += '<span class="name">' + room.name + '</span> ';
if (room.unread > 0) {
content += '<span class="' + (room.unread > 0 ? 'unread' : '') + '">(' + room.unread + ')</span>';
}
content += '</li>';
});
content += '</ul>';
$(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 =
'<div data-id="{{id}}" id="{{elementId}}" class="{{classes}}">' +
' <div class="image">' +
' <img src="index.php?module=ajax&action=profilbild&id={{user}}" alt="{{alt}}">' +
' </div>' +
' <div class="head">' +
' <span class="sender">{{name}}</span>' +
' <span class="time">{{time}}</span>' +
' </div>' +
' <div class="text">{{text}}</div>' +
'</div>';
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(
'<div class="date"><span>' + dateString + '</span></div>'
);
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(
'<div class="date"><span>' + me.storage.oldestDateString + '</span></div>'
);
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();
});