diff --git a/app/controllers/chat.js b/app/controllers/chat.js
index c39ff86..6b9e4cc 100644
--- a/app/controllers/chat.js
+++ b/app/controllers/chat.js
@@ -50,6 +50,12 @@ export default class ChatController extends SiteController {
this.getRoomCreateView.bind(this),
);
+ router.get(
+ '/room/:roomId/join',
+ // limiterService.create(limiterService.config.chat.getRoomView),
+ this.getRoomJoin.bind(this),
+ );
+
router.get(
'/room/:roomId',
// limiterService.create(limiterService.config.chat.getRoomView),
@@ -86,6 +92,17 @@ export default class ChatController extends SiteController {
res.render('chat/room/create');
}
+ async getRoomJoin (req, res, next) {
+ const { chat: chatService } = this.dtp.services;
+ try {
+ await chatService.joinRoom(res.locals.room, req.user);
+ res.status(200).json({ success: true, room: res.locals.room });
+ } catch (error) {
+ this.log.error('failed to join chat room', { error });
+ return next(error);
+ }
+ }
+
async getRoomView (req, res) {
res.locals.currentView = 'chat-room';
res.render('chat/room/view');
diff --git a/app/models/chat-room.js b/app/models/chat-room.js
index 475f1ea..88b8fcf 100644
--- a/app/models/chat-room.js
+++ b/app/models/chat-room.js
@@ -10,7 +10,7 @@ import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const ChatRoomSchema = new Schema({
- created: { type: Date, default: Date.now, required: true, index: 1, expires: '7d' },
+ created: { type: Date, default: Date.now, required: true, index: 1 },
lastActivity: { type: Date, index: -1 },
owner: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
name: { type: String, required: true },
@@ -18,7 +18,12 @@ const ChatRoomSchema = new Schema({
capacity: { type: Number, required: true, min: MIN_ROOM_CAPACITY, max: MAX_ROOM_CAPACITY },
invites: { type: [Schema.ObjectId], select: false },
members: { type: [Schema.ObjectId], select: false },
+ present: { type: [Schema.ObjectId], select: false },
banned: { type: [Schema.ObjectId], select: false },
+ stats: {
+ memberCount: { type: Number, default: 0, min: 0, max: MAX_ROOM_CAPACITY, required: true },
+ presentCount: { type: Number, default: 0, min: 0, max: MAX_ROOM_CAPACITY, required: true },
+ },
});
export default mongoose.model('ChatRoom', ChatRoomSchema);
\ No newline at end of file
diff --git a/app/services/chat.js b/app/services/chat.js
index 18b1077..1d47152 100644
--- a/app/services/chat.js
+++ b/app/services/chat.js
@@ -10,6 +10,8 @@ const ChatRoom = mongoose.model('ChatRoom');
const ChatMessage = mongoose.model('ChatMessage');
// const ChatRoomInvite = mongoose.model('ChatRoomInvite');
+import numeral from 'numeral';
+
import { SiteService, SiteError } from '../../lib/site-lib.js';
import { MAX_ROOM_CAPACITY } from '../models/lib/constants.js';
@@ -24,6 +26,11 @@ export default class ChatService extends SiteService {
async start ( ) {
const { user: userService } = this.dtp.services;
+
+ this.templates = {
+ memberListItem: this.loadViewTemplate('chat/components/member-list-item-standalone.pug'),
+ };
+
this.populateChatRoom = [
{
path: 'owner',
@@ -45,6 +52,7 @@ export default class ChatService extends SiteService {
}
room.capacity = MAX_ROOM_CAPACITY;
room.members = [owner._id];
+ room.stats.memberCount = 1;
await room.save();
@@ -60,12 +68,141 @@ export default class ChatService extends SiteService {
}
async joinRoom (room, user) {
- await ChatRoom.updateOne(
+ const roomData = await ChatRoom.findOne({ _id: room._id, banned: user._id }).lean();
+ if (roomData) {
+ throw new SiteError(401, 'You are banned from this chat room');
+ }
+
+ const response = await ChatRoom.updateOne(
{ _id: room._id },
{
$push: { members: user._id },
},
);
+
+ this.log.debug('joinRoom complete', { response });
+ return response;
+ }
+
+ async chatRoomCheckIn (room, member) {
+ const NOW = new Date();
+
+ // indicate presence in the chat room's Mongo document
+ const roomData = await ChatRoom.findOneAndUpdate(
+ { _id: room._id },
+ {
+ $addToSet: { present: member._id },
+ $inc: { 'stats.presentCount': 1 },
+ },
+ {
+ new: true,
+ },
+ );
+
+ this.log.debug('member checking into chat room', {
+ room: {
+ _id: room._id,
+ name: room.name,
+ presentCount: roomData.stats.presentCount,
+ },
+ member: {
+ _id: member._id,
+ username: member.username,
+ },
+ });
+
+ /*
+ * Broadcast a control message to all room members that a new member has
+ * joined the room.
+ */
+ const displayList = this.createDisplayList('chat-control');
+ displayList.removeElement(`ul#present-members li[data-member-id="${member._id}"]`);
+ displayList.addElement(
+ `ul#chat-active-members[data-room-id="${room._id}"]`,
+ 'afterBegin',
+ this.templates.memberListItem({ room, member }),
+ );
+
+ displayList.setTextContent(
+ `.chat-present-count`,
+ numeral(roomData.stats.presentCount).format('0,0'),
+ );
+
+ const systemMessage = {
+ created: NOW.toISOString(),
+ content: `@${member.username} has connected to the room.`,
+ };
+
+ this.dtp.emitter.to(room._id.toString()).emit('chat-control', { displayList, systemMessages: [systemMessage] });
+
+ }
+
+ async chatRoomCheckOut (room, member) {
+ const NOW = new Date();
+ const roomData = await ChatRoom.findOneAndUpdate(
+ { _id: room._id },
+ {
+ $pull: { present: member._id },
+ $inc: { 'stats.presentCount': -1 },
+ },
+ {
+ new: true,
+ },
+ );
+
+ this.log.debug('member checking out of chat room', {
+ room: {
+ _id: room._id,
+ name: room.name,
+ presentCount: roomData.stats.presentCount,
+ },
+ member: {
+ _id: member._id,
+ username: member.username,
+ },
+ });
+
+ /*
+ * Broadcast a control message to all room members that a new member has
+ * joined the room.
+ */
+ const displayList = this.createDisplayList('chat-control');
+ displayList.removeElement(`ul#chat-active-members li[data-member-id="${member._id}"]`);
+
+ displayList.setTextContent(
+ `.chat-present-count`,
+ numeral(roomData.stats.presentCount).format('0,0'),
+ );
+
+ const systemMessage = {
+ created: NOW.toISOString(),
+ content: `@${member.username} has connected to the room.`,
+ };
+
+ this.dtp.emitter.to(room._id.toString()).emit('chat-control', { displayList, systemMessages: [systemMessage] });
+ }
+
+ async checkRoomMember (room, member) {
+ if (room.owner._id.equals(member._id)) {
+ return true;
+ }
+
+ const search = { _id: room._id, members: member._id };
+ const checkRoom = await ChatRoom.findOne(search).select('name').lean();
+ if (!checkRoom) {
+ throw new SiteError(403, `You are not a member of ${checkRoom.name}`);
+ }
+
+ return true;
+ }
+
+ async isRoomMember (room, member) {
+ if (room.owner._id.equals(member._id)) {
+ return true;
+ }
+ const search = { _id: room._id, members: member._id };
+ const checkRoom = await ChatRoom.findOne(search).select('name').lean();
+ return !!checkRoom;
}
async leaveRoom (room, user) {
diff --git a/app/views/chat/components/member-list-item-standalone.pug b/app/views/chat/components/member-list-item-standalone.pug
new file mode 100644
index 0000000..5562967
--- /dev/null
+++ b/app/views/chat/components/member-list-item-standalone.pug
@@ -0,0 +1,2 @@
+include member-list-item
++renderChatMemberListItem (room, member, { isHost, isGuest })
\ No newline at end of file
diff --git a/app/views/chat/components/member-list-item.pug b/app/views/chat/components/member-list-item.pug
new file mode 100644
index 0000000..6f353a6
--- /dev/null
+++ b/app/views/chat/components/member-list-item.pug
@@ -0,0 +1,99 @@
+include ../../user/components/profile-picture
+mixin renderChatMemberListItem (room, member, options)
+ -
+ options = Object.assign({
+ isHost: false,
+ isGuest: false,
+ }, options);
+
+ var isRoomOwner = room.owner._id.equals(member._id);
+ var isSystemMod = user && user.flags && (user.flags.isAdmin || user.flags.isModerator);
+ var memberName = member.displayName || member.username;
+ // var isChannelMod = user && Array.isArray(message.channel.moderators) && !!message.channel.moderators.find((moderator) => moderator._id.equals(user._id));
+
+ li(data-member-id= member._id, data-member-username= member.username)
+ div(uk-grid).uk-grid-collapse.uk-flex-middle.member-list-item
+ .uk-width-auto
+ img(src='/img/default-member.png').member-profile-icon
+ .uk-width-expand.uk-text-small
+ a(href=`/member/${member.username}`, uk-tooltip={ title: `Visit ${member.username}`}).uk-link-reset
+ .member-display-name= member.displayName || member.username
+ .member-username @#{member.username}
+ .uk-width-auto.chat-user-menu
+ button(type="button").dtp-button-dropdown
+ i.fas.fa-ellipsis-v
+ div(
+ data-room-id= room._id,
+ uk-dropdown={ animation: "uk-animation-scale-up", duration: 250 },
+ data-mode="click",
+ ).dtp-chatmsg-menu
+ ul.uk-nav.uk-dropdown-nav
+ li.uk-nav-header= member.username
+ li
+ a(
+ href="",
+ data-username= member.username,
+ onclick="return dtp.app.mentionChatUser(event);",
+ ) Mention
+ li
+ a(
+ href="",
+ data-room-id= room._id,
+ data-room-name= room.name,
+ data-user-id= member._id,
+ data-username= member.username,
+ onclick="return dtp.app.muteChatUser(event);",
+ ) Mute
+
+ if (options.isHost || options.isGuest) && !isRoomOwner
+ li.uk-nav-divider
+ if options.isHost
+ li
+ a(
+ href,
+ data-environment="ChatRoom",
+ data-room-id= room._id,
+ data-room-name= room.name,
+ data-user-id= member._id,
+ data-username= member.username,
+ data-display-name= member.displayName,
+ onclick="return dtp.app.removeRoomHost(event);",
+ ) Remove host
+ if options.isGuest
+ li
+ a(
+ href,
+ data-environment="ChatRoom",
+ data-room-id= room._id,
+ data-room-name= room.name,
+ data-user-id= member._id,
+ data-username= member.username,
+ data-display-name= member.displayName,
+ onclick="return dtp.app.removeRoomGuest(event);",
+ ) Remove guest
+
+ if isSystemMod || isChannelMod
+ li.uk-nav-divider
+ li
+ a(
+ href="",
+ data-environment="ChatRoom",
+ data-room-id= room._id,
+ data-room-name= room.name,
+ data-user-id= member._id,
+ data-username= member.username,
+ data-display-name= member.displayName,
+ onclick="return dtp.app.confirmBanUserFromEnvironment(event);",
+ ) Ban from room
+
+ if isSystemMod
+ li
+ a(
+ href="",
+ data-room-id= room._id,
+ data-room-name= room.name,
+ data-user-id= member._id,
+ data-username= member.username,
+ data-display-name= member.displayName,
+ onclick="return dtp.adminApp.confirmBanUser(event);",
+ ) Ban from #{site.name}
\ No newline at end of file
diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug
index a853031..68fb840 100644
--- a/app/views/chat/room/view.pug
+++ b/app/views/chat/room/view.pug
@@ -27,7 +27,7 @@ block view-content
mixin renderLiveMember (member)
div(data-user-id= member._id, data-username= member.username).stage-live-member
- video(src="/static/video/gdl-crush.mp4", autoplay, muted, loop, disablepictureinpicture, disableremoteplayback)
+ video(poster="/img/default-poster.png", disablepictureinpicture, disableremoteplayback)
.uk-flex.live-meta.no-select
.live-username.uk-width-expand
.uk-text-truncate= member.displayName || member.username
@@ -38,30 +38,20 @@ block view-content
i.fas.fa-cog
.dtp-chat-stage
- .chat-sidebar
- .chat-stage-header Active Members
+ #room-member-panel.chat-sidebar
+ .chat-stage-header
+ div(uk-grid).uk-grid-small.uk-grid-middle
+ .uk-width-expand
+ .uk-text-truncate Active Members
+ .uk-width-auto
+ .chat-present-count.uk-text-small ---
.sidebar-panel
- ul(id="chat-active-members").uk-list.uk-list-collapse
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
- +renderMemberListEntry(user)
+ ul(id="chat-active-members", data-room-id= room._id).uk-list.uk-list-collapse
.chat-stage-header Idle Members
.sidebar-panel
- ul(id="chat-idle-members").uk-list.uk-list-collapse
- +renderMemberListEntry(user, { idle: true })
- +renderMemberListEntry(user, { idle: true })
- +renderMemberListEntry(user, { idle: true })
- +renderMemberListEntry(user, { idle: true })
+ .uk-text-italic.uk-text-muted There are no idle members.
+ ul(id="chat-idle-members", data-room-id= room._id, hidden).uk-list.uk-list-collapse
.chat-container
.chat-stage-header
@@ -69,32 +59,40 @@ block view-content
.uk-width-expand= room.name
.chat-content-panel
- div(uk-grid).uk-grid-collapse.live-content
- .uk-width-expand
- .chat-media
- div(uk-grid)
- div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@l uk-width-1-4@xl")
- +renderLiveMember(user)
+ .live-content
+ .chat-media
+ div(uk-grid)
+ div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@l uk-width-1-4@xl")
+ +renderLiveMember(user)
- .uk-width-auto
- .chat-messages
- -
- var testMessage = {
- created: new Date(),
- author: user,
- content: "This is the chat content panel. It should word-wrap and scroll correctly, and will be where individual chat messages will render as they arrive and are sent.",
- };
+ div(id="chat-message-list").chat-messages
+ -
+ var testMessage = {
+ created: new Date(),
+ author: user,
+ content: "This is the chat content panel. It should word-wrap and scroll correctly, and will be where individual chat messages will render as they arrive and are sent.",
+ };
+
+ +renderChatMessage(testMessage)
+ +renderChatMessage(testMessage)
+ +renderChatMessage(testMessage)
- +renderChatMessage(testMessage)
- +renderChatMessage(testMessage)
- +renderChatMessage(testMessage)
- +renderChatMessage(testMessage)
+ .chat-input-panel
+ form(
+ id="chat-input-form",
+ data-room-id= room._id,
+ onsubmit="return window.dtp.app.sendUserChat(event);",
+ hidden= user && user.flags && user.flags.isCloaked,
+ ).uk-form
+ textarea(id="chat-input-text", name="chatInput", rows=2).uk-textarea.uk-resize-none.uk-border-rounded
+ .uk-margin-small
+ .uk-flex
+ .uk-width-expand
+ .uk-width-auto
+ button(id="chat-send-btn", type="submit", uk-tooltip={ title: 'Send message' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded
+ i.fas.fa-paper-plane
- .chat-input-panel
- textarea(id="chat-input", name="chatInput", rows=2).uk-textarea.uk-resize-none.uk-border-rounded
- .uk-margin-small
- .uk-flex
- .uk-width-expand
- .uk-width-auto
- button(type="submit", uk-tooltip={ title: 'Send message' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded
- i.fas.fa-paper-plane
\ No newline at end of file
+block viewjs
+ script.
+ window.dtp = window.dtp || { };
+ window.dtp.room = !{JSON.stringify(room)};
\ No newline at end of file
diff --git a/client/css/dtp-site.less b/client/css/dtp-site.less
index 780ef8c..f70619b 100644
--- a/client/css/dtp-site.less
+++ b/client/css/dtp-site.less
@@ -1,4 +1,5 @@
@import "site/main.less";
+@import "site/button.less";
@import "site/navbar.less";
@import "site/stage.less";
@import "site/image.less";
\ No newline at end of file
diff --git a/client/css/site/button.less b/client/css/site/button.less
index e69de29..13fad7a 100644
--- a/client/css/site/button.less
+++ b/client/css/site/button.less
@@ -0,0 +1,23 @@
+button.dtp-button-dropdown {
+ /*
+ * This is a series of settings that improve the "clickable area" of the
+ * button without making it take up more space in the layout.
+ */
+ display: inline-block;
+ position: relative;
+ padding: 1em;
+ margin: -1em;
+
+ &.always-on-top {
+ z-index: 1;
+ }
+
+ /*
+ * The actual "style" of the thing
+ */
+ background: none;
+ border: none;
+ outline: none;
+ color: #e8e8e8;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/client/css/site/stage.less b/client/css/site/stage.less
index d276163..db6afed 100644
--- a/client/css/site/stage.less
+++ b/client/css/site/stage.less
@@ -7,11 +7,6 @@
display: flex;
- .flex-row-break {
- flex-basis: 100%;
- height: 0;
- }
-
.chat-stage-header {
flex-grow: 0;
flex-shrink: 0;
@@ -42,37 +37,52 @@
}
}
+ img.member-profile-icon {
+ width: 32px;
+ height: auto;
+ border-radius: 5px;
+ margin-right: 5px;
+ }
+
+ .member-list-item {
+ line-height: 1.1em;
+
+ .member-display-name {
+ line-height: 1.1em;
+ }
+ .member-username {
+ line-height: 1em;
+ }
+ }
+
.chat-container {
box-sizing: border-box;
display: flex;
+ flex-grow: 1;
flex-direction: column;
- align-items: stretch;
background-color: #a8a8a8;
color: #1a1a1a;
.chat-content-panel {
box-sizing: border-box;
- position: relative;
display: flex;
- flex-wrap: wrap;
-
- height: 100%;
+ flex: 1;
.live-content {
- flex-grow: 1;
+ display: flex;
+ flex: 1;
.chat-media {
box-sizing: border-box;
- flex-basis: 0;
flex-grow: 1;
- height: 100%;
padding: @stage-panel-padding;
background-color: #4a4a4a;
+ overflow-y: auto;
.stage-live-member {
padding: 4px 4px 0 4px;
@@ -84,6 +94,11 @@
video {
display: block;
+
+ aspect-ratio: 16 / 9;
+ width: 100%;
+ height: auto;
+
border-radius: 3px;
}
@@ -104,26 +119,23 @@
flex-shrink: 0;
width: 320px;
- height: 100%;
padding: @stage-panel-padding;
overflow-y: scroll;
.chat-message {
- line-height: 1;
-
+ padding: 5px;
margin-bottom: 10px;
- border-bottom: solid 1px #4a4a4a;
-
- &:last-child {
- border-bottom: none;
- }
+
+ line-height: 1;
+ border-radius: 4px;
+
+ background-color: #e8e8e8;
+ color: #1a1a1a;
- img.member-profile-icon {
- width: 32px;
- height: auto;
- border-radius: 5px;
- margin-right: 5px;
+ &.system-message {
+ background-color: #d1f3db;
+ border-radius: 4px;
}
.message-attribution {
@@ -154,15 +166,15 @@
}
}
- .chat-input-panel {
- flex-basis: 100%;
- flex-grow: 0;
- flex-shrink: 0;
-
- padding: @stage-panel-padding;
- background-color: #1a1a1a;
- color: #e8e8e8;
- }
+ }
+
+ .chat-input-panel {
+ flex-grow: 0;
+ flex-shrink: 0;
+
+ padding: @stage-panel-padding;
+ background-color: #1a1a1a;
+ color: #e8e8e8;
}
}
diff --git a/client/img/default-poster.png b/client/img/default-poster.png
new file mode 100644
index 0000000..f7dc2cf
Binary files /dev/null and b/client/img/default-poster.png differ
diff --git a/client/js/chat-client.js b/client/js/chat-client.js
index 01ff773..bbc1439 100644
--- a/client/js/chat-client.js
+++ b/client/js/chat-client.js
@@ -5,13 +5,15 @@
'use strict';
const DTP_COMPONENT_NAME = 'DtpChatApp';
-window.dtp = window.dtp || { };
+const dtp = window.dtp = window.dtp || { };
import DtpApp from 'lib/dtp-app.js';
import QRCode from 'qrcode';
import Cropper from 'cropperjs';
+import dayjs from 'dayjs';
+
export class ChatApp extends DtpApp {
constructor (user) {
@@ -19,12 +21,134 @@ export class ChatApp extends DtpApp {
this.loadSettings();
this.log.info('DTP app client online');
+ this.chat = {
+ form: document.querySelector('#chat-input-form'),
+ messageList: document.querySelector('#chat-message-list'),
+ messages: [ ],
+ messageMenu: document.querySelector('.chat-message-menu'),
+ input: document.querySelector('#chat-input-text'),
+ sendButton: document.querySelector('#chat-send-btn'),
+ isAtBottom: true,
+ };
+
window.addEventListener('dtp-load', this.onDtpLoad.bind(this));
+ window.addEventListener('unload', this.onDtpUnload.bind(this));
}
async onDtpLoad ( ) {
this.log.info('dtp-load event received. Connecting to platform.');
- await this.connect();
+ await this.connect({
+ mode: 'User',
+ onSocketConnect: this.onChatSocketConnect.bind(this),
+ onSocketDisconnect: this.onChatSocketDisconnect.bind(this),
+ });
+ }
+
+ async onDtpUnload ( ) {
+ await this.socket.disconnect();
+ }
+
+ async onChatSocketConnect (socket) {
+ this.log.debug('onSocketConnect', 'attaching socket events');
+ socket.on('chat-control', this.onChatControl.bind(this));
+ socket.on('system-message', this.onSystemMessage.bind(this));
+
+ if (dtp.room) {
+ await this.joinChatChannel(dtp.room);
+ }
+ }
+
+ async onChatSocketDisconnect (socket) {
+ this.log.debug('onSocketDisconnect', 'detaching socket events');
+ socket.off('chat-control', this.onChatControl.bind(this));
+ socket.off('system-message', this.onSystemMessage.bind(this));
+ }
+
+ async onChatControl (message) {
+ const isAtBottom = this.chat.isAtBottom;
+
+ if (message.displayList) {
+ this.displayEngine.executeDisplayList(message.displayList);
+ }
+
+ if (Array.isArray(message.systemMessages) && (message.systemMessages.length > 0)) {
+ for await (const sm of message.systemMessages) {
+ await this.onSystemMessage(sm);
+ }
+ }
+
+ if (message.cmd) {
+ switch (message.cmd) {
+ case 'call-start':
+ if (message.mediaServer && !this.call) {
+ dtp.mediaServer = message.mediaServer;
+ setTimeout(this.joinWebCall.bind(this), Math.floor(Math.random() * 3000));
+ }
+ break;
+
+ case 'call-end':
+ if (this.chat) {
+ this.chat.closeCall();
+ }
+ break;
+ }
+ }
+
+ this.scrollChatToBottom(isAtBottom);
+ }
+
+ async onSystemMessage (message) {
+ if (message.displayList) {
+ this.displayEngine.executeDisplayList(message.displayList);
+ }
+
+ if (!message.created || !message.content) {
+ return;
+ }
+
+ if (!this.chat || !this.chat.messageList) {
+ return;
+ }
+
+ const systemMessage = document.createElement('div');
+ systemMessage.classList.add('chat-message');
+ systemMessage.classList.add('system-message');
+
+ const chatContent = document.createElement('div');
+ chatContent.classList.add('chat-content');
+ chatContent.classList.add('uk-text-break');
+ chatContent.innerHTML = message.content;
+ systemMessage.appendChild(chatContent);
+
+ const chatTimestamp = document.createElement('div');
+ chatTimestamp.classList.add('chat-timestamp');
+ chatTimestamp.classList.add('uk-text-small');
+ chatTimestamp.setAttribute('data-dtp-timestamp', message.created);
+ chatTimestamp.innerHTML = dayjs(message.created).format('hh:mm:ss a');
+ systemMessage.appendChild(chatTimestamp);
+
+ this.chat.messageList.appendChild(systemMessage);
+ this.chat.messages.push(systemMessage);
+
+ while (this.chat.messages.length > 50) {
+ const message = this.chat.messages.shift();
+ this.chat.messageList.removeChild(message);
+ }
+ if (this.chat.isAtBottom) {
+ this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
+ }
+ }
+
+ async joinChatChannel (room) {
+ try {
+ const response = await fetch(`/chat/room/${dtp.room._id}/join`);
+ await this.processResponse(response);
+
+ await this.socket.joinChannel(dtp.room._id, 'ChatRoom');
+ } catch (error) {
+ this.log.error('failed to join chat room', { room, error });
+ UIkit.modal.alert(`Failed to join chat room: ${error.message}`);
+ }
}
async confirmNavigation (event) {
@@ -295,4 +419,47 @@ export class ChatApp extends DtpApp {
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, options);
}
+
+ scrollChatToBottom (isAtBottom = true) {
+ if (this.chat && this.chat.messageList && isAtBottom) {
+ this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight);
+ setTimeout(( ) => {
+ this.chat.isAtBottom = true;
+ this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight);
+ this.chat.isModifying = false;
+ }, 25);
+ }
+ }
+
+ async onChatMessageListScroll (event) {
+ const scrollPos = this.chat.messageList.scrollTop + this.chat.messageList.clientHeight;
+
+ if (!this.chat.isModifying) {
+ this.chat.isAtBottom = (scrollPos >= (this.chat.messageList.scrollHeight - 10));
+ this.chat.isAtTop = (scrollPos <= 0);
+ }
+
+ if (event && (this.chat.isAtBottom || this.chat.isAtTop)) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if (this.chat.isAtBottom) {
+ this.chat.messageMenu.classList.remove('chat-menu-visible');
+ } else {
+ this.chat.messageMenu.classList.add('chat-menu-visible');
+ }
+ }
+
+ async resumeChatScroll ( ) {
+ this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
+ this.chat.isAtBottom = true;
+ this.chat.messageMenu.classList.remove('chat-menu-visible');
+ }
+
+ async onWindowResize ( ) {
+ if (this.chat.messageList && this.chat.isAtBottom) {
+ this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000);
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/client/js/dtp-log.js b/lib/client/js/dtp-log.js
index 58f6d85..4e51b74 100644
--- a/lib/client/js/dtp-log.js
+++ b/lib/client/js/dtp-log.js
@@ -75,7 +75,7 @@ export default class DtpWebLog {
};
const env = document.querySelector('body').getAttribute('data-dtp-env');
- if (env === 'local') {
+ if (env === 'development') {
this.enable();
}
}
diff --git a/lib/client/js/dtp-socket.js b/lib/client/js/dtp-socket.js
index cceebc5..7868e3d 100644
--- a/lib/client/js/dtp-socket.js
+++ b/lib/client/js/dtp-socket.js
@@ -89,6 +89,12 @@ export default class DtpWebSocket {
}
}
+ disconnect ( ) {
+ // remove disconnect handler since we're being deliberate
+ this.socket.off('disconnect', this.onSocketDisconnect.bind(this));
+ this.socket.disconnect();
+ }
+
async onSocketConnect ( ) {
this.log.info('onSocketConnect', 'WebSocket connected');
this.isConnected = true;
@@ -164,7 +170,8 @@ export default class DtpWebSocket {
}
this.joinChannel(message.user._id, 'User');
- document.dispatchEvent(new Event('socketConnected'));
+ this.log.info('onSocketUserAuthenticated', 'dispatching dtp-socket-connected');
+ document.dispatchEvent(new Event('dtp-socket-connected'));
}
async onSocketWidgetAuthenticated (message) {
@@ -175,8 +182,7 @@ export default class DtpWebSocket {
this.options.onSocketConnect(this.socket);
}
- // this.joinChannel(message.channel._id, 'Channel');
- document.dispatchEvent(new Event('socketConnected'));
+ document.dispatchEvent(new Event('dtp-socket-connected'));
}
async joinChannel (channelId, channelType, passcode) {
@@ -195,7 +201,7 @@ export default class DtpWebSocket {
}
const event = new CustomEvent('dtp-channel-joined', { detail: message });
- window.dispatchEvent(event);
+ document.dispatchEvent(event);
}
async leaveChannel (channelId) {
diff --git a/lib/site-ioserver.js b/lib/site-ioserver.js
index 0b6595b..a4790e5 100644
--- a/lib/site-ioserver.js
+++ b/lib/site-ioserver.js
@@ -259,7 +259,12 @@ export class SiteIoServer extends SiteCommon {
async onSocketDisconnect (session, reason) {
const { chat: chatService } = this.dtp.services;
- this.log.debug('socket disconnect', { sid: session.socket.id, consumerId: (session.user || session.channel)._id, reason });
+ this.log.debug('socket disconnect', {
+ sid: session.socket.id,
+ consumerId: (session.user || session.channel)._id,
+ joinedRooms: session.joinedRooms.size,
+ reason,
+ });
if (session.user && session.joinedRooms.size > 0) {
for await (const room of session.joinedRooms) {
diff --git a/webpack.config.js b/webpack.config.js
index a2bd9d5..4edc359 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -68,8 +68,8 @@ export default {
},
performance: {
hints: 'warning',
- maxEntrypointSize: 512 * 1024,
- maxAssetSize: 512 * 1024,
+ maxEntrypointSize: 2 * 102412 * 1024,
+ maxAssetSize: 2 * 102412 * 1024,
},
plugins,
module: {