Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 83 additions & 13 deletions pos-module-chat/modules/chat/public/assets/js/pos-chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ window.pos.modules.chat = function(userSettings = {}){
// are there more pages (bool)
module.settings.morePages = true;

// gap (ms) since the previous message after which a timestamp is shown again (number)
module.settings.timeGroupThreshold = 7 * 60 * 1000;

// stores all the conversation list related stuff (object)
module.settings.conversations = {};
// container for the conversations list (dom node)
Expand Down Expand Up @@ -241,6 +244,40 @@ window.pos.modules.chat = function(userSettings = {}){
};


// purpose: decides whether a message's timestamp should be shown, given the
// timestamp of the message right before it in chronological order
// arguments: the current message's date (Date), the previous message's date (Date/null)
// returns: true if there is no previous message or the gap exceeds the threshold (bool)
// ------------------------------------------------------------------------
module.settings.shouldShowTime = (currentDate, previousDate) => {
if(!previousDate){
return true;
}

return (currentDate - previousDate) > module.settings.timeGroupThreshold;
};


// purpose: reads the date of a rendered message from its time element
// arguments: the message's <li> (dom node/null)
// returns: the message's created_at as a Date, or null if unavailable
// ------------------------------------------------------------------------
module.settings.dateOf = (li) => {
const time = li ? li.querySelector(module.settings.messageTemplate.dateSelector) : null;
return (time && time.dateTime) ? new Date(time.dateTime) : null;
};


// purpose: fills in a message's time element, hiding the text (but keeping
// datetime, used for ordering/gap comparisons) when it shouldn't show
// arguments: the time element (dom node), the message's date (Date), whether to show it (bool)
// ------------------------------------------------------------------------
module.settings.setMessageTime = (timeEl, date, show) => {
timeEl.dateTime = date.toISOString();
timeEl.textContent = show ? module.settings.timezonedDate(date) : '';
};


// purpose: scrolls the chat window to the bottom
// arguments: scroll behavior - 'auto' (instant) or 'smooth' (string, default: 'auto')
// ------------------------------------------------------------------------
Expand Down Expand Up @@ -371,26 +408,40 @@ window.pos.modules.chat = function(userSettings = {}){
// clone message template
const messageHtml = messageData.status === 'received' ? module.settings.messageTemplate.received.content.cloneNode(true) : module.settings.messageTemplate.sent.content.cloneNode(true);
// fill template with data
messageHtml.querySelector(module.settings.messageTemplate.dateSelector).textContent = module.settings.timezonedDate(new Date(messageData.created_at));
messageHtml.querySelector(module.settings.messageTemplate.dateSelector).dateTime = messageData.created_at;
messageHtml.querySelector(module.settings.messageTemplate.messageSelector).innerHTML = encodeHtml(messageData.message).replace(/(\r\n|\r|\n)/g, '<br>');

// Insert in chronological order (by created_at) rather than by arrival order, so a
// burst of messages renders correctly even if channel delivery arrives out of order.
// Falls back to appending at the end (the common case: the newest message).
const messageDate = new Date(messageData.created_at);
let appendedAtEnd = true;
let insertBeforeLi = null;
for(const li of module.settings.messagesList.querySelectorAll(':scope > li')){
const time = li.querySelector(module.settings.messageTemplate.dateSelector);
const liDate = (time && time.dateTime) ? new Date(time.dateTime) : null;
const liDate = module.settings.dateOf(li);
if(liDate && liDate > messageDate){
module.settings.messagesList.insertBefore(messageHtml, li);
appendedAtEnd = false;
insertBeforeLi = li;
break;
}
}

if(appendedAtEnd){
// the timestamp is only shown when it's been more than 7 minutes since the message
// that precedes this one chronologically (not necessarily the previous sibling in the DOM,
// though in the common append-at-the-end case it is the same thing)
const previousLi = insertBeforeLi ? insertBeforeLi.previousElementSibling : module.settings.messagesList.lastElementChild;
const timeEl = messageHtml.querySelector(module.settings.messageTemplate.dateSelector);
module.settings.setMessageTime(timeEl, messageDate, module.settings.shouldShowTime(messageDate, module.settings.dateOf(previousLi)));

if(insertBeforeLi){
module.settings.messagesList.insertBefore(messageHtml, insertBeforeLi);

// this message now sits between the old previous message and insertBeforeLi, so
// insertBeforeLi's own timestamp visibility (based on the gap to its predecessor) may
// need to be rechecked - the gap to its new, closer predecessor can only have shrunk
const nextTimeEl = insertBeforeLi.querySelector(module.settings.messageTemplate.dateSelector);
const nextDate = module.settings.dateOf(insertBeforeLi);
if(nextDate){
module.settings.setMessageTime(nextTimeEl, nextDate, module.settings.shouldShowTime(nextDate, messageDate));
}
} else {
// append the message to the chat
module.settings.messagesList.append(messageHtml);
}
Expand Down Expand Up @@ -426,20 +477,32 @@ window.pos.modules.chat = function(userSettings = {}){
.then((data) => {
// construct HTML elements for messages
let html = document.createDocumentFragment();
// tracks the chronologically previous message within this (older) batch, oldest first -
// the very first message of a batch has no known predecessor yet, so it always shows its time
let previousDate = null;

Object.entries(data.results).reverse().forEach(([key, messageData]) => {
messageData = Object.assign(messageData, { status: (module.settings.currentUserId == messageData.autor_id) ? 'sent' : 'received'});

// clone message template
const messageHtml = messageData.status === 'received' ? module.settings.messageTemplate.received.content.cloneNode(true) : module.settings.messageTemplate.sent.content.cloneNode(true);
const messageDate = new Date(messageData.created_at);
// fill template with data
messageHtml.querySelector(module.settings.messageTemplate.dateSelector).textContent = module.settings.timezonedDate(new Date(messageData.created_at));
messageHtml.querySelector(module.settings.messageTemplate.dateSelector).dateTime = messageData.created_at;
module.settings.setMessageTime(messageHtml.querySelector(module.settings.messageTemplate.dateSelector), messageDate, module.settings.shouldShowTime(messageDate, previousDate));
messageHtml.querySelector(module.settings.messageTemplate.messageSelector).innerHTML = encodeHtml(messageData.message).replace(/(\r\n|\r|\n)/g, '<br>');

html.append(messageHtml);

previousDate = messageDate;
});

// the message that used to be the oldest one visible now follows the newest message of
// this freshly-loaded (older) batch instead of having no predecessor - recheck its timestamp
const oldFirstLi = module.settings.messagesList.firstElementChild;
const oldFirstDate = module.settings.dateOf(oldFirstLi);
if(oldFirstDate && previousDate){
module.settings.setMessageTime(oldFirstLi.querySelector(module.settings.messageTemplate.dateSelector), oldFirstDate, module.settings.shouldShowTime(oldFirstDate, previousDate));
}

// put the messages on top
module.settings.messagesList.prepend(html);
Expand Down Expand Up @@ -484,9 +547,16 @@ window.pos.modules.chat = function(userSettings = {}){
// purpose: parses the dates outputted from BE with JS so that everyting uses browser locale
// ------------------------------------------------------------------------
module.parseDates = () => {
document.querySelectorAll('.pos-chat-message time').forEach(date => {
let currentDate = new Date(date.dateTime);
date.innerText = module.settings.timezonedDate(currentDate);
document.querySelectorAll('.pos-chat-message time').forEach(time => {
// the back-end already decided whether this timestamp should be visible (grouping
// messages less than 7 minutes apart) - an empty one means it decided to hide it,
// so leave it alone instead of reinstating it here
if(!time.dateTime || !time.textContent.trim()){
return;
}

let currentDate = new Date(time.dateTime);
time.innerText = module.settings.timezonedDate(currentDate);
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,19 @@
align-items: end;
}

.pos-chat-message:not(:first-child) time {
margin-block-start: .5em;
}

.pos-chat-message time {
font-size: .9rem;
color: var(--pos-color-content-text-supplementary);
}

.pos-chat-message time:empty {
display: none;
}

.pos-chat-message-content {
max-width: 100%;
padding: calc(var(--pos-padding-cell) / 2) calc(var(--pos-padding-cell) / 1.5);
Expand Down
19 changes: 18 additions & 1 deletion pos-module-chat/modules/chat/public/views/partials/inbox.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,31 @@
<ul class="pos-chat-messages" id="chat-messagesList">
{% liquid
assign list = current_conversation.messages | reverse
# 7 minutes, in seconds - a message only repeats the timestamp once the gap
# since the previous message grows past this
assign time_group_threshold = 420
assign previous_created_at = null

for message in list
if message.autor_id == current_profile.id
assign authored = true
else
assign authored = false
endif

render 'modules/chat/message', message: message, authored: authored, timezone: current_profile.properties.timezone
assign show_time = true
if previous_created_at
assign current_epoch = message.created_at | to_time | date: '%s' | plus: 0
assign previous_epoch = previous_created_at | to_time | date: '%s' | plus: 0
assign gap_seconds = current_epoch | minus: previous_epoch

if gap_seconds < time_group_threshold
assign show_time = false
endif
endif
assign previous_created_at = message.created_at

render 'modules/chat/message', message: message, authored: authored, timezone: current_profile.properties.timezone, show_time: show_time
endfor
%}
</ul>
Expand Down
21 changes: 16 additions & 5 deletions pos-module-chat/modules/chat/public/views/partials/message.liquid
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
{% doc %}
@param {boolean} authored - if current message has been authored by the current user
@param {boolean} [show_time] - if the timestamp should be visible or not
@param {object} message - text message to be shown
@param {string} timezone - current user timezone
{% enddoc %}


{% liquid
assign msg = message.message | markdown: '{ "elements": [ "br" ] }'

# show_time defaults to true - callers only pass false when this message sits
# within 7 minutes of the previous one and the timestamp would be redundant
assign display_time = true
if show_time == false
assign display_time = false
endif
%}

<li class="pos-chat-message {% if authored %} pos-chat-message-authored {% endif %}">

<time datetime="{{ message.created_at }}">
{% if message.created_at %}
{{ message.created_at | l: 'long', timezone }}
{% endif %}
</time>
<time datetime="{{ message.created_at }}">{%- if display_time and message.created_at -%}{{ message.created_at | l: 'long', timezone }}{%- endif -%}</time>

<div class="pos-chat-message-content">
{{ msg }}
Expand Down
2 changes: 1 addition & 1 deletion pos-module-chat/pos-module.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"machine_name": "chat",
"name": "Pos Module Chat",
"version": "2.1.8",
"version": "2.1.9",
"dependencies": {
"core": "^2.1.9",
"user": "^5.3.0",
Expand Down
Loading