From dbc47abac4e971ee582950044c1372dca3a507b5 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 18 Apr 2024 21:38:50 -0400 Subject: [PATCH] several updates - Added a new emoji picker and its presentation logic - Added ability to delete a chat room (as owner) - Removing a chat message now also removes all attachments on that message from storage - Fixed many UI bugs in chat view - --- README.md | 5 ++- app/controllers/chat.js | 13 +++++-- app/services/chat.js | 11 +++++- app/services/video.js | 10 +++++ app/views/chat/components/message.pug | 17 ++++++--- app/views/chat/room/settings.pug | 8 +++- app/views/chat/room/view.pug | 24 +++++++++--- app/views/components/emoji-picker.pug | 8 ++++ client/css/dtp-site.less | 7 ++-- client/css/site/emoji-picker.less | 22 +++++++++++ client/css/site/stage.less | 25 +++++++++++- client/js/chat-client.js | 55 +++++++++++++++++++++++++++ client/js/index.js | 2 + package.json | 1 + yarn.lock | 8 ++++ 15 files changed, 194 insertions(+), 22 deletions(-) create mode 100644 app/views/components/emoji-picker.pug create mode 100644 client/css/site/emoji-picker.less diff --git a/README.md b/README.md index 2574637..842cfed 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # DTP Chat -This project is currently being used to develop an all-new harness for developing and deploying DTP web apps. Gulp is now gone, it's based on Webpack, Nodemon and BrowserSync. \ No newline at end of file +This project is currently being used to develop an all-new harness for developing and deploying DTP web apps. Gulp is now gone, it's based on Webpack, Nodemon and BrowserSync. + +## Emoji Picker +Chat currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. \ No newline at end of file diff --git a/app/controllers/chat.js b/app/controllers/chat.js index 42b3bc9..6a311d3 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -219,14 +219,21 @@ export default class ChatController extends SiteController { } } - async deleteRoom (req, res, next) { + async deleteRoom (req, res) { const { chat: chatService } = this.dtp.services; try { await chatService.destroyRoom(req.user, res.locals.room); - res.redirect('/'); + + const displayList = this.createDisplayList('chat-room-delete'); + displayList.navigateto('/'); + + res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to destroy chat room', { error }); - return next(error); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); } } } \ No newline at end of file diff --git a/app/services/chat.js b/app/services/chat.js index 07991de..d028103 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -374,14 +374,23 @@ export default class ChatService extends SiteService { } async removeMessage (message) { - const { image: imageService } = this.dtp.services; + const { image: imageService, video: videoService } = this.dtp.services; if (message.attachments) { if (Array.isArray(message.attachments.images) && (message.attachments.images.length > 0)) { for (const image of message.attachments.images) { + this.log.debug('removing message attachment', { imageId: image._id }); await imageService.deleteImage(image); } } + if (Array.isArray(message.attachments.videos) && (message.attachments.videos.length > 0)) { + for (const video of message.attachments.videos) { + this.log.debug('removing video attachment', { videoId: video._id }); + await videoService.removeVideo(video); + } + } } + + this.log.debug('removing chat message', { messageId: message._id }); await ChatMessage.deleteOne({ _id: message._id }); } } \ No newline at end of file diff --git a/app/services/video.js b/app/services/video.js index d1386e2..b49b053 100644 --- a/app/services/video.js +++ b/app/services/video.js @@ -184,4 +184,14 @@ export default class VideoService extends SiteService { .lean(); return video; } + + async removeVideo (video) { + const { minio: minioService } = this.dtp.services; + + if (video.thumbnail) { + await this.removeVideoThumbnailImage(video); + } + + await minioService.removeObject(video.media.bucket, video.media.key); + } } \ No newline at end of file diff --git a/app/views/chat/components/message.pug b/app/views/chat/components/message.pug index 91e218e..12c3836 100644 --- a/app/views/chat/components/message.pug +++ b/app/views/chat/components/message.pug @@ -7,11 +7,9 @@ mixin renderChatMessage (message) +renderProfilePicture(message.author, { iconClass: 'member-profile-icon' }) .uk-width-expand .message-attribution.uk-margin-small.no-select - .uk-flex.uk-flex-top - .uk-width-expand - if (message.author.displayName && (message.author.displayName.length > 0)) - .author-display-name= message.author.displayName - .author-username @#{message.author.username} + div(uk-grid).uk-grid-small.uk-flex-middle + .uk-width-auto + .author-display-name= message.author.displayName || message.author.username .uk-width-auto .message-timestamp( data-dtp-timestamp= message.created, @@ -43,4 +41,11 @@ mixin renderChatMessage (message) if Array.isArray(message.links) && (message.links.length > 0) each link in message.links div(class="uk-width-large").uk-margin-small - +renderLinkPreview(link, { layout: 'responsive' }) \ No newline at end of file + +renderLinkPreview(link, { layout: 'responsive' }) + .uk-width-auto + .uk-text-bold ! + + .message-menu + div(uk-grid).uk-grid-small + .uk-width-auto + div Emoji reacts & shit diff --git a/app/views/chat/room/settings.pug b/app/views/chat/room/settings.pug index 8393b29..f861b6a 100644 --- a/app/views/chat/room/settings.pug +++ b/app/views/chat/room/settings.pug @@ -18,4 +18,10 @@ block view-content input(id="topic", name="topic", type="text", placeholder="Enter room topic or leave blank", value= room.topic).uk-input .uk-card-footer.uk-flex.uk-flex-right - button(type="submit").uk-button.uk-button-default.uk-border-rounded Save Settings \ No newline at end of file + button( + type="button", + data-room-id= room._id, + data-room-name= room.name, + onclick="dtp.app.confirmRoomDelete(event);", + ).uk-button.uk-button-danger.uk-border-rounded.uk-margin-right Delete Room + button(type="submit").uk-button.uk-button-primary.uk-border-rounded Save Settings \ No newline at end of file diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug index c94b1a4..6c9c54a 100644 --- a/app/views/chat/room/view.pug +++ b/app/views/chat/room/view.pug @@ -85,19 +85,31 @@ block view-content .uk-flex .uk-width-expand div(uk-grid).uk-grid-small + .uk-width-auto + button( + type="button", + data-target="#chat-input-text", + uk-tooltip="Select emojis to add", + onclick="dtp.app.showEmojiPicker(event);", + ).uk-button.uk-button-default.uk-button-small.uk-border-rounded + i.fa-regular.fa-face-smile .uk-width-auto .uk-form-custom - input(id="image-files", name="imageFiles", type="file") + input(id="image-files", name="imageFiles", type="file", uk-tooltip="Select an image to attach") button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded - i.fa.fa-image + span + i.fa-regular.fa-image .uk-width-auto .uk-form-custom - input(id="video-file", name="videoFiles", type="file") + input(id="video-file", name="videoFiles", type="file", uk-tooltip="Select a video to attach") button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded - i.fa.fa-video + span + i.fa-solid.fa-video .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 - i.fas.fa-paper-plane + button(id="chat-send-btn", type="submit", uk-tooltip={ title: 'Send message' }).uk-button.uk-button-default.uk-button-small.uk-border-rounded + i.fa-regular.fa-paper-plane + + include ../../components/emoji-picker block viewjs script. diff --git a/app/views/components/emoji-picker.pug b/app/views/components/emoji-picker.pug new file mode 100644 index 0000000..1b55848 --- /dev/null +++ b/app/views/components/emoji-picker.pug @@ -0,0 +1,8 @@ +.emoji-picker-display + .emoji-picker-prompt.sr-only Select an emoji + emoji-picker( + class={ + 'dark': (user && (user.ui.theme === 'chat-dark')), + 'light': (!user || (user.ui.theme === 'chat-light')), + } + ) \ No newline at end of file diff --git a/client/css/dtp-site.less b/client/css/dtp-site.less index a505e23..c0700cf 100644 --- a/client/css/dtp-site.less +++ b/client/css/dtp-site.less @@ -2,7 +2,8 @@ @import "site/main.less"; @import "site/button.less"; -@import "site/navbar.less"; -@import "site/stage.less"; +@import "site/emoji-picker.less"; +@import "site/image.less"; @import "site/link-preview.less"; -@import "site/image.less"; \ No newline at end of file +@import "site/navbar.less"; +@import "site/stage.less"; \ No newline at end of file diff --git a/client/css/site/emoji-picker.less b/client/css/site/emoji-picker.less new file mode 100644 index 0000000..d38ec78 --- /dev/null +++ b/client/css/site/emoji-picker.less @@ -0,0 +1,22 @@ +.emoji-picker-display { + position: fixed; + top: 0; right: 0; bottom: 0; left: 0; + + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + + background: rgba(0,0,0, 0.8); + color: #e8e8e8; + + &.picker-active { + display: flex; + } + + .emoji-picker-prompt { + margin-bottom: 10px; + font-size: 1.5em; + color: #e8e8e8; + } +} \ No newline at end of file diff --git a/client/css/site/stage.less b/client/css/site/stage.less index efbc276..2c6d369 100644 --- a/client/css/site/stage.less +++ b/client/css/site/stage.less @@ -40,7 +40,7 @@ width: 32px; height: auto; border-radius: 5px; - margin-right: 5px; + margin-right: 10px; } .member-list-item { @@ -140,6 +140,9 @@ overflow-y: scroll; .chat-message { + box-sizing: border-box; + position: relative; + padding: 5px; margin-bottom: 5px; @@ -148,6 +151,26 @@ background-color: @chat-message-bgcolor; color: @chat-message-color; + + &:hover { + .message-menu { + display: block; + } + } + + .message-menu { + display: none; + + box-sizing: border-box; + position: absolute; + top: 0; right: 10px; + + padding: 5px 10px; + background-color: white; + color: #1a1a1a; + + border-radius: 16px; + } &.system-message { font-size: 0.8em; diff --git a/client/js/chat-client.js b/client/js/chat-client.js index aea2590..6ffc788 100644 --- a/client/js/chat-client.js +++ b/client/js/chat-client.js @@ -49,6 +49,15 @@ export class ChatApp extends DtpApp { window.addEventListener('unload', this.onDtpUnload.bind(this)); this.updateTimestamps(); + + this.emojiPickerDisplay = document.querySelector('.emoji-picker-display'); + if (this.emojiPickerDisplay) { + this.emojiPickerDisplay.addEventListener('click', this.onEmojiPickerClose.bind(this)); + } + this.emojiPicker = document.querySelector('emoji-picker'); + if (this.emojiPicker) { + this.emojiPicker.addEventListener('emoji-click', this.onEmojiPicked.bind(this)); + } } async startAudio ( ) { @@ -373,6 +382,23 @@ export class ChatApp extends DtpApp { return true; } + async confirmRoomDelete (event) { + const target = event.currentTarget || event.target; + const roomId = target.getAttribute('data-room-id'); + const roomName = target.getAttribute('data-room-name'); + try { + await UIkit.modal.confirm(`Are you sure you want to delete "${roomName}"?`); + } catch (error) { + return; + } + try { + const response = await fetch(`/chat/room/${roomId}`, { method: 'DELETE' }); + await this.processResponse(response); + } catch (error) { + UIkit.modal.alert(error.message); + } + } + async generateOtpQR (canvas, keyURI) { QRCode.toCanvas(canvas, keyURI); } @@ -683,4 +709,33 @@ export class ChatApp extends DtpApp { this.chat.messageList.scrollTo(0, this.chat.messageList.scrollHeight + 50000); } } + + async showEmojiPicker (event) { + const target = event.currentTarget || event.target; + const emojiTargetSelector = target.getAttribute('data-target'); + + this.emojiPickerTarget = document.querySelector(emojiTargetSelector); + if (!this.emojiPickerTarget) { + UIkit.modal.alert('Invalid emoji picker target'); + return; + } + + this.emojiPickerDisplay.classList.add('picker-active'); + } + + async onEmojiPickerClose (event) { + if (!this.emojiPickerDisplay) { + return; + } + if (!event.target.classList.contains('emoji-picker-display')) { + return; + } + this.emojiPickerDisplay.classList.remove('picker-active'); + } + + async onEmojiPicked (event) { + event = event.detail; + this.log.info('onEmojiPicked', 'An emoji has been selected', { event }); + this.emojiPickerTarget.value += event.unicode; + } } \ No newline at end of file diff --git a/client/js/index.js b/client/js/index.js index 2220d6c..b50537d 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -11,6 +11,8 @@ import { ChatApp } from './chat-client.js'; import DtpWebLog from 'lib/dtp-log.js'; window.addEventListener('load', async ( ) => { + await import('emoji-picker-element'); + dtp.log = new DtpWebLog(DTP_COMPONENT_NAME); dtp.env = document.body.getAttribute('data-dtp-env'); diff --git a/package.json b/package.json index c9f4c81..6adf223 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dotenv": "^16.4.5", "email-domain-check": "^1.1.4", "email-validator": "^2.0.4", + "emoji-picker-element": "^1.21.3", "express": "^4.19.2", "express-limiter": "^1.6.1", "express-session": "^1.18.0", diff --git a/yarn.lock b/yarn.lock index e464042..e59a0fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3709,6 +3709,7 @@ __metadata: dotenv: "npm:^16.4.5" email-domain-check: "npm:^1.1.4" email-validator: "npm:^2.0.4" + emoji-picker-element: "npm:^1.21.3" express: "npm:^4.19.2" express-limiter: "npm:^1.6.1" express-session: "npm:^1.18.0" @@ -3825,6 +3826,13 @@ __metadata: languageName: node linkType: hard +"emoji-picker-element@npm:^1.21.3": + version: 1.21.3 + resolution: "emoji-picker-element@npm:1.21.3" + checksum: 10c0/d7c1f98c598a23f86e6d1e1d5ade8713b31e021e3b5388b175106476976e9a41751497cd6e448d85402811ed5dabed3b38177b0da3bcdadb2a6b263a9282c149 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0"