diff --git a/app/controllers/chat.js b/app/controllers/chat.js index 6b9e4cc..7f0c5bd 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -25,6 +25,7 @@ export default class ChatController extends SiteController { } = this.dtp.services; const authRequired = sessionService.authCheckMiddleware({ requireLogin: true }); + const multer = this.createMulter(ChatController.slug); const router = express.Router(); this.dtp.app.use('/chat', router); @@ -39,6 +40,12 @@ export default class ChatController extends SiteController { router.param('roomId', this.populateRoomId.bind(this)); + router.post( + '/room/:roomId/message', + multer.none(), + this.postRoomMessage.bind(this), + ); + router.post( '/room', // limiterService.create(limiterService.config.chat.postCreateRoom), @@ -78,6 +85,20 @@ export default class ChatController extends SiteController { } } + async postRoomMessage (req, res) { + const { chat: chatService } = this.dtp.services; + try { + await chatService.sendRoomMessage(res.locals.room, req.user, req.body); + return res.status(200).json({ success: true }); + } catch (error) { + this.log.error('failed to send chat room message', { error }); + return res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } + } + async postCreateRoom (req, res, next) { const { chat: chatService } = this.dtp.services; try { @@ -103,8 +124,18 @@ export default class ChatController extends SiteController { } } - async getRoomView (req, res) { - res.locals.currentView = 'chat-room'; - res.render('chat/room/view'); + async getRoomView (req, res, next) { + const { chat: chatService } = this.dtp.services; + try { + res.locals.currentView = 'chat-room'; + + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.messages = await chatService.getRoomMessages(res.locals.room, res.locals.pagination); + + res.render('chat/room/view'); + } catch (error) { + this.log.error('failed to present the chat room view', { error }); + return next(error); + } } } \ No newline at end of file diff --git a/app/services/chat.js b/app/services/chat.js index 1d47152..60f01c5 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -5,7 +5,6 @@ 'use strict'; import mongoose from 'mongoose'; -// const AuthToken = mongoose.model('AuthToken'); const ChatRoom = mongoose.model('ChatRoom'); const ChatMessage = mongoose.model('ChatMessage'); // const ChatRoomInvite = mongoose.model('ChatRoomInvite'); @@ -28,6 +27,7 @@ export default class ChatService extends SiteService { const { user: userService } = this.dtp.services; this.templates = { + message: this.loadViewTemplate('chat/components/message-standalone.pug'), memberListItem: this.loadViewTemplate('chat/components/member-list-item-standalone.pug'), }; @@ -37,6 +37,19 @@ export default class ChatService extends SiteService { select: userService.USER_SELECT, }, ]; + this.populateChatMessage = [ + { + path: 'channel', + }, + { + path: 'author', + select: userService.USER_SELECT, + }, + { + path: 'mentions', + select: userService.USER_SELECT, + }, + ]; } async createRoom (owner, roomDefinition) { @@ -130,11 +143,15 @@ export default class ChatService extends SiteService { const systemMessage = { created: NOW.toISOString(), - content: `@${member.username} has connected to the room.`, + content: `${member.displayName || member.username} has entered the room.`, }; - this.dtp.emitter.to(room._id.toString()).emit('chat-control', { displayList, systemMessages: [systemMessage] }); - + this.dtp.emitter + .to(room._id.toString()) + .emit('chat-control', { + displayList, + systemMessages: [systemMessage], + }); } async chatRoomCheckOut (room, member) { @@ -182,6 +199,44 @@ export default class ChatService extends SiteService { this.dtp.emitter.to(room._id.toString()).emit('chat-control', { displayList, systemMessages: [systemMessage] }); } + async sendRoomMessage (room, author, messageDefinition) { + const { text: textService, user: userService } = this.dtp.services; + const NOW = new Date(); + + const message = new ChatMessage(); + message.created = NOW; + message.channelType = 'ChatRoom'; + message.channel = room._id; + message.author = author._id; + message.content = textService.filter(messageDefinition.content); + + await message.save(); + + const messageObj = message.toObject(); + + let viewModel = Object.assign({ }, this.dtp.app.locals); + messageObj.author = userService.filterUserObject(author); + viewModel = Object.assign(viewModel, { message: messageObj }); + + const html = this.templates.message(viewModel); + this.dtp.emitter + .to(room._id.toString()) + .emit('chat-message', { message: messageObj, html }); + + return messageObj; + } + + async getRoomMessages (room, pagination) { + const messages = await ChatMessage + .find({ channel: room._id }) + .sort({ created: 1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateChatMessage) + .lean(); + return messages; + } + async checkRoomMember (room, member) { if (room.owner._id.equals(member._id)) { return true; diff --git a/app/views/chat/components/message-standalone.pug b/app/views/chat/components/message-standalone.pug new file mode 100644 index 0000000..270c119 --- /dev/null +++ b/app/views/chat/components/message-standalone.pug @@ -0,0 +1,2 @@ +include message ++renderChatMessage(message) \ No newline at end of file diff --git a/app/views/chat/components/message.pug b/app/views/chat/components/message.pug index a88243e..302ccd6 100644 --- a/app/views/chat/components/message.pug +++ b/app/views/chat/components/message.pug @@ -11,6 +11,12 @@ mixin renderChatMessage (message) .author-display-name= message.author.displayName .author-username @#{message.author.username} .uk-width-auto - .message-timestamp= dayjs(message.created).format('h:mm a') - .message-content - div!= marked.parse(message.content) + .message-timestamp( + data-dtp-timestamp= message.created, + data-dtp-timestamp-format= "time", + uk-tooltip={ title: dayjs(message.created).format('MMM D, YYYY') } + )= dayjs(message.created).format('h:mm a') + + if message.content && (message.content.length > 0) + .message-content + div!= marked.parse(message.content, { renderer: fullMarkedRenderer }) \ No newline at end of file diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug index 68fb840..dafba37 100644 --- a/app/views/chat/room/view.pug +++ b/app/views/chat/room/view.pug @@ -1,4 +1,6 @@ extends ../../layout/main +block vendorcss + link(rel='stylesheet', href=`/highlight.js/styles/qtcreator-light.min.css?v=${pkg.version}`) block view-content include ../components/message @@ -37,6 +39,8 @@ block view-content .uk-margin-small-left i.fas.fa-cog + block view-navbar + .dtp-chat-stage #room-member-panel.chat-sidebar .chat-stage-header @@ -55,13 +59,21 @@ block view-content .chat-container .chat-stage-header - div(uk-grid) - .uk-width-expand= room.name + div(uk-grid).uk-grid-small + .uk-width-expand + .uk-text-truncate= room.name + if room.owner._id.equals(user._id) + .uk-width-auto + a(href=`/chat/room/${room._id}/settings`, uk-tooltip={ title: 'Configure room settings' }).uk-link-reset + i.fas.fa-cog + .uk-width-auto + a(href="/", uk-tooltip={ title: 'Leave room' }).uk-link-reset + i.fas.fa-person-through-window .chat-content-panel .live-content .chat-media - div(uk-grid) + div(uk-grid).uk-flex-center div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@l uk-width-1-4@xl") +renderLiveMember(user) @@ -73,24 +85,26 @@ block view-content 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) + each message of messages + +renderChatMessage(message) .chat-input-panel form( + method="POST", + action=`/chat/room/${room._id}/message`, id="chat-input-form", data-room-id= room._id, onsubmit="return window.dtp.app.sendUserChat(event);", hidden= user && user.flags && user.flags.isCloaked, + enctype="multipart/form-data" ).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 + textarea(id="chat-input-text", name="content", 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-primary.uk-button-small.uk-border-rounded.uk-light + i.fas.fa-paper-plane block viewjs script. diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index d53298a..1547807 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -5,12 +5,6 @@ nav(style="background: #000000;").uk-navbar-container.uk-light a(href="/", aria-label="Back to Home").uk-navbar-item.uk-logo.uk-padding-remove-left img(src="/img/nav-icon.png").navbar-logo - ul.uk-navbar-nav - li.uk-active - a(href="/") - span - i.fas.fa-home - span HOME .uk-navbar-right if !user ul.uk-navbar-nav diff --git a/app/views/home.pug b/app/views/home.pug index 88a4615..68c4378 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -2,9 +2,20 @@ extends layout/main block view-content mixin renderRoomListEntry (room) - a(href=`/chat/room/${room._id}`).uk-link-reset - .uk-text-bold= room.name - .uk-text-small= room.topic || '(no topic assigned)' + div(uk-grid) + .uk-width-expand + a(href=`/chat/room/${room._id}`).uk-link-reset + .uk-text-bold= room.name + .uk-text-small= room.topic || '(no topic assigned)' + .uk-width-auto + .uk-text-small Active + .uk-text-bold(class={ + 'uk-text-success': (room.stats.presentCount > 0), + 'uk-text-muted': (room.stats.presentCount === 0), + })= room.stats.presentCount + .uk-width-auto + .uk-text-small Members + .uk-text-bold= room.stats.memberCount section.uk-section.uk-section-default .uk-container diff --git a/client/css/site/stage.less b/client/css/site/stage.less index db6afed..358ecd7 100644 --- a/client/css/site/stage.less +++ b/client/css/site/stage.less @@ -1,9 +1,9 @@ -@stage-panel-padding: 4px 10px; +@stage-panel-padding: 5px; @stage-border-color: #4a4a4a; .dtp-chat-stage { - position: absolute; - top: @site-navbar-height; right: 0; bottom: 0; left: 0; + position: fixed; + top: 0; right: 0; bottom: 0; left: 0; display: flex; @@ -13,7 +13,7 @@ padding: @stage-panel-padding; - font-weight: bold; + font-size: 0.9em; background-color: #2a2a2a; color: #e8e8e8; @@ -95,15 +95,15 @@ video { display: block; - aspect-ratio: 16 / 9; width: 100%; height: auto; + aspect-ratio: 16 / 9; border-radius: 3px; } .live-meta { - color: #e8e8e8; + color: #a8a8a8; .live-username { color: inherit; @@ -118,14 +118,14 @@ flex-grow: 0; flex-shrink: 0; - width: 320px; + width: 400px; padding: @stage-panel-padding; overflow-y: scroll; .chat-message { padding: 5px; - margin-bottom: 10px; + margin-bottom: 5px; line-height: 1; border-radius: 4px; @@ -134,8 +134,20 @@ color: #1a1a1a; &.system-message { - background-color: #d1f3db; + font-size: 0.8em; + background-color: transparent; + color: #4a4a4a; border-radius: 4px; + + .message-content { + margin-bottom: 2px; + } + + .message-timestamp { + border-top: solid 1px #4a4a4a; + font-size: 0.9em; + color: #808080; + } } .message-attribution { @@ -150,7 +162,7 @@ } .message-timestamp { - font-size: 0.9em; + font-size: 0.8em; } } @@ -161,6 +173,16 @@ margin-bottom: 10px; color: #2a2a2a; } + + pre { + padding: 0; + background: transparent; + border: none; + + code { + padding: 5px; + } + } } } } diff --git a/client/js/chat-audio.js b/client/js/chat-audio.js new file mode 100644 index 0000000..fab23ec --- /dev/null +++ b/client/js/chat-audio.js @@ -0,0 +1,151 @@ +// chat-audio.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +const DTP_COMPONENT_NAME = 'ChatAudio'; + +import DtpLog from 'lib/dtp-log'; + +const AudioContext = window.AudioContext || window.webkitAudioContext; + +export default class ChatAudio { + + constructor ( ) { + this.log = new DtpLog(DTP_COMPONENT_NAME); + } + + start ( ) { + this.log.info('start', 'starting Web Audio API main context'); + this.ctx = new AudioContext(); + + this.masterVolume = this.ctx.createGain(); + this.masterVolume.connect(this.ctx.destination); + + this.musicGain = this.ctx.createGain(); + this.musicGain.value = 0.7; + this.musicGain.connect(this.masterVolume); + + this.sounds = { }; + } + + async loadSound (soundId, url) { + if (this.sounds[soundId]) { + throw new Error('Already have sound registered for soundId'); + } + + const audioBuffer = await this.loadAudioBuffer(url); + const sound = { soundId, url, audioBuffer }; + this.sounds[soundId] = sound; + + return sound; + } + + hasSound (soundId) { + return !!this.sounds[soundId]; + } + + playSound (soundId, options) { + const sound = this.sounds[soundId]; + if (!sound) { + throw new Error(`Invalid soundId: ${soundId}`); + } + + this.log.info('playSound', 'playing sound', { soundId }); + return { soundId, ...this.playAudioBuffer(sound.audioBuffer, options) }; + } + + loadAudioBuffer (url) { + return new Promise(async (resolve, reject) => { + const response = await fetch(url); + const audioData = await response.arrayBuffer(); + this.ctx.decodeAudioData(audioData, resolve, reject); + }); + } + + playAudioBuffer (buffer, options) { + options = Object.assign({ + gain: 0.4, + loop: false, + }, options); + + const source = this.ctx.createBufferSource(); + source.buffer = buffer; + source.loop = options.loop; + + const gainNode = this.ctx.createGain(); + gainNode.gain.value = options.gain; + + source.connect(gainNode); + gainNode.connect(this.masterVolume); + + source.start(); + + return { gain: gainNode, source }; + } + + setMusicStream (url) { + let source; + + this.stopMusicStream(); + this.log.debug('setMusicStream', 'setting new music stream', { url }); + this.music = new Audio(); + this.music.setAttribute('loop', 'loop'); + + source = document.createElement('source'); + source.setAttribute('src', `${url}.ogg`); + source.setAttribute('type', 'audio/ogg'); + this.music.appendChild(source); + + source = document.createElement('source'); + source.setAttribute('src', `${url}.mp3`); + source.setAttribute('type', 'audio/mp3'); + this.music.appendChild(source); + } + + playMusicStream ( ) { + if (this.musicSource) { + return; + } + + this.musicSource = this.ctx.createMediaElementSource(this.music); + this.musicSource.connect(this.musicGain); + + this.log.debug('playMusicStream', 'starting music stream playback'); + this.music.play(); + } + + stopMusicStream ( ) { + if (!this.musicSource) { + return; + } + + this.log.debug('pauseMusicStream', 'stopping music stream playback'); + this.music.pause(); + + this.musicGain.value = 0; + + this.musicSource.disconnect(this.musicGain); + delete this.musicSource; + } + + get musicVolume ( ) { return this.musicGain.gain.value; } + set musicVolume (volume) { + this.musicGain.gain.value = volume; + } + + get haveMusicStream ( ) { + return !!this.music && + !!this.musicSource && + !!this.musicGain + ; + } + + get isMusicPaused ( ) { + if (!this.music) { + return true; + } + return this.music.paused; + } +} \ No newline at end of file diff --git a/client/js/chat-client.js b/client/js/chat-client.js index bbc1439..4e9a045 100644 --- a/client/js/chat-client.js +++ b/client/js/chat-client.js @@ -8,14 +8,18 @@ const DTP_COMPONENT_NAME = 'DtpChatApp'; const dtp = window.dtp = window.dtp || { }; import DtpApp from 'lib/dtp-app.js'; +import ChatAudio from './chat-audio.js'; import QRCode from 'qrcode'; import Cropper from 'cropperjs'; import dayjs from 'dayjs'; +import hljs from 'highlight.js'; export class ChatApp extends DtpApp { + static get SFX_CHAT_MESSAGE ( ) { return 'chat-message'; } + constructor (user) { super(DTP_COMPONENT_NAME, user); this.loadSettings(); @@ -31,8 +35,170 @@ export class ChatApp extends DtpApp { isAtBottom: true, }; + if (this.chat.input) { + this.chat.input.addEventListener('keydown', this.onChatInputKeyDown.bind(this)); + this.observer = new MutationObserver(this.onChatMessageListChanged.bind(this)); + this.observer.observe(this.chat.messageList, { childList: true }); + hljs.highlightAll(); + } + window.addEventListener('dtp-load', this.onDtpLoad.bind(this)); window.addEventListener('unload', this.onDtpUnload.bind(this)); + + this.updateTimestamps(); + } + + async startAudio ( ) { + this.log.info('startAudio', 'starting ChtaAudio'); + this.audio = new ChatAudio(); + this.audio.start(); + try { + await Promise.all([ + this.audio.loadSound(ChatApp.SFX_CHAT_MESSAGE, '/static/sfx/chat-message.mp3'), + ]); + } catch (error) { + this.log.error('startAudio', 'failed to load sound', { error }); + // fall through + } + } + + async onChatInputKeyDown (event) { + if (event.key === 'Enter' && !event.shiftKey) { + if (dtp.room) { + return this.sendChatRoomMessage(event); + } + if (dtp.thread) { + return this.sendPrivateMessage(event); + } + return this.sendUserChat(event); + } + } + + async onChatMessageListChanged (mutationList) { + this.log.info('onMutation', 'DOM mutation received', { mutationList }); + if (!Array.isArray(mutationList) || (mutationList.length === 0)) { + return; + } + for (const mutation of mutationList) { + for (const node of mutation.addedNodes) { + if (typeof node.querySelectorAll !== 'function') { + continue; + } + const timestamps = node.querySelectorAll("[data-dtp-timestamp]"); + this.updateTimestamps(timestamps); + } + } + hljs.highlightAll(); + } + + updateTimestamps ( ) { + const nodeList = document.querySelectorAll("[data-dtp-timestamp]"); + this.log.debug('updateTimestamps', 'updating timestamps', { count: nodeList.length }); + for (const ts of nodeList) { + const date = ts.getAttribute('data-dtp-timestamp'); + const format = ts.getAttribute('data-dtp-timestamp-format'); + if (!date) { continue; } + switch (format) { + case 'date': + ts.textContent = dayjs(date).format('MMM DD, YYYY'); + break; + case 'time': + ts.textContent = dayjs(date).format('h:mm a'); + break; + case 'datetime': + ts.textContent = dayjs(date).format('MMM D [at] h:mm a'); + break; + case 'fuzzy': + ts.textContent = dayjs(date).fromNow(); + break; + + case 'timestamp': + default: + ts.textContent = dayjs(date).format('hh:mm:ss a'); + break; + } + } + } + + async sendUserChat (event) { + event.preventDefault(); + + if (!dtp.user) { + UIkit.modal.alert('There is a problem with Chat. Please refresh the page.'); + return; + } + + if (this.chatTimeout) { + return; + } + + const channelId = dtp.user._id; + const content = this.chat.input.value; + this.chat.input.value = ''; + + if (content.length === 0) { + return true; + } + + this.log.debug('sendUserChat', 'sending chat message', { channel: this.user._id, content }); + this.socket.sendUserChat(channelId, content); + + // set focus back to chat input + this.chat.input.focus(); + + const isFreeMember = false; + this.chat.sendButton.setAttribute('disabled', ''); + this.chat.sendButton.setAttribute('uk-tooltip', isFreeMember ? 'Waiting 30 seconds' : 'Waiting 5 seconds'); + + this.chatTimeout = setTimeout(( ) => { + delete this.chatTimeout; + this.chat.sendButton.removeAttribute('disabled'); + this.chat.sendButton.setAttribute('uk-tooltip', 'Send message'); + }, isFreeMember ? 30000 : 5000); + + return true; + } + + async sendChatRoomMessage (event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + if (this.chatTimeout) { + return; + } + + const form = new FormData(this.chat.form); + const roomId = this.chat.form.getAttribute('data-room-id'); + + const content = this.chat.input.value; + this.chat.input.value = ''; + if (content.length === 0) { + return true; + } + + try { + this.log.info('sendChatRoomMessage', 'sending chat message', { room: roomId, content }); + const response = await fetch(this.chat.form.action, { + method: this.chat.form.method, + body: form, + }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(`Failed to send chat message: ${error.message}`); + } + + // set focus back to chat input + this.chat.input.focus(); + + this.chat.sendButton.setAttribute('disabled', ''); + this.chatTimeout = setTimeout(( ) => { + delete this.chatTimeout; + this.chat.sendButton.removeAttribute('disabled'); + }, 5000); + + return true; } async onDtpLoad ( ) { @@ -50,6 +216,7 @@ export class ChatApp extends DtpApp { async onChatSocketConnect (socket) { this.log.debug('onSocketConnect', 'attaching socket events'); + socket.on('chat-message', this.onChatMessage.bind(this)); socket.on('chat-control', this.onChatControl.bind(this)); socket.on('system-message', this.onSystemMessage.bind(this)); @@ -60,10 +227,17 @@ export class ChatApp extends DtpApp { async onChatSocketDisconnect (socket) { this.log.debug('onSocketDisconnect', 'detaching socket events'); + socket.off('chat-message', this.onChatMessage.bind(this)); socket.off('chat-control', this.onChatControl.bind(this)); socket.off('system-message', this.onSystemMessage.bind(this)); } + async onChatMessage (message) { + this.log.info('onChatMessage', 'chat message received', { message }); + this.chat.messageList.insertAdjacentHTML('beforeend', message.html); + this.audio.playSound(ChatApp.SFX_CHAT_MESSAGE); + } + async onChatControl (message) { const isAtBottom = this.chat.isAtBottom; @@ -113,18 +287,21 @@ export class ChatApp extends DtpApp { const systemMessage = document.createElement('div'); systemMessage.classList.add('chat-message'); systemMessage.classList.add('system-message'); + systemMessage.classList.add('no-select'); const chatContent = document.createElement('div'); - chatContent.classList.add('chat-content'); + chatContent.classList.add('message-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('message-timestamp'); chatTimestamp.classList.add('uk-text-small'); + chatTimestamp.classList.add('uk-text-right'); chatTimestamp.setAttribute('data-dtp-timestamp', message.created); - chatTimestamp.innerHTML = dayjs(message.created).format('hh:mm:ss a'); + chatTimestamp.setAttribute('data-dtp-timestamp-format', 'time'); + chatTimestamp.innerHTML = dayjs(message.created).format('h:mm:ss a'); systemMessage.appendChild(chatTimestamp); this.chat.messageList.appendChild(systemMessage); diff --git a/client/js/index.js b/client/js/index.js index e4b3fd4..2220d6c 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -17,6 +17,8 @@ window.addEventListener('load', async ( ) => { dtp.app = new ChatApp(dtp.user); dtp.log.info('load handler', 'application instance created', { env: dtp.env }); + await dtp.app.startAudio(); + dtp.log.debug('load', 'dispatching load event'); window.dispatchEvent(new Event('dtp-load')); }); \ No newline at end of file diff --git a/client/static/sfx/chat-message.mp3 b/client/static/sfx/chat-message.mp3 new file mode 100644 index 0000000..bbda4dd Binary files /dev/null and b/client/static/sfx/chat-message.mp3 differ diff --git a/dtp-chat.js b/dtp-chat.js index 3c1395e..fdbaad0 100644 --- a/dtp-chat.js +++ b/dtp-chat.js @@ -53,8 +53,9 @@ class Harness { constructor ( ) { this.config = { root: __dirname }; - this.log = new SiteLog(this, Harness); this.models = [ ]; + + this.log = new SiteLog(this, Harness); } async start ( ) { @@ -297,6 +298,7 @@ class Harness { this.app.use('/dayjs', cacheOneDay, express.static(path.join(this.config.root, 'node_modules', 'dayjs'))); this.app.use('/numeral', cacheOneDay, express.static(path.join(this.config.root, 'node_modules', 'numeral', 'min'))); + this.app.use('/highlight.js', cacheOneDay, express.static(path.join(this.config.root, 'node_modules', 'highlight.js'))); /* * Webpack integration @@ -482,8 +484,8 @@ class Harness { viewModel.fullMarkedRenderer.link = confirmedLinkRenderer; viewModel.safeMarkedRenderer = new Marked.Renderer(); - viewModel.safeMarkedRenderer.link = safeLinkRenderer; viewModel.safeMarkedRenderer.image = safeImageRenderer; + viewModel.safeMarkedRenderer.link = safeLinkRenderer; viewModel.markedConfigChat = { renderer: this.safeMarkedRenderer, diff --git a/nodemon.json b/nodemon.json index f8708a8..27b9a3b 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,4 +1,9 @@ { "verbose": true, - "ignore": ["dist"] + "ignore": [ + "dist", + "client/**/*", + "lib/client/**/*", + "node_modules/**/*" + ] } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 4edc359..1ad0a95 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -41,6 +41,7 @@ if (webpackMode === 'development') { files: [ './dist/*.js', './dist/*.css', + './app/views/**/*', ], }), );