From ddb6f87fb8d6b7f1fc9d074dd6863a83b45daee4 Mon Sep 17 00:00:00 2001
From: enured <enis.nuredini@uzh.ch>
Date: Fri, 5 Jan 2024 18:17:11 +0100
Subject: [PATCH] F17462: Final V1.0. refs #17462

---
 extension/Classes/Core/AbstractBuildForm.php |   4 +-
 extension/Classes/Core/Constants.php         |  48 ++++
 extension/Classes/Core/Form/Chat.php         | 223 +++++++++++--------
 extension/Classes/Core/QuickFormQuery.php    |  21 +-
 extension/Classes/Core/Save.php              |  14 --
 extension/Classes/Sql/qfqDefaultTables.sql   |  29 +--
 javascript/src/Helper/qfqChat.js             |  96 ++++++--
 7 files changed, 291 insertions(+), 144 deletions(-)

diff --git a/extension/Classes/Core/AbstractBuildForm.php b/extension/Classes/Core/AbstractBuildForm.php
index 40f4a3444..c1f1fe278 100644
--- a/extension/Classes/Core/AbstractBuildForm.php
+++ b/extension/Classes/Core/AbstractBuildForm.php
@@ -3169,12 +3169,12 @@ abstract class AbstractBuildForm {
         $chatWindowAttribute .= Support::doAttribute('data-load-api', $this->sip->queryStringToSip(Path::urlApi(API_LOAD_PHP) . '?' . 'mode=form_chat_load&chat_config=' . $chatConfigJson, RETURN_URL));
         $chatWindowAttribute .= Support::doAttribute('data-save-api', $this->sip->queryStringToSip(Path::urlApi(API_SAVE_PHP) . '?' . 'mode=form_chat_save&chat_config=' . $chatConfigJson, RETURN_URL));
 
-        $chatHead = '<div class="qfq-chat-window" ' . $chatWindowAttribute . '><span class="fas fa-search chat-search-activate qfq-skip-dirty"></span><div class="chat-search"><input type="text" class="chat-search-input qfq-skip-dirty" placeholder="Search..." ' . $chatConfig['disabledFlag'] . '><button class="chat-search-btn qfq-skip-dirty" ' . $chatConfig['disabledFlag'] . '>Search</button><span class="chat-search-info"></span></div><div class="chat-messages"><div class="fas fa-arrow-circle-up chat-top-symbol"></div>';
+        $chatHead = '<div class="qfq-chat-window" ' . $chatWindowAttribute . '><span class="fas fa-search chat-search-activate qfq-skip-dirty"></span><div class="chat-search"><input type="text" class="chat-search-input qfq-skip-dirty" placeholder="Search..."><button class="chat-search-btn qfq-skip-dirty">Search</button><span class="chat-search-info"></span></div><div class="chat-messages"><div class="fas fa-arrow-circle-up chat-top-symbol"></div>';
         $chatTail = '</div></div>';
 
         $jsonChat = Chat::createChatJson($chatConfig, $this->dbArray[$this->dbIndexQfq]);
 
-        if ($jsonChat['xId'] == 0) {
+        if (empty($jsonChat[FE_TYPE_CHAT])) {
             $chatContent = '<div class="chat-no-message"><span class="label label-default">No messages found</span></div>';
         } else {
             $chatContent = '<div class="chat-loader-container" style="display: none;"><div class="chat-loader"></div></div>';
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index c3764e663..30ae3ce31 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -948,6 +948,8 @@ const API_FORM_UPDATE_DISABLED = 'disabled';
 const API_FORM_UPDATE_REQUIRED = 'required';
 const API_HEADER_VARIABLE_KEY = 'apiResponseHeader';
 
+const API_CHAT_UPDATE_DISABLED = API_FORM_UPDATE_DISABLED;
+
 const API_ELEMENT_UPDATE = 'element-update';
 const API_ELEMENT_ATTRIBUTE = 'attr';
 const API_ELEMENT_CONTENT = 'content';
@@ -1695,6 +1697,52 @@ const FILE_PRIORITY = 'filePriority';
 const FILE_MODE_WRITE = 'w';
 const FILE_MODE_APPEND = 'a';
 
+// Chat constants
+const CHAT_CLIENT_MESSAGE = 'message';
+const CHAT_CLIENT_MESSAGE_ID_LIST = 'messageIdList';
+const CHAT_DB_COLUMN_NAMES = 'dbColumnNames';
+const CHAT_TABLE_NAME = 'tableName';
+const CHAT_COLUMN_MAP = 'columnMap';
+const CHAT_TYPE_MESSAGE = CHAT_CLIENT_MESSAGE;
+const CHAT_COLUMN_ID = 'id';
+const CHAT_COLUMN_XID = 'xId';
+const CHAT_COLUMN_USERNAME = 'username';
+const CHAT_COLUMN_MESSAGE = 'message';
+const CHAT_COLUMN_PID_CREATOR = 'pIdCreator';
+const CHAT_COLUMN_PID_TEAM = 'pIdTeam';
+const CHAT_COLUMN_CID_TOPIC = 'cIdTopic';
+const CHAT_COLUMN_CID_THREAD = 'cIdThread';
+const CHAT_COLUMN_X_GRID_STATUS = 'xGrIdStatus';
+const CHAT_COLUMN_TYPE = 'type';
+const CHAT_COLUMN_TYPE_MESSAGE = 'message';
+const CHAT_COLUMN_TYPE_TAG = 'tag';
+const CHAT_COLUMN_CREATED = 'created';
+
+const CHAT_LOAD_MODE_REFRESH = 'refresh';
+const CHAT_LOAD_MODE_PING = 'ping';
+const CHAT_LOAD_MODE_FIRST = 'first';
+
+const CHAT_MODE_SAVE = 'chat_save';
+const CHAT_MODE_LOAD = 'chat_load';
+
+const CHAT_CONFIG_SIP = 'chat_config';
+
+const CHAT_DONE = 'chatDone';
+const CHAT_WEBSOCKET_RECEIVER_IDS = 'receiverIds';
+const CHAT_FLAG_MORE_RECORDS = 'flagMoreRecords';
+
+const CHAT_PARAMETER_XID = CHAT_COLUMN_XID;
+const CHAT_PARAMETER_PID_CREATOR = CHAT_COLUMN_PID_CREATOR;
+const CHAT_PARAMETER_USERNAME = CHAT_COLUMN_USERNAME;
+const CHAT_PARAMETER_CID_TOPIC = CHAT_COLUMN_CID_TOPIC;
+const CHAT_PARAMETER_OPTION_DONE = 'optionDone';
+const CHAT_PARAMETER_OPTION_REMINDER = 'optionReminder';
+const CHAT_PARAMETER_THREAD = 'thread';
+const CHAT_PARAMETER_PID_TEAM = CHAT_COLUMN_PID_TEAM;
+const CHAT_PARAMETER_X_GRID_STATUS = CHAT_COLUMN_X_GRID_STATUS;
+
+const CHAT_TYPE_TAG_DONE = '__done';
+
 // DATABASE
 const DB_NUM_ROWS = 'numRows';
 const DB_AFFECTED_ROWS = 'affectedRows';
diff --git a/extension/Classes/Core/Form/Chat.php b/extension/Classes/Core/Form/Chat.php
index 8018d8362..9402ac81e 100644
--- a/extension/Classes/Core/Form/Chat.php
+++ b/extension/Classes/Core/Form/Chat.php
@@ -25,11 +25,14 @@ use Ratchet\ConnectionInterface;
  * @package qfq
  */
 class Chat implements MessageComponentInterface {
-    protected $clients;
-    protected $logFile;
-    protected $clientInfo;
+    protected \SplObjectStorage $clients;
+    protected string $logFile;
+    protected array $clientInfo;
     protected $clientConnections;
 
+    /**
+     * @throws \UserFormException
+     */
     public function __construct() {
         $this->clients = new \SplObjectStorage;
         $this->logFile = Path::absoluteWebsocketLogFile();
@@ -71,7 +74,7 @@ class Chat implements MessageComponentInterface {
 
         if (!empty($this->clientInfo[$from->resourceId])) {
             // send ping to client with given receiverId
-            foreach ($this->clientInfo[$from->resourceId]['receiverIds'] as $receiverId) {
+            foreach ($this->clientInfo[$from->resourceId][CHAT_WEBSOCKET_RECEIVER_IDS] as $receiverId) {
                 if (isset($this->clientConnections[$receiverId])) {
                     $this->clientConnections[$receiverId]->send($data->data);
                 }
@@ -94,37 +97,38 @@ class Chat implements MessageComponentInterface {
     }
 
     private function updateReceiverIds($from): void {
-        if (!isset($this->clientInfo[$from->resourceId]['receiverIds'])) {
-            $this->clientInfo[$from->resourceId]['receiverIds'] = array();
+        if (!isset($this->clientInfo[$from->resourceId][CHAT_WEBSOCKET_RECEIVER_IDS])) {
+            $this->clientInfo[$from->resourceId][CHAT_WEBSOCKET_RECEIVER_IDS] = array();
         }
 
         foreach ($this->clients as $client) {
-            $fromCreator = $this->clientInfo[$from->resourceId]['pIdCreator'] ?? null;
-            $clientCreator = $this->clientInfo[$client->resourceId]['pIdCreator'] ?? null;
+            $fromCreator = $this->clientInfo[$from->resourceId][CHAT_COLUMN_PID_CREATOR] ?? null;
+            $clientCreator = $this->clientInfo[$client->resourceId][CHAT_COLUMN_PID_CREATOR] ?? null;
 
-            $fromRecordId = $this->clientInfo[$from->resourceId]['xId'] ?? null;
-            $clientRecordId = $this->clientInfo[$client->resourceId]['xId'] ?? null;
+            $fromRecordId = $this->clientInfo[$from->resourceId][CHAT_COLUMN_XID] ?? null;
+            $clientRecordId = $this->clientInfo[$client->resourceId][CHAT_COLUMN_XID] ?? null;
 
-            $fromTopicId = $this->clientInfo[$from->resourceId]['cIdTopic'] ?? null;
-            $clientTopicId = $this->clientInfo[$client->resourceId]['cIdTopic'] ?? null;
+            $fromTopicId = $this->clientInfo[$from->resourceId][CHAT_COLUMN_CID_TOPIC] ?? null;
+            $clientTopicId = $this->clientInfo[$client->resourceId][CHAT_COLUMN_CID_TOPIC] ?? null;
 
-            $fromThreadId = $this->clientInfo[$from->resourceId]['cIdThread'] ?? null;
-            $clientThreadId = $this->clientInfo[$client->resourceId]['cIdThread'] ?? null;
+            $fromThreadId = $this->clientInfo[$from->resourceId][CHAT_COLUMN_CID_THREAD] ?? null;
+            $clientThreadId = $this->clientInfo[$client->resourceId][CHAT_COLUMN_CID_THREAD] ?? null;
 
             $isDifferentClient = $from !== $client;
-            $isNotInReceiverIds = !in_array($client->resourceId, $this->clientInfo[$from->resourceId]['receiverIds']);
+            $isNotInReceiverIds = !in_array($client->resourceId, $this->clientInfo[$from->resourceId][CHAT_WEBSOCKET_RECEIVER_IDS]);
 
             $isValidCreator = empty($fromGroupList) && $fromCreator === $clientCreator;
             $isValidRecordId = $fromRecordId === $clientRecordId;
             $isValidTopicId = $fromTopicId === $clientTopicId;
 
             if ($isDifferentClient && $isValidRecordId && $isNotInReceiverIds && $isValidTopicId) {
-                $this->clientInfo[$from->resourceId]['receiverIds'][] = $client->resourceId;
+                $this->clientInfo[$from->resourceId][CHAT_WEBSOCKET_RECEIVER_IDS][] = $client->resourceId;
             }
         }
     }
 
-    /**
+    /** Create chat configuration
+     *
      * @param array $formElement
      * @param Store $store
      * @return array
@@ -137,153 +141,198 @@ class Chat implements MessageComponentInterface {
         $chatConfig = array();
 
         // [apId:rId,pId:pIdCreator,grIdStatus:xGrIdStatus,grIdGroup:grIdGroupList,...]
-        $chatConfig['dbColumnNames'] = array (
+        $chatConfig[CHAT_DB_COLUMN_NAMES] = array (
             'id' => 'id',
             'xId' => 'xId',
             'pIdCreator' => 'pIdCreator',
-            'xGrIdStatus' => 'xGrIdStatus',
+            'pIdTeam' => 'pIdTeam',
             'cIdTopic' => 'cIdTopic',
             'cIdThread' => 'cIdThread',
+            'cIdLastRead' => 'cIdLastRead',
+            'xGrIdStatus' => 'xGrIdStatus',
             'type' => 'type',
-            'done' => 'done',
+            'emoticon' => 'emoticon',
             'reference' => 'reference',
             'message' => 'message',
             'username' => 'username',
             'created' => 'created'
         );
 
-        $chatConfig['columnMap'] = $formElement['columnMap'] ?? '[]';
-
-        OnArray::mapColumns($chatConfig['dbColumnNames'], $chatConfig['columnMap']);
-        $chatConfig['tableName'] = $formElement['tableName'] ?? 'Chat';
+        $chatConfig[CHAT_COLUMN_MAP] = $formElement[CHAT_COLUMN_MAP] ?? '[]';
 
-        $chatConfig['xId'] = $formElement['xId'] ?? 0;
-        $chatConfig['pIdCreator'] = empty($formElement['pIdCreator']) ? 0 : $formElement['pIdCreator'];
-        $chatConfig['pIdTeam'] = $formElement['pIdTeam'] ?? 0;
-        $chatConfig['username'] = empty($formElement['username']) ? 'Anonymous' : $formElement['username'];
-        $chatConfig['xGrIdStatus'] = $formElement['xGrIdStatus'] ?? 0;
-        $chatConfig['cIdTopic'] = $formElement['cIdTopic'] ?? 0;
-        $chatConfig['optionDone'] = $formElement['optionDone'] ?? 'off';
-        $chatConfig['optionReminder'] = $formElement['optionReminder'] ?? 0;
-        $chatConfig['thread'] = $formElement['thread'] ?? 0;
+        OnArray::mapColumns($chatConfig[CHAT_DB_COLUMN_NAMES], $chatConfig[CHAT_COLUMN_MAP]);
+        $chatConfig[CHAT_TABLE_NAME] = $formElement[CHAT_TABLE_NAME] ?? 'Chat';
 
-        $chatConfig['disabledFlag'] = $formElement['disabledFlag'] ?? '';
+        $chatConfig[CHAT_COLUMN_XID] = $formElement[CHAT_PARAMETER_XID] ?? 0;
+        $chatConfig[CHAT_COLUMN_PID_CREATOR] = empty($formElement[CHAT_PARAMETER_PID_CREATOR]) ? 0 : $formElement[CHAT_PARAMETER_PID_CREATOR];
+        $chatConfig[CHAT_COLUMN_PID_TEAM] = $formElement[CHAT_PARAMETER_PID_TEAM] ?? 0;
+        $chatConfig[CHAT_COLUMN_USERNAME] = empty($formElement[CHAT_PARAMETER_USERNAME]) ? 'Anonym' : $formElement[CHAT_PARAMETER_USERNAME];
+        $chatConfig[CHAT_COLUMN_X_GRID_STATUS] = $formElement[CHAT_PARAMETER_X_GRID_STATUS] ?? 0;
+        $chatConfig[CHAT_COLUMN_CID_TOPIC] = $formElement[CHAT_PARAMETER_CID_TOPIC] ?? 0;
+        $chatConfig[CHAT_PARAMETER_OPTION_DONE] = $formElement[CHAT_PARAMETER_OPTION_DONE] ?? 'off';
+        $chatConfig[CHAT_PARAMETER_OPTION_REMINDER] = $formElement[CHAT_PARAMETER_OPTION_REMINDER] ?? 0;
+        $chatConfig[CHAT_PARAMETER_THREAD] = $formElement[CHAT_PARAMETER_THREAD] ?? 0;
 
         return $chatConfig;
     }
 
-    public static function createChatJson($chatConfig, Database $db, $messageIdList = '', $loadMode = 'refresh'): array {
+    /** Fetch from client requested messages and return them as a json.
+     *
+     * @param $chatConfig
+     * @param Database $db
+     * @param string $messageIdList
+     * @param string $loadMode
+     * @return array
+     * @throws \CodeException
+     * @throws \DbException
+     * @throws \UserFormException
+     */
+    public static function createChatJson($chatConfig, Database $db, string $messageIdList = '', string $loadMode = CHAT_LOAD_MODE_REFRESH): array {
         $json = array();
-        $sqlLimit = 10;
+        $sqlLimit = 11;
 
         // Define LIMIT 10 for lazy loading (refresh) or LIMIT 1 for client ping as new sender message
-        if ($loadMode === 'ping') {
+        if ($loadMode === CHAT_LOAD_MODE_PING) {
             $sqlLimit = 1;
         }
 
-        $dbColumnNames = $chatConfig['dbColumnNames'] ?? array();
+        $dbColumnNames = $chatConfig[CHAT_DB_COLUMN_NAMES] ?? array();
         // Get all dones withou threadId
         $sqlDoneNoThread = 'SELECT ' . implode(', ', array_map(function($column) use ($dbColumnNames) {
                 return $dbColumnNames[$column];
-            }, ['pIdCreator']))
-            . ' FROM ' . $chatConfig['tableName'] . ' WHERE ' . $dbColumnNames['cIdTopic'] . ' = ? AND ' . $dbColumnNames['xId'] . ' = ? AND ' . $dbColumnNames['type'] . ' = ? ORDER BY ' . $dbColumnNames['id'];
-        $sqlDoneNotThreadParams = [$chatConfig['cIdTopic'], $chatConfig['xId'], 'done'];
+            }, [CHAT_COLUMN_PID_CREATOR]))
+            . ' FROM ' . $chatConfig[CHAT_TABLE_NAME] . ' WHERE ' . $dbColumnNames[CHAT_COLUMN_CID_TOPIC] . ' = ? AND '
+            . $dbColumnNames[CHAT_COLUMN_XID] . ' = ? AND ' . $dbColumnNames[CHAT_COLUMN_TYPE] . ' = ? AND ' . $dbColumnNames[CHAT_COLUMN_MESSAGE]
+            . ' =  ? ORDER BY ' . $dbColumnNames[CHAT_COLUMN_ID];
+
+        $sqlDoneNotThreadParams = [$chatConfig[CHAT_COLUMN_CID_TOPIC], $chatConfig[CHAT_COLUMN_XID], CHAT_COLUMN_TYPE_TAG, CHAT_TYPE_TAG_DONE];
 
         // Get all messages from database. without threadId
         $sqlAllNotThread = 'SELECT ' . implode(', ', array_map(function($column) use ($dbColumnNames) {
                 return $dbColumnNames[$column];
-            }, ['id', 'xId', 'pIdCreator', 'cIdTopic', 'message', 'username', 'created']))
-            . ' FROM ' . $chatConfig['tableName']
-            . ' WHERE ' . $dbColumnNames['xId'] . ' = ? AND ' . $dbColumnNames['cIdTopic'] . ' = ? AND !FIND_IN_SET(' . $dbColumnNames['id'] . ', ?) AND ' . $dbColumnNames['type'] . ' = ? '
-            . 'ORDER BY ' . $dbColumnNames['created'] . ' DESC LIMIT ' . $sqlLimit;
-        $sqlAllNotThreadParams = [$chatConfig['xId'], $chatConfig['cIdTopic'], $messageIdList, 'message'];
+            }, [CHAT_COLUMN_ID, CHAT_COLUMN_XID, CHAT_COLUMN_PID_CREATOR, CHAT_COLUMN_CID_TOPIC, CHAT_COLUMN_MESSAGE, CHAT_COLUMN_USERNAME, CHAT_COLUMN_CREATED]))
+            . ' FROM ' . $chatConfig[CHAT_TABLE_NAME]
+            . ' WHERE ' . $dbColumnNames[CHAT_COLUMN_XID] . ' = ? AND ' . $dbColumnNames[CHAT_COLUMN_CID_TOPIC] . ' = ? AND !FIND_IN_SET(' . $dbColumnNames[CHAT_COLUMN_ID]
+            . ', ?) AND ' . $dbColumnNames[CHAT_COLUMN_TYPE] . ' = ? '
+            . 'ORDER BY ' . $dbColumnNames[CHAT_COLUMN_CREATED] . ' DESC LIMIT ' . $sqlLimit;
+
+        $sqlAllNotThreadParams = [$chatConfig[CHAT_COLUMN_XID], $chatConfig[CHAT_COLUMN_CID_TOPIC], $messageIdList, CHAT_COLUMN_TYPE_MESSAGE];
 
         $resultAllDone = $db->sql($sqlDoneNoThread, ROW_REGULAR, $sqlDoneNotThreadParams);
         $resultAll = $db->sql($sqlAllNotThread, ROW_REGULAR, $sqlAllNotThreadParams);
+
+        // Get moreResults flag for client and remove the last one
+        $flagMoreRecords = count($resultAll) === 11;
+        if ($flagMoreRecords) {
+            array_pop($resultAll);
+        }
+
         $chatJson = array();
 
         if (!empty($resultAll)) {
             foreach ($resultAll as $message) {
-                $chatJson[$message[$dbColumnNames['id']]] = $message;
+                $chatJson[$message[$dbColumnNames[CHAT_COLUMN_ID]]] = $message;
             }
         }
         // Return last message id as value
         if (!empty($resultAll)) {
-            $columnId = array_column($resultAll, $dbColumnNames[COLUMN_ID]);
-            $recordId = array_column($resultAll, $dbColumnNames['xId']);
-            $createdDate = array_column($resultAll, $dbColumnNames[COLUMN_CREATED]);
+            $columnId = array_column($resultAll, $dbColumnNames[CHAT_COLUMN_ID]);
+            $recordId = array_column($resultAll, $dbColumnNames[CHAT_COLUMN_XID]);
+            $createdDate = array_column($resultAll, $dbColumnNames[CHAT_COLUMN_CREATED]);
 
             $value = max($columnId);
             $recordId = max($recordId);
             $createdDate = max($createdDate);
         }
 
-        $json['xId'] = $recordId ?? 0;
-        $json[COLUMN_CREATED] = $createdDate ?? '';
+        $json[CHAT_COLUMN_XID] = $recordId ?? 0;
+        $json[CHAT_COLUMN_CREATED] = $createdDate ?? '';
         $json[FE_TYPE_CHAT] = $chatJson;
-        $json['chatDone'] = $resultAllDone;
-        $json['pIdCreator'] = $chatConfig['pIdCreator'];
-        $json['dbColumnNames'] = $dbColumnNames;
+        $json[CHAT_DONE] = $resultAllDone;
+        $json[CHAT_COLUMN_PID_CREATOR] = $chatConfig[CHAT_COLUMN_PID_CREATOR];
+        $json[CHAT_DB_COLUMN_NAMES] = $dbColumnNames;
+        $json[CHAT_FLAG_MORE_RECORDS] = $loadMode === CHAT_LOAD_MODE_PING ? null : $flagMoreRecords;
 
         return $json;
     }
 
+    /** Save new message given from client with given chat configuration
+     *
+     * @param $chatConfig
+     * @param $feMode
+     * @param Store $store
+     * @param Database $db
+     * @return array
+     * @throws \CodeException
+     * @throws \DbException
+     * @throws \UserFormException
+     * @throws \UserReportException
+     */
     public static function saveChatMessage($chatConfig, $feMode, Store $store, Database $db): array {
         $json = array();
         $chatJson = array();
 
-        if ($feMode === FE_MODE_HIDDEN || $feMode === FE_MODE_READONLY) {
+        if ($feMode[API_CHAT_UPDATE_DISABLED]) {
             return $json;
         }
 
         // Get current value from client store
-        $value = $store::getVar('message', STORE_CLIENT, SANITIZE_ALLOW_ALLBUT);
+        $value = $store::getVar(CHAT_CLIENT_MESSAGE, STORE_CLIENT, SANITIZE_ALLOW_ALLBUT);
 
-        $chatConfig = Chat::createChatConfig($chatConfig, $store, $db);
+        $chatConfig = Chat::createChatConfig($chatConfig);
         $chatParams = [
-            'xId' => $chatConfig['xId'] ?? 0,
-            'pIdCreator' => $chatConfig['pIdCreator'] ?? 0,
-            'xGrIdStatus' => $chatConfig['xGrIdStatus'] ?? 0,
-            'cIdTopic' => $chatConfig['cIdTopic'] ?? 0,
-            'username' => $chatConfig['username'] ?? 0
+            CHAT_COLUMN_XID => $chatConfig[CHAT_COLUMN_XID] ?? 0,
+            CHAT_COLUMN_PID_CREATOR => $chatConfig[CHAT_COLUMN_PID_CREATOR] ?? 0,
+            CHAT_COLUMN_CID_TOPIC => $chatConfig[CHAT_COLUMN_CID_TOPIC] ?? 0,
+            CHAT_COLUMN_USERNAME => $chatConfig[CHAT_COLUMN_USERNAME] ?? 0,
+            CHAT_COLUMN_X_GRID_STATUS => $chatConfig[CHAT_COLUMN_X_GRID_STATUS] ?? 0
         ];
 
-        $dbColumnNames = $chatConfig["dbColumnNames"];
+        $dbColumnNames = $chatConfig[CHAT_DB_COLUMN_NAMES];
 
         // Insert new record for chat
-        $sqlInsert = 'INSERT INTO ' . $chatConfig['tableName'] . '(' . $dbColumnNames["xId"]  . ', '. $dbColumnNames["pIdCreator"]
-            . ',' . $dbColumnNames["xGrIdStatus"] . ',' . $dbColumnNames["cIdTopic"] . ','
-            . $dbColumnNames["message"] . ',' . $dbColumnNames["username"] . ')'
+        $sqlInsert = 'INSERT INTO ' . $chatConfig[CHAT_TABLE_NAME] . '(' . $dbColumnNames[CHAT_COLUMN_XID]  . ', '. $dbColumnNames[CHAT_COLUMN_PID_CREATOR]
+            . ',' . $dbColumnNames[CHAT_COLUMN_X_GRID_STATUS] . ',' . $dbColumnNames[CHAT_COLUMN_CID_TOPIC] . ','
+            . $dbColumnNames[CHAT_COLUMN_MESSAGE] . ',' . $dbColumnNames[CHAT_COLUMN_USERNAME] . ')'
             . ' VALUES (?,?,?,?,?,?)';
 
-        $insertParams = [$chatParams['xId'], $chatParams['pIdCreator'], $chatParams['xGrIdStatus'], $chatParams['cIdTopic'], $value, $chatParams['username']];
+        $insertParams = [$chatParams[CHAT_COLUMN_XID], $chatParams[CHAT_COLUMN_PID_CREATOR], $chatParams[CHAT_COLUMN_X_GRID_STATUS], $chatParams[CHAT_COLUMN_CID_TOPIC], $value, $chatParams[CHAT_COLUMN_USERNAME]];
         $id = $db->sql($sqlInsert, ROW_REGULAR, $insertParams);
 
         // Get inserted record data for chat json
         $sql = 'SELECT ' . implode(', ', array_map(function($column) use ($dbColumnNames) {
                 return $dbColumnNames[$column];
-            }, ['id', 'xId', 'pIdCreator', 'cIdTopic', 'message', 'username', 'created']))
-            . ' FROM ' . $chatConfig['tableName']
-            . ' WHERE ' . $dbColumnNames['id'] . ' = ?';
+            }, [CHAT_COLUMN_ID, CHAT_COLUMN_XID, CHAT_COLUMN_PID_CREATOR, CHAT_COLUMN_CID_TOPIC, CHAT_COLUMN_MESSAGE, CHAT_COLUMN_USERNAME, CHAT_COLUMN_CREATED]))
+            . ' FROM ' . $chatConfig[CHAT_TABLE_NAME]
+            . ' WHERE ' . $dbColumnNames[CHAT_COLUMN_ID] . ' = ?';
         $sqlParams = [$id];
 
         $resultAll = $db->sql($sql, ROW_REGULAR, $sqlParams);
 
         if (!empty($resultAll)) {
             foreach ($resultAll as $message) {
-                $chatJson[$message[$dbColumnNames['id']]] = $message;
+                $chatJson[$message[$dbColumnNames[CHAT_COLUMN_ID]]] = $message;
             }
         }
 
-        $json['xId'] = $recordId ?? 0;
-        $json[COLUMN_CREATED] = $createdDate ?? date("Y-m-d H:i:s");
+        $json[CHAT_COLUMN_XID] = $recordId ?? 0;
+        $json[CHAT_COLUMN_CREATED] = $createdDate ?? date("Y-m-d H:i:s");
         $json[FE_TYPE_CHAT] = $chatJson;
-        $json['pIdCreator'] = $chatConfig['pIdCreator'];
-        $json['dbColumnNames'] = $chatConfig["dbColumnNames"];
+        $json[CHAT_COLUMN_PID_CREATOR] = $chatConfig[CHAT_COLUMN_PID_CREATOR];
+        $json[CHAT_DB_COLUMN_NAMES] = $chatConfig[CHAT_DB_COLUMN_NAMES];
+        $json[CHAT_FLAG_MORE_RECORDS] = null;
 
         return $json;
     }
 
+
+    /** Optional function to compare two different comma separated lists
+     * If one of the value matches, true will be returned.
+     *
+     * @param $groupList1
+     * @param $groupList2
+     * @return bool
+     */
     public static function existsInGroup($groupList1, $groupList2): bool {
         $bool = false;
 
@@ -300,22 +349,14 @@ class Chat implements MessageComponentInterface {
         return $bool;
     }
 
-    public static function checkAccess($groupList1, $groupList2, &$fe, &$placeholder = ''): void {
-        if (!empty($groupList1)) {
-            if (!OnString::isValidNumberList($groupList1) || !OnString::isValidNumberList($groupList2)) {
-                throw new \UserFormException("Invalid grIdGroupList or grIdCreatorGroupList parameter.", ERROR_INVALID_CHAT_DATA);
-            }
-
-            if (!Chat::existsInGroup($groupList1, $groupList2)) {
-                $placeholder = 'placeholder="No access for this chat."';
-                $fe[FE_MODE] = $fe[FE_MODE] === FE_MODE_HIDDEN ? $fe[FE_MODE] : FE_MODE_READONLY;
-            }
-        }
-    }
-
+    /** Function to refresh xId in client info if given
+     * @param $from
+     * @param $data
+     * @return void
+     */
     private function refreshRecordId($from, $data): void {
-        if ($this->clientInfo[$from->resourceId]['xId'] == 0 && $data->xId) {
-            $this->clientInfo[$from->resourceId]['xId'] = $data->xId;
+        if ($this->clientInfo[$from->resourceId][CHAT_COLUMN_XID] == 0 && $data->xId) {
+            $this->clientInfo[$from->resourceId][CHAT_COLUMN_XID] = $data->xId;
         }
     }
 }
\ No newline at end of file
diff --git a/extension/Classes/Core/QuickFormQuery.php b/extension/Classes/Core/QuickFormQuery.php
index e22e673d4..939926629 100644
--- a/extension/Classes/Core/QuickFormQuery.php
+++ b/extension/Classes/Core/QuickFormQuery.php
@@ -2650,16 +2650,23 @@ EOF;
         }
     }
 
-    private function doChat($mode = 'chat_load'): array {
+    /**
+     * @throws \CodeException
+     * @throws \UserFormException
+     * @throws \DbException
+     * @throws \UserReportException
+     */
+    private function doChat($mode = CHAT_MODE_LOAD): array {
         // Get currently existing messageIds from client
-        $messageIdList = $this->store::getVar('messageIdList', STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL);
-        $loadMode = $this->store::getVar('loadMode', STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL);
-        $chatConfig = $this->store::getVar('chat_config', STORE_SIP . STORE_EMPTY, SANITIZE_ALLOW_ALL);
+        $messageIdList = $this->store::getVar(CHAT_CLIENT_MESSAGE_ID_LIST, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL);
+        $loadMode = $this->store::getVar('load_mode', STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL);
+        $elementModeJson = $this->store::getVar('element_mode', STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL);
+        $elementMode = json_decode($elementModeJson, true);
+        $chatConfig = $this->store::getVar(CHAT_CONFIG_SIP, STORE_SIP . STORE_EMPTY, SANITIZE_ALLOW_ALL);
         $chatConfig = json_decode($chatConfig, true);
 
-        $feMode = 'show';
-        if ($mode === 'chat_save') {
-            return Chat::saveChatMessage($chatConfig, $feMode, $this->store, $this->dbArray[$this->dbIndexQfq]);
+        if ($mode === CHAT_MODE_SAVE) {
+            return Chat::saveChatMessage($chatConfig, $elementMode, $this->store, $this->dbArray[$this->dbIndexQfq]);
         } else {
             return Chat::createChatJson($chatConfig, $this->dbArray[$this->dbIndexQfq], $messageIdList, $loadMode);
         }
diff --git a/extension/Classes/Core/Save.php b/extension/Classes/Core/Save.php
index 44502a4fd..7e3d437c6 100644
--- a/extension/Classes/Core/Save.php
+++ b/extension/Classes/Core/Save.php
@@ -416,21 +416,7 @@ class Save {
         $formValues = $this->createEmptyTemplateGroupElements($formValues);
 
         $feColumnTypes = array();
-        $feColumnTypeChat = array();
         foreach ($this->feSpecNative as $fe) {
-            if ($fe[FE_TYPE] === FE_TYPE_CHAT && isset($formValues[$fe[FE_NAME]]) && $formValues[$fe[FE_NAME]] !== '') {
-                $feColumnTypeChat[$fe[FE_NAME]][FE_VALUE] = $formValues[$fe[FE_NAME]];
-
-                // Check for access and change fe mode dynamically
-                $groupList1 = isset($fe['grIdGroupList']) ? $this->evaluate->parse($fe['grIdGroupList'], ROW_REGULAR) : '';
-                $groupList2 = isset($fe['grIdCreatorGroupList']) ? $this->evaluate->parse($fe['grIdCreatorGroupList'], ROW_REGULAR) : '';
-                $fe['grIdGroupList'] = $groupList1;
-                Chat::checkAccess($groupList1, $groupList2, $fe);
-
-                $feColumnTypeChat[$fe[FE_NAME]]['fe'] = $fe;
-                continue;
-            }
-
             $feColumnTypes[$fe[FE_NAME]] = $fe[FE_TYPE];
         }
 
diff --git a/extension/Classes/Sql/qfqDefaultTables.sql b/extension/Classes/Sql/qfqDefaultTables.sql
index c18fccfea..db4fba992 100644
--- a/extension/Classes/Sql/qfqDefaultTables.sql
+++ b/extension/Classes/Sql/qfqDefaultTables.sql
@@ -313,20 +313,21 @@ CREATE TABLE IF NOT EXISTS `FileUpload`
 # Used to save chat conversations.
 CREATE TABLE IF NOT EXISTS `Chat`
 (
-    `id`            INT(11)                                     NOT NULL AUTO_INCREMENT,
-    `xId`           INT(11)                                     NOT NULL DEFAULT '0',
-    `pIdCreator`    INT(11)                                     NOT NULL DEFAULT '0',
-    `pIdTeam`       INT(11)                                     NOT NULL DEFAULT '0',
-    `cIdTopic`      INT(11)                                     NOT NULL DEFAULT '0',
-    `cIdThread`     INT(11)                                     NOT NULL DEFAULT '0',
-    `xGrIdStatus`   INT(11)                                     NOT NULL DEFAULT '0',
-    `type`          ENUM('message','topic','done','reminder')   NOT NULL DEFAULT 'message',
-    `done`          ENUM('yes','no')                            NOT NULL DEFAULT 'no',
-    `message`       TEXT                                        NOT NULL DEFAULT '',
-    `reference`     VARCHAR(255)                                NOT NULL DEFAULT '',
-    `username`      VARCHAR(128)                                NOT NULL DEFAULT '',
-    `modified`      DATETIME                                    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    `created`       DATETIME                                    NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `id`            INT(11)                                             NOT NULL AUTO_INCREMENT,
+    `xId`           INT(11)                                             NOT NULL DEFAULT '0',
+    `pIdCreator`    INT(11)                                             NOT NULL DEFAULT '0',
+    `pIdTeam`       INT(11)                                             NOT NULL DEFAULT '0',
+    `cIdTopic`      INT(11)                                             NOT NULL DEFAULT '0',
+    `cIdThread`     INT(11)                                             NOT NULL DEFAULT '0',
+    `cIdLastRead`   INT(11)                                             NOT NULL DEFAULT '0',
+    `xGrIdStatus`   INT(11)                                             NOT NULL DEFAULT '0',
+    `type`          ENUM('message','topic','tag','reminder','read')     NOT NULL DEFAULT 'message',
+    `message`       TEXT                                                NOT NULL DEFAULT '',
+    `username`      VARCHAR(128)                                        NOT NULL DEFAULT '',
+    `emoticon`      VARCHAR(255)                                        NOT NULL DEFAULT '',
+    `reference`     VARCHAR(255)                                        NOT NULL DEFAULT '',
+    `modified`      DATETIME                                            NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    `created`       DATETIME                                            NOT NULL DEFAULT CURRENT_TIMESTAMP,
     PRIMARY KEY (`id`),
     KEY `xId` (`xId`)
 ) ENGINE = InnoDB
diff --git a/javascript/src/Helper/qfqChat.js b/javascript/src/Helper/qfqChat.js
index e9ff7e3a4..3b31f2e48 100644
--- a/javascript/src/Helper/qfqChat.js
+++ b/javascript/src/Helper/qfqChat.js
@@ -62,13 +62,15 @@ QfqNS.Helper = QfqNS.Helper || {};
             this.chatConfig = this.chatWindow.getAttribute("data-chat-config");
             this.loadApi = this.chatWindow.getAttribute("data-load-api");
             this.saveApi = this.chatWindow.getAttribute("data-save-api");
-            this.sipChatElement = document.querySelector('input[name="s"]');
             this.currentSearchIndex = 0;
             this.searchResults = [];
             this.lastSearchTerm = '';
             this.connection = '';
             this.chatRefresh = false;
             this.chatLoaded = false;
+            this.flagMoreRecords = false;
+            this.topBtnClicked = false;
+            this.isLoadingMessages = false;
             this.init();
         }
 
@@ -88,7 +90,12 @@ QfqNS.Helper = QfqNS.Helper || {};
 
             if (this.topBtn) {
                 this.topBtn.addEventListener('click', () => {
+                    let that = this;
+                    that.topBtnClicked = true;
+                    this.loadNextMessages(that);
+
                     this.chatMessages.scrollTo({ top: 0, behavior: 'smooth' });
+                    this.checkOverflow();
                 });
             }
 
@@ -126,8 +133,15 @@ QfqNS.Helper = QfqNS.Helper || {};
 
             this.chatMessages.addEventListener('scroll', () => {
                 let that = this;
+                if (that.topBtnClicked) {
+                    if (that.chatMessages.scrollTop === 0) {
+                        that.topBtnClicked = false;
+                    }
+                    this.checkOverflow();
+                    return;
+                }
+                this.checkScrollPosition();
                 this.checkOverflow();
-                this.loadNextMessages(that);
             });
 
             this.checkOverflow();
@@ -182,11 +196,12 @@ QfqNS.Helper = QfqNS.Helper || {};
             }
         }
 
+        // Scroll to the bottom of chat window.
         scrollToBottom() {
-            var forceReflow = this.chatMessages.offsetHeight;
             this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
         }
 
+        // Handle search with given criteria.
         handleSearch() {
             let filter = this.searchInput.value;
 
@@ -203,6 +218,7 @@ QfqNS.Helper = QfqNS.Helper || {};
             this.scrollToCurrentResult();
         }
 
+        // Execute the search logic.
         performSearch(filter) {
             // Implement the search logic here
             // Update this.searchResults based on the search
@@ -256,6 +272,7 @@ QfqNS.Helper = QfqNS.Helper || {};
             }, this);
         }
 
+        // Scroll to currently searched result
         scrollToCurrentResult() {
             if (this.searchResults.length > 0 && this.searchResults[this.currentSearchIndex]) {
                 const selectedElement = this.searchResults[this.currentSearchIndex].element;
@@ -275,16 +292,22 @@ QfqNS.Helper = QfqNS.Helper || {};
             this.updateSearchInfo();
         }
 
+        // Hide or show the top button
         checkOverflow() {
-            if (this.chatMessages.scrollTop > 5) {
-                this.topBtn.style.visibility = 'visible';
-            } else {
-                this.topBtn.style.visibility = 'hidden';
-            }
+            setTimeout(() => {
+                if (this.chatMessages.scrollTop > 5) {
+                    this.topBtn.style.visibility = 'visible';
+                } else if (this.flagMoreRecords) {
+                    this.topBtn.style.visibility = 'visible';
+                } else {
+                    this.topBtn.style.visibility = 'hidden';
+                }
+            }, 200);
         }
 
+        // Load the next 10 messages if scrolling is on top and there exists more records.
         loadNextMessages(that) {
-            if (this.chatMessages.scrollTop === 0) {
+            if (this.chatMessages.scrollTop === 0 && this.flagMoreRecords) {
                 this.getMessagesFromServer(that, 'refresh');
             }
         }
@@ -299,12 +322,14 @@ QfqNS.Helper = QfqNS.Helper || {};
             }
         }
 
+        // Reset the search filter input
         resetFilter(filter) {
             this.currentSearchIndex = 0;
             this.searchResults = [];
             this.lastSearchTerm = filter;
         }
 
+        // Set up the html for the individual message bubble.
         createNewMessagePlain(key, message, actualChatState, dbColumnNames) {
             let chatConfig = {};
             let bubbleClass = 'chat-left-bubble';
@@ -378,6 +403,10 @@ QfqNS.Helper = QfqNS.Helper || {};
             return chatContainerElement;
         }
 
+        // Create the html chat content with response data elements from server
+        // The messages will be automatically ordered and set in right place over the given id as key.
+        // This function is very flexible and can manage messages which aren't in right order returned from server.
+        // Server always gets a list from currently showing messages which allows us to response only the none existing ones.
         refreshChat(element, chatItems, actualChatState, dbColumnNames, load_mode) {
             let messageIdKey = dbColumnNames.id;
 
@@ -431,6 +460,8 @@ QfqNS.Helper = QfqNS.Helper || {};
             }
         }
 
+        // Load message data from server with given chat configuration.
+        // Result is returned as json.
         getMessagesFromServer(that, load_mode) {
             // Server request over load.php only if spinner exists // place holder !== null
             if ((that.chatSpinner === undefined || that.chatSpinner === null) && load_mode === 'first') {
@@ -442,7 +473,7 @@ QfqNS.Helper = QfqNS.Helper || {};
             let messageIdList = messageIds.join(',');
 
             messageIdList = qfqChat.htmlEncode(messageIdList);
-            let serializedForm = `loadMode=${load_mode}&messageIdList=${encodeURIComponent(messageIdList)}`;
+            let serializedForm = `load_mode=${load_mode}&messageIdList=${encodeURIComponent(messageIdList)}`;
 
             // Loading spinner only if page loads first time
             if (load_mode === 'first') {
@@ -465,6 +496,9 @@ QfqNS.Helper = QfqNS.Helper || {};
                     actualState.pIdCreator = response['chat-update'].pIdCreator;
                     actualState.created = response['chat-update'].created;
                     actualState.chatDone = response['chat-update'].chatDone;
+                    if (response['chat-update'].flagMoreRecords !== null) {
+                        that.flagMoreRecords = response['chat-update'].flagMoreRecords;
+                    }
 
                     that.refreshChat(that.chatWindow, chatItems, actualState, dbColumnNames, load_mode);
                     that.chatSpinner.style.display = 'none';
@@ -483,9 +517,29 @@ QfqNS.Helper = QfqNS.Helper || {};
             xhr.send(serializedForm);
         }
 
-    }
+        // Load next 10 previous messages if scroll on top is reached
+        checkScrollPosition() {
+            // Check if the user has scrolled to the top
+            if (this.chatMessages.scrollTop === 0) {
+                let that = this;
+                if (this.isLoadingMessages) {
+                    return;
+                }
+                this.isLoadingMessages = true;
+                let oldScrollHeight = this.chatMessages.scrollHeight;
+
+                this.loadNextMessages(that);
 
+                setTimeout(() => {
+                    let newScrollHeight = this.chatMessages.scrollHeight;
+                    this.chatMessages.scrollTop += (newScrollHeight - oldScrollHeight);
+                    this.isLoadingMessages = false;
+                }, 100);
+            }
+        }
+    }
 
+    // Set the new input state which comes over dynamic update. The only feature that is dependent on the form functionality.
     qfqChat.setInputState = function (element, configItem) {
         let inputContainer = element.nextElementSibling;
         let inputElement = inputContainer.querySelector(".chat-input-field");
@@ -493,21 +547,23 @@ QfqNS.Helper = QfqNS.Helper || {};
         inputElement.required = configItem.required;
     }
 
+    // Get the disabled required properties from input element. Can be useful on server side.
     qfqChat.getInputState = function (inputElement) {
-        let feMode = [];
-        feMode.required = inputElement.required;
-        feMode.disabled = inputElement.disabled;
+        let elementMode = {};
+        elementMode.required = inputElement.required;
+        elementMode.disabled = inputElement.disabled;
 
-        return JSON.stringify(feMode);
+        return JSON.stringify(elementMode);
     }
 
+    // Handle chat message submit.
     qfqChat.submit = function(that) {
         let chatInput = that.chatInput.value;
         chatInput = chatInput.trim();
         chatInput = this.htmlEncode(chatInput);
         let inputState = this.getInputState(that.chatInput);
 
-        let serializedForm = `message=${encodeURIComponent(chatInput)}&feMode=${inputState}`;
+        let serializedForm = `message=${encodeURIComponent(chatInput)}&element_mode=${inputState}`;
         let submitUrl = that.saveApi;
 
         if (chatInput === ''){
@@ -532,7 +588,14 @@ QfqNS.Helper = QfqNS.Helper || {};
                 actualState.created = response['chat-update'].created;
                 actualState.chatDone = response['chat-update'].chatDone;
 
+                // Set the flag only if response is not null.
+                if (response['chat-update'].flagMoreRecords !== null) {
+                    that.flagMoreRecords = response['chat-update'].flagMoreRecords;
+                }
+
+                // Create chat content
                 that.refreshChat(that.chatWindow, chatItems, actualState, dbColumnNames, 'save');
+
                 // Clear input field
                 that.chatInput.value = '';
 
@@ -558,6 +621,7 @@ QfqNS.Helper = QfqNS.Helper || {};
         xhr.send(serializedForm);
     };
 
+    // Encode message input to prevent JS scripting attacks.
     qfqChat.htmlEncode = function(str) {
         let div = document.createElement('div');
         div.textContent = str;
-- 
GitLab