Implement condensed messages in Vue

This commit is contained in:
Pavel Djundik 2018-07-09 15:14:44 +03:00 committed by Pavel Djundik
parent 6116edaa06
commit 9ab5b9d791
9 changed files with 137 additions and 109 deletions

View File

@ -2,7 +2,6 @@
<div <div
:id="'msg-' + message.id" :id="'msg-' + message.id"
:class="['msg', message.type, {self: message.self, highlight: message.highlight}]" :class="['msg', message.type, {self: message.self, highlight: message.highlight}]"
:data-time="message.time"
:data-from="message.from && message.from.nick" :data-from="message.from && message.from.nick"
> >
<span <span

View File

@ -0,0 +1,92 @@
<template>
<div class="msg condensed closed" ref="condensedContainer">
<div class="condensed-summary">
<span class="time"></span>
<span class="from"></span>
<span class="content">
{{condensedText}}
<button class="toggle-button" aria-label="Toggle status messages" @click="onExpandClick"></button>
</span>
</div>
<Message
v-for="message in messages"
:message="message"
:key="message.id"/>
</div>
</template>
<script>
const constants = require("../js/constants");
import Message from "./Message.vue";
export default {
name: "MessageCondensed",
components: {
Message
},
props: {
messages: Array,
},
computed: {
condensedText() {
const obj = {};
constants.condensedTypes.forEach((type) => {
obj[type] = 0;
});
for (const message of this.messages) {
obj[message.type]++;
}
// Count quits as parts in condensed messages to reduce information density
obj.part += obj.quit;
const strings = [];
constants.condensedTypes.forEach((type) => {
if (obj[type]) {
switch (type) {
case "away":
strings.push(obj[type] + (obj[type] > 1 ? " users have gone away" : " user has gone away"));
break;
case "back":
strings.push(obj[type] + (obj[type] > 1 ? " users have come back" : " user has come back"));
break;
case "chghost":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed hostname" : " user has changed hostname"));
break;
case "join":
strings.push(obj[type] + (obj[type] > 1 ? " users have joined" : " user has joined"));
break;
case "part":
strings.push(obj[type] + (obj[type] > 1 ? " users have left" : " user has left"));
break;
case "nick":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick"));
break;
case "kick":
strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked"));
break;
case "mode":
strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set"));
break;
}
}
});
let text = strings.pop();
if (strings.length) {
text = strings.join(", ") + ", and " + text;
}
return text;
},
},
methods: {
onExpandClick() {
this.$refs.condensedContainer.classList.toggle("closed");
},
},
};
</script>

View File

@ -5,7 +5,7 @@
aria-live="polite" aria-live="polite"
aria-relevant="additions" aria-relevant="additions"
> >
<template v-for="(message, id) in channel.messages"> <template v-for="(message, id) in getCondensedMessages">
<div <div
v-if="shouldDisplayDateMarker(id)" v-if="shouldDisplayDateMarker(id)"
:key="message.id + '-date'" :key="message.id + '-date'"
@ -26,7 +26,10 @@
> >
<span class="unread-marker-text"/> <span class="unread-marker-text"/>
</div> </div>
<MessageCondensed v-if="message.type === 'condensed'" :key="message.id" :messages="message.messages"/>
<Message <Message
v-else
:message="message" :message="message"
:key="message.id"/> :key="message.id"/>
</template> </template>
@ -34,16 +37,55 @@
</template> </template>
<script> <script>
const constants = require("../js/constants");
import Message from "./Message.vue"; import Message from "./Message.vue";
import MessageCondensed from "./MessageCondensed.vue";
export default { export default {
name: "MessageList", name: "MessageList",
components: { components: {
Message, Message,
MessageCondensed,
}, },
props: { props: {
channel: Object, channel: Object,
}, },
computed: {
getCondensedMessages() {
if (this.channel.type !== "channel") {
return this.channel.messages;
}
const condensed = [];
let lastCondensedContainer = null;
for (const message of this.channel.messages) {
// If this message is not condensable, or its an action affecting our user,
// then just append the message to container and be done with it
if (message.self || message.highlight || !constants.condensedTypes.includes(message.type)) {
lastCondensedContainer = null;
condensed.push(message);
continue;
}
if (lastCondensedContainer === null) {
lastCondensedContainer = {
type: "condensed",
messages: [],
};
condensed.push(lastCondensedContainer);
}
lastCondensedContainer.id = message.id; // TODO
lastCondensedContainer.messages.push(message);
}
return condensed;
},
},
methods: { methods: {
shouldDisplayDateMarker(id) { shouldDisplayDateMarker(id) {
const previousTime = this.channel.messages[id - 1]; const previousTime = this.channel.messages[id - 1];

View File

@ -1,72 +0,0 @@
"use strict";
const constants = require("./constants");
const templates = require("../views");
module.exports = {
updateText,
getStoredTypes,
};
function getStoredTypes(condensed) {
const obj = {};
constants.condensedTypes.forEach((type) => {
obj[type] = condensed.data(type) || 0;
});
return obj;
}
function updateText(condensed, addedTypes) {
const obj = getStoredTypes(condensed);
Object.keys(addedTypes).map((type) => {
obj[type] += addedTypes[type];
condensed.data(type, obj[type]);
});
// Count quits as parts in condensed messages to reduce information density
obj.part += obj.quit;
const strings = [];
constants.condensedTypes.forEach((type) => {
if (obj[type]) {
switch (type) {
case "away":
strings.push(obj[type] + (obj[type] > 1 ? " users have gone away" : " user has gone away"));
break;
case "back":
strings.push(obj[type] + (obj[type] > 1 ? " users have come back" : " user has come back"));
break;
case "chghost":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed hostname" : " user has changed hostname"));
break;
case "join":
strings.push(obj[type] + (obj[type] > 1 ? " users have joined" : " user has joined"));
break;
case "part":
strings.push(obj[type] + (obj[type] > 1 ? " users have left" : " user has left"));
break;
case "nick":
strings.push(obj[type] + (obj[type] > 1 ? " users have changed nick" : " user has changed nick"));
break;
case "kick":
strings.push(obj[type] + (obj[type] > 1 ? " users were kicked" : " user was kicked"));
break;
case "mode":
strings.push(obj[type] + (obj[type] > 1 ? " modes were set" : " mode was set"));
break;
}
}
});
let text = strings.pop();
if (strings.length) {
text = strings.join(", ") + ", and " + text;
}
condensed.find(".condensed-summary .content")
.html(text + templates.msg_condensed_toggle());
}

View File

@ -119,10 +119,6 @@ window.vueMounted = () => {
}); });
}); });
chat.on("click", ".condensed-summary .content", function() {
$(this).closest(".msg.condensed").toggleClass("closed");
});
const openWindow = function openWindow(e, {keepSidebarOpen, pushState, replaceHistory} = {}) { const openWindow = function openWindow(e, {keepSidebarOpen, pushState, replaceHistory} = {}) {
const self = $(this); const self = $(this);
const target = self.attr("data-target"); const target = self.attr("data-target");

View File

@ -1,12 +1,7 @@
"use strict"; "use strict";
const $ = require("jquery"); const $ = require("jquery");
const templates = require("../views");
const options = require("./options");
const renderPreview = require("./renderPreview");
const utils = require("./utils"); const utils = require("./utils");
const constants = require("./constants");
const condensed = require("./condensed");
const JoinChannel = require("./join-channel"); const JoinChannel = require("./join-channel");
const {vueApp} = require("./vue"); const {vueApp} = require("./vue");
@ -15,6 +10,7 @@ module.exports = {
trimMessageInChannel, trimMessageInChannel,
}; };
/*
function appendMessage(container, chanId, chanType, msg) { function appendMessage(container, chanId, chanType, msg) {
if (utils.lastMessageId < msg.id) { if (utils.lastMessageId < msg.id) {
utils.lastMessageId = msg.id; utils.lastMessageId = msg.id;
@ -109,6 +105,7 @@ function buildChatMessage(msg) {
return renderedMessage; return renderedMessage;
} }
*/
function renderNetworks(data, singleNetwork) { function renderNetworks(data, singleNetwork) {
// Add keyboard handlers to the "Join a channel…" form inputs/button // Add keyboard handlers to the "Join a channel…" form inputs/button

View File

@ -2,7 +2,6 @@
const $ = require("jquery"); const $ = require("jquery");
const socket = require("../socket"); const socket = require("../socket");
const condensed = require("../condensed");
const {vueApp, findChannel} = require("../vue"); const {vueApp, findChannel} = require("../vue");
socket.on("more", function(data) { socket.on("more", function(data) {
@ -31,21 +30,4 @@ socket.on("more", function(data) {
if (data.messages.length !== 100) { if (data.messages.length !== 100) {
scrollable.find(".show-more").removeClass("show"); scrollable.find(".show-more").removeClass("show");
} }
return;
// Join duplicate condensed messages together
const condensedDuplicate = chan.find(".msg.condensed + .msg.condensed");
if (condensedDuplicate) {
const condensedCorrect = condensedDuplicate.prev();
condensed.updateText(condensedCorrect, condensed.getStoredTypes(condensedDuplicate));
condensedCorrect
.append(condensedDuplicate.find(".msg"))
.toggleClass("closed", condensedDuplicate.hasClass("closed"));
condensedDuplicate.remove();
}
}); });

View File

@ -1,7 +0,0 @@
<div class="msg condensed closed" data-time="{{time}}">
<div class="condensed-summary">
<span class="time">{{tz time}}</span>
<span class="from"></span>
<span class="content"></span>
</div>
</div>

View File

@ -1 +0,0 @@
<button class="toggle-button" aria-label="Toggle status messages"></button>