changeset 85:05b500bd6235

chat: chat implementation, first draft: this chat use the new dynamic pages feature. Updates are pushed directly by server. Identities are used to retrieve avatar, and first letter of nickname is used to generate an avatar is none is found (temporary, a more elaborate avatar generation should follow in the future). Scroll is done automatically when new messages arrive, except if scroll is not at the end, as it probably means that user is checking history. User can resize text area and use [shift] + [enter] to enter multi-line messages. History will then scroll to bottom after message has been sent.
author Goffi <goffi@goffi.org>
date Wed, 03 Jan 2018 01:12:16 +0100
parents b2ef34e602cf
children 92ca411ee635
files default/chat/chat.html default/chat/message.html default/components/common.html default/static/chat.css default/static/chat.js
diffstat 5 files changed, 199 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/default/chat/chat.html	Wed Jan 03 01:12:16 2018 +0100
@@ -0,0 +1,22 @@
+{{ script.include('chat') }}
+{% if not embedded %}{% extends 'base/base.html' %}{% endif %}
+{% block title %}{{ target_jid }} - {{ super() }}{% endblock %}
+{% block body %}
+<div class="chat_widget">
+    <div id="messages">
+    {% if subject is defined %}
+        <div id="subject">
+            {{- subject -}}
+        </div>
+    {% endif %}
+    {% for msg in messages %}
+        {% include 'chat/message.html' %}
+    {% endfor %}
+    </div>
+    <div class="message_box">
+        <textarea id="message_input" name="message" type="text" placeholder="{{_("enter your message")}}"></textarea>
+    </div>
+</div>
+{% endblock body %}
+
+{% block footer %}{% endblock footer %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/default/chat/message.html	Wed Jan 03 01:12:16 2018 +0100
@@ -0,0 +1,17 @@
+<p id="{{msg.id}}" class="msg_{{msg.type}} {{'own_msg' if msg.from_ == own_jid.full()}}">
+    {%- if msg.type != C.MESS_TYPE_INFO %}
+        {%- set author = identities[msg.from_].nick | default(msg.from_) -%}
+        {%- if identities[msg.from_].avatar_basename is defined %}
+            <img class="avatar" src="{{cache_path}}{{identities[msg.from_].avatar_basename}}">
+        {% else %}
+            <span class="avatar generated">{{author|first|upper}}</span>
+        {%- endif -%}
+        <span class="msg_header">
+            <span class="author">{{author}}</span>
+            <span class="date">{{msg.timestamp|date_fmt('auto_day')}}</span>
+        </span>
+    {% endif -%}
+    <span class="msg_body">
+        {{- msg.html or (msg.text|urlize(nofollow=true, target="_blank")) -}}
+    </span>
+</p>
--- a/default/components/common.html	Wed Jan 03 01:12:16 2018 +0100
+++ b/default/components/common.html	Wed Jan 03 01:12:16 2018 +0100
@@ -6,6 +6,7 @@
     'merge-request_new': _('Create new merge request'),
     'tickets_list': _('Tickets'),
     'ticket_new': _('Create new ticket'),
+    'chat': _('Chat'),
 } %}
 
 {% macro menu(menus, class='') %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/default/static/chat.css	Wed Jan 03 01:12:16 2018 +0100
@@ -0,0 +1,99 @@
+:root {
+  --message_input-height: 2rem;
+}
+
+#main_area {
+    overflow: hidden;
+}
+
+.chat_widget {
+	position: absolute;
+    height: 100%;
+    width: 100%;
+    box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
+}
+
+#messages {
+    height: 100%;
+    min-height: 150px;
+    overflow: auto;
+    box-sizing: border-box;
+    resize: vertical;
+    transition: height 0.6s;
+}
+
+#subject {
+	padding: 1em;
+	text-align: center;
+	background: #eee;
+	font-style: italic;
+}
+
+#messages > p {
+    margin: 0;
+    padding: 0.5em 0 0 0.5em;
+}
+
+#messages .msg_body {
+    white-space: pre-wrap;
+}
+
+#message_input {
+    width: 100%;
+    height: 100%;
+    padding-top: 0.5rem;
+    margin: 0;
+    box-sizing: border-box;
+    resize: none;
+}
+
+.message_box {
+    flex: 1 1 calc(var(--message_input-height));
+    position: relative;
+    min-height: 1rem;
+    box-sizing: border-box;
+}
+
+
+#messages > p.msg_info {
+    white-space: pre-wrap;
+    font-family: monospace;
+    color: #049282;
+}
+
+.avatar {
+	height: 2rem;
+	width: 2rem;
+	float: left;
+	background: #ccc;
+	border-radius: 0.2rem;
+	margin-right: 0.3rem;
+	margin-top: 0.3rem;
+}
+
+.generated {
+    font-size: 1.5rem;
+    text-align: center;
+    background:  #43d2f6;
+}
+
+.msg_header {
+	display: block;
+	font-size: 0.9em;
+}
+
+.author {
+    font-weight: bold;
+}
+
+.date {
+	color: #777;
+}
+
+@media (min-width: 800px) {
+    #messages > p {
+        padding-left: 1.5em;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/default/static/chat.js	Wed Jan 03 01:12:16 2018 +0100
@@ -0,0 +1,60 @@
+/* SàT Template: Chat page handling
+ *
+ * Copyright (C) 2017 Jérôme Poisson (goffi@goffi.org)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+var msgInput = document.getElementById('message_input');
+var messages = document.getElementById('messages');
+const messagesTransitionOri = messages.style.transition;
+
+msgInput.addEventListener('keypress', function(event) {
+    if (event.which == 13 && !event.shiftKey) {
+        if (messages.style.height !== '100%') {
+            messages.style.transition = messagesTransitionOri;
+            setTimeout(function() {
+                messages.style.transition = 'initial';
+                messages.scrollTop = messages.scrollHeight;
+            }, 1000);
+            messages.style.height = "100%";
+        }
+        if (!this.value.trim()) {
+            return;
+        }
+        socket.send({'type': 'msg',
+                     'body': this.value});
+        this.value = '';
+        event.preventDefault();
+    }}
+);
+
+var mutationCb = function(mutationsList) {
+    scrollPos = messages.scrollTop + messages.clientHeight;
+    if (messages.lastChild.offsetTop - scrollPos - 10 <= 0) {
+        // we auto scroll only if we are at the bottom of the page
+        // else the use is probably checking history
+        // Note thas this doesn't take margin into account,
+        // we suppose margin to be 0 for messages children
+        messages.scrollTop = messages.scrollHeight;
+    }
+};
+
+var observer = new MutationObserver(mutationCb);
+
+observer.observe(messages, { childList: true });
+// we want to start with scrolling at bottom
+messages.scrollTop = messages.scrollHeight;
+messages.style.transition = 'initial';