From a9a68b4207d3ddce2268daa275c61ae96726ad77 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 18 Apr 2024 01:39:01 -0400 Subject: [PATCH] image and video attachments --- app/controllers/chat.js | 32 +++++- app/controllers/image.js | 9 +- app/controllers/video.js | 123 +++++++++++++++++++++ app/models/image.js | 4 - app/services/chat.js | 67 ++++++++++- app/services/host-cache.js | 12 +- app/services/image.js | 22 ++-- app/services/video.js | 35 +++++- app/views/chat/components/message.pug | 19 +++- app/views/chat/room/view.pug | 14 ++- app/workers/host-services.js | 37 +++++-- client/css/dtp-site.less | 2 + client/css/site/stage.less | 40 ++++++- client/css/site/uikit-theme.dtp-dark.less | 1 + client/css/site/uikit-theme.dtp-light.less | 25 +++-- client/css/site/uk-lightbox.less | 4 + config/limiter.js | 15 ++- nodemon.json | 1 + 18 files changed, 394 insertions(+), 68 deletions(-) create mode 100644 app/controllers/video.js create mode 100644 client/css/site/uk-lightbox.less diff --git a/app/controllers/chat.js b/app/controllers/chat.js index 56cbc8d..181a793 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -44,7 +44,7 @@ export default class ChatController extends SiteController { router.post( '/room/:roomId/message', - multer.fields([{ name: 'imageFiles', maxCount: 6 }, { name: 'videoFile', maxCount: 1 }]), + multer.fields([{ name: 'imageFiles', maxCount: 6 }, { name: 'videoFiles', maxCount: 1 }]), this.postRoomMessage.bind(this), ); @@ -77,6 +77,11 @@ export default class ChatController extends SiteController { this.getRoomView.bind(this), ); + router.delete( + '/room/:roomId', + this.deleteRoom.bind(this), + ); + return router; } @@ -96,7 +101,19 @@ 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, req.imageFiles, req.videoFiles); + this.log.debug('post attachments', { + imageFiles: req.files.imageFiles, + videoFiles: req.files.videoFiles, + }); + + await chatService.sendRoomMessage( + res.locals.room, + req.user, + req.body, + req.files.imageFiles, + req.files.videoFiles, + ); + return res.status(200).json({ success: true }); } catch (error) { this.log.error('failed to send chat room message', { error }); @@ -159,4 +176,15 @@ export default class ChatController extends SiteController { return next(error); } } + + async deleteRoom (req, res, next) { + const { chat: chatService } = this.dtp.services; + try { + await chatService.destroyRoom(req.user, res.locals.room); + res.redirect('/'); + } catch (error) { + this.log.error('failed to destroy chat room', { error }); + return next(error); + } + } } \ No newline at end of file diff --git a/app/controllers/image.js b/app/controllers/image.js index 9b6594b..4f39f04 100644 --- a/app/controllers/image.js +++ b/app/controllers/image.js @@ -22,6 +22,7 @@ export default class ImageController extends SiteController { async start ( ) { const { dtp } = this; const { limiter: limiterService } = dtp.services; + const limiterConfig = limiterService.config.image; const router = express.Router(); dtp.app.use('/image', router); @@ -40,24 +41,24 @@ export default class ImageController extends SiteController { router.param('imageId', this.populateImage.bind(this)); router.post('/', - limiterService.create(limiterService.config.image.postCreateImage), + limiterService.create(limiterConfig.postCreateImage), imageUpload.single('file'), this.postCreateImage.bind(this), ); router.get('/proxy', - limiterService.create(limiterService.config.image.getProxyImage), + limiterService.create(limiterConfig.getProxyImage), this.getProxyImage.bind(this), ); router.get('/:imageId', - limiterService.create(limiterService.config.image.getImage), + limiterService.create(limiterConfig.getImage), this.getHostCacheImage.bind(this), // this.getImage.bind(this), ); router.delete('/:imageId', - limiterService.create(limiterService.config.image.deleteImage), + limiterService.create(limiterConfig.deleteImage), this.deleteImage.bind(this), ); } diff --git a/app/controllers/video.js b/app/controllers/video.js new file mode 100644 index 0000000..793223f --- /dev/null +++ b/app/controllers/video.js @@ -0,0 +1,123 @@ +// video.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import express from 'express'; + +import { pipeline } from 'node:stream'; + +import { SiteController, SiteError } from '../../lib/site-lib.js'; + +export default class VideoController extends SiteController { + + static get name ( ) { return 'VideoController'; } + static get slug ( ) { return 'video'; } + + constructor (dtp) { + super(dtp, VideoController); + } + + async start ( ) { + const { + limiter: limiterService, + session: sessionService, + } = this.dtp.services; + const limiterConfig = limiterService.config.video; + + this.templates = { + passwordResetComplete: this.loadViewTemplate('auth/password-reset-complete.pug'), + }; + + const authRequired = sessionService.authCheckMiddleware({ requireLogin: true }); + + const router = express.Router(); + this.dtp.app.use('/video', authRequired, router); + + router.use(async (req, res, next) => { + res.locals.currentView = 'video'; + return next(); + }); + + router.param('videoId', this.populateVideoId.bind(this)); + + router.get( + '/:videoId/media', + limiterService.create(limiterConfig.getVideoMedia), + this.getVideoMedia.bind(this), + ); + + return router; + } + + async populateVideoId (req, res, next, videoId) { + const { video: videoService } = this.dtp.services; + try { + res.locals.video = await videoService.getVideoById(videoId); + if (!res.locals.video) { + throw new SiteError(404, 'Video not found'); + } + return next(); + } catch (error) { + this.log.error('failed to populate video', { videoId, error }); + return next(error); + } + } + + async getVideoMedia (req, res, next) { + const { minio: minioService } = this.dtp.services; + try { + let artifact = res.locals.video.media; + + const artifactStat = await minioService.statObject(artifact.bucket, artifact.key); + const fileInfo = Object.assign({ }, artifact); + const range = req.get('range'); + if (range) { + let [start, end] = range.replace(/bytes=/, '').split('-').map((term) => parseInt(term.trim(), 10)); + if (!isNaN(start) && isNaN(end)) { + end = artifactStat.size - 1; + } + if (isNaN(start) && !isNaN(end)) { + start = artifactStat.size - end; + end = artifactStat.size - 1; + } + fileInfo.range = { start, end }; + } + + this.log.debug('starting video media stream', { + media: artifact, + fileInfo, + }); + + const stream = await minioService.openDownloadStream(fileInfo); + if (fileInfo.range) { + res.writeHead(206, { + 'Content-Type': 'video/mp4', + 'Content-Length': fileInfo.range.end - fileInfo.range.start + 1, + 'Accept-Ranges': 'bytes', + 'Content-Range': `bytes ${fileInfo.range.start}-${fileInfo.range.end}/${artifactStat.size}`, + 'Cache-Control': 'public, maxage=86400, s-maxage=86400, immutable', + }); + } else { + res.writeHead(200, { + 'Accept-Ranges': 'bytes', + 'Content-Type': 'video/mp4', + 'Content-Length': artifactStat.size, + 'Cache-Control': 'public, maxage=86400, s-maxage=86400, immutable', + }); + } + + pipeline(stream, res, (err) => { + if (err) { + this.log.debug('failed to stream media', { err }); + return; + } + this.log.info('media stream sent'); + }); + } catch (error) { + this.log.error('failed to open media stream', { videoId: res.locals.video._id, error }); + return next(error); + } + } +} diff --git a/app/models/image.js b/app/models/image.js index 3da65ca..d7edcc5 100644 --- a/app/models/image.js +++ b/app/models/image.js @@ -11,10 +11,6 @@ const ImageSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1 }, owner: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, caption: { type: String, maxLength: 300 }, - flags: { - isSensitive: { type: Boolean, default: false, required: true }, - isPendingAttachment: { type: Boolean, default: false, required: true }, - }, type: { type: String, required: true }, size: { type: Number, required: true }, file: { diff --git a/app/services/chat.js b/app/services/chat.js index f263835..07991de 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -7,7 +7,7 @@ import mongoose from 'mongoose'; const ChatRoom = mongoose.model('ChatRoom'); const ChatMessage = mongoose.model('ChatMessage'); -// const ChatRoomInvite = mongoose.model('ChatRoomInvite'); +const ChatRoomInvite = mongoose.model('ChatRoomInvite'); import numeral from 'numeral'; @@ -56,7 +56,12 @@ export default class ChatService extends SiteService { { path: 'links', populate: linkService.populateLink, - + }, + { + path: 'attachments.images', + }, + { + path: 'attachments.videos', }, ]; } @@ -82,9 +87,10 @@ export default class ChatService extends SiteService { } async destroyRoom (user, room) { - if (user._id.equals(room.owner._id)) { + if (!user._id.equals(room.owner._id)) { throw new SiteError(401, 'This is not your chat room'); } + await this.removeInvitesForRoom(room); await this.removeMessagesForChannel(room); await ChatRoom.deleteOne({ _id: room._id }); } @@ -209,8 +215,13 @@ export default class ChatService extends SiteService { } async sendRoomMessage (room, author, messageDefinition, imageFiles, videoFiles) { - const { text: textService, user: userService } = this.dtp.services; const NOW = new Date(); + const { + image: imageService, + text: textService, + user: userService, + video: videoService, + } = this.dtp.services; const message = new ChatMessage(); message.created = NOW; @@ -223,6 +234,20 @@ export default class ChatService extends SiteService { message.hashtags = await textService.findHashtags(message.content); message.links = await textService.findLinks(author, message.content, { channelId: room._id }); + if (imageFiles) { + for (const imageFile of imageFiles) { + const image = await imageService.create(author, { }, imageFile); + message.attachments.images.push(image._id); + } + } + + if (videoFiles) { + for (const videoFile of videoFiles) { + const video = await videoService.createVideo(author, { }, videoFile); + message.attachments.videos.push(video._id); + } + } + await message.save(); const messageObj = message.toObject(); @@ -324,7 +349,39 @@ export default class ChatService extends SiteService { return rooms; } + async removeInvitesForRoom (room) { + await ChatRoomInvite.deleteMany({ room: room._id }); + } + async removeMessagesForChannel (channel) { - await ChatMessage.deleteMany({ channel: channel._id }); + this.log.alert('removing all messages for channel', { channelId: channel._id }); + await ChatMessage + .find({ channel: channel._id }) + .cursor() + .eachAsync(async (message) => { + await this.removeMessage(message); + }, 4); + } + + async expireMessages ( ) { + const NOW = new Date(); + await ChatMessage + .find({ expires: { $lt: NOW } }) + .cursor() + .eachAsync(async (message) => { + await this.removeMessage(message); + }, 4); + } + + async removeMessage (message) { + const { image: imageService } = this.dtp.services; + if (message.attachments) { + if (Array.isArray(message.attachments.images) && (message.attachments.images.length > 0)) { + for (const image of message.attachments.images) { + await imageService.deleteImage(image); + } + } + } + await ChatMessage.deleteOne({ _id: message._id }); } } \ No newline at end of file diff --git a/app/services/host-cache.js b/app/services/host-cache.js index 7960db2..877ef08 100644 --- a/app/services/host-cache.js +++ b/app/services/host-cache.js @@ -133,11 +133,13 @@ export default class HostCacheService extends SiteService { if (!this.transactions) { return; } - for (const key of this.transactions) { - this.log.alert('destroying host cache transaction', { key }); - const transaction = this.transactions[key]; - transaction.reject(error); - delete this.transactions[key]; + if (this.transactions) { + for (const key in this.transactions) { + this.log.alert('destroying host cache transaction', { key }); + const transaction = this.transactions[key]; + transaction.reject(error); + delete this.transactions[key]; + } } } } \ No newline at end of file diff --git a/app/services/image.js b/app/services/image.js index 7b6611d..d6fe8fc 100644 --- a/app/services/image.js +++ b/app/services/image.js @@ -8,7 +8,7 @@ import path from 'node:path'; import fs from 'node:fs'; import mongoose from 'mongoose'; -const StreamRayImage = mongoose.model('Image'); +const ChatImage = mongoose.model('Image'); import sharp from 'sharp'; @@ -39,7 +39,7 @@ export default class ImageService extends SiteService { async create (owner, imageDefinition, file) { const NOW = new Date(); - const { chat: chatService, minio: minioService } = this.dtp.services; + const { minio: minioService } = this.dtp.services; this.log.debug('processing uploaded image', { imageDefinition, file }); @@ -48,15 +48,9 @@ export default class ImageService extends SiteService { // create an Image model instance, but leave it here in application memory. // we don't persist it to the db until MinIO accepts the binary data. - const image = new StreamRayImage(); + const image = new ChatImage(); image.created = NOW; image.owner = owner._id; - if (imageDefinition.caption) { - image.caption = chatService.filterText(imageDefinition.caption); - } - image.flags.isSensitive = imageDefinition['flags.isSensitive'] === 'on'; - image.flags.isPendingAttachment = imageDefinition['flags.isPendingAttachment'] === 'on'; - image.type = file.mimetype; image.size = file.size; @@ -90,14 +84,14 @@ export default class ImageService extends SiteService { } async getImageById (imageId) { - const image = await StreamRayImage + const image = await ChatImage .findById(imageId) .populate(this.populateImage); return image; } async getRecentImagesForOwner (owner) { - const images = await StreamRayImage + const images = await ChatImage .find({ owner: owner._id }) .sort({ created: -1 }) .limit(10) @@ -117,7 +111,7 @@ export default class ImageService extends SiteService { this.log.error('failed to remove image from storage', { error }); // fall through } - await StreamRayImage.deleteOne({ _id: image._id }); + await ChatImage.deleteOne({ _id: image._id }); } async processImageFile (owner, file, outputs, options) { @@ -145,7 +139,7 @@ export default class ImageService extends SiteService { service.log.debug('processing image', { output, outputMetadata }); - const image = new StreamRayImage(); + const image = new ChatImage(); image.created = NOW; image.owner = owner._id; image.type = `image/${output.format}`; @@ -235,7 +229,7 @@ export default class ImageService extends SiteService { } async reportStats ( ) { - const chart = await StreamRayImage.aggregate([ + const chart = await ChatImage.aggregate([ { $match: { }, }, diff --git a/app/services/video.js b/app/services/video.js index a2a5ac5..d1386e2 100644 --- a/app/services/video.js +++ b/app/services/video.js @@ -4,12 +4,14 @@ 'use strict'; +import fs from 'node:fs'; import path from 'node:path'; import mongoose from 'mongoose'; const Video = mongoose.model('Video'); import mime from 'mime'; +import numeral from 'numeral'; import { SiteService, SiteError } from '../../lib/site-lib.js'; import user from '../models/user.js'; @@ -23,6 +25,23 @@ export default class VideoService extends SiteService { super(dtp, VideoService); } + async start ( ) { + const { user: userService } = this.dtp.services; + await super.start(); + + await fs.promises.mkdir(process.env.VIDEO_WORK_PATH, { recursive: true }); + + this.populateVideo = [ + { + path: 'owner', + select: userService.USER_SELECT, + }, + { + path: 'thumbnail', + }, + ]; + } + async createVideo (owner, attachmentDefinition, file) { const NOW = new Date(); const { media: mediaService, minio: minioService } = this.dtp.services; @@ -89,16 +108,18 @@ export default class VideoService extends SiteService { await video.save(); + await this.extractVideoAttachmentThumbnail(video, file); + return video.toObject(); } async extractVideoAttachmentThumbnail (video, file) { - const { media: mediaService } = module.services; + const { media: mediaService } = this.dtp.services; const thumbnailFile = path.join(process.env.VIDEO_WORK_PATH, `${video._id}.png`); const ffmpegThumbnailArgs = [ '-y', '-i', file.path, - '-ss', numeral(video.media.metadata.duration * 0.1).format('hh:mm:ss'), + '-ss', numeral(video.media.metadata.duration * 0.05).format('hh:mm:ss'), '-frames:v', '1', thumbnailFile, ]; @@ -113,7 +134,7 @@ export default class VideoService extends SiteService { async setVideoThumbnailImage (video, thumbnailFile) { const { image: imageService } = this.dtp.services; - await this.removeThumbnailImage(video); + await this.removeVideoThumbnailImage(video); const imageFileDesc = { path: thumbnailFile }; const outputs = [ @@ -155,4 +176,12 @@ export default class VideoService extends SiteService { async setVideoStatus (video, status) { await Video.updateOne({ _id: video._id }, { $set: { status } }); } + + async getVideoById (videoId) { + const video = await Video + .findOne({ _id: videoId }) + .populate(this.populateVideo) + .lean(); + return video; + } } \ No newline at end of file diff --git a/app/views/chat/components/message.pug b/app/views/chat/components/message.pug index 2af43af..91e218e 100644 --- a/app/views/chat/components/message.pug +++ b/app/views/chat/components/message.pug @@ -22,7 +22,24 @@ mixin renderChatMessage (message) if message.content && (message.content.length > 0) .message-content div!= marked.parse(message.content, { renderer: fullMarkedRenderer }) - + + if message.attachments + .message-attachments + if Array.isArray(message.attachments.images) && (message.attachments.images.length > 0) + div(class="uk-child-width-1-1 uk-child-width-1-2@s uk-child-width-1-3@m uk-child-width-1-4@l uk-child-width-1-5@xl", uk-grid, uk-lightbox="animation: slide").uk-grid-small + each image of message.attachments.images + a(href=`/image/${image._id}`, data-type="image", data-caption= `${image.metadata.width}x${image.metadata.height} | ${image.metadata.space.toUpperCase()} | ${image.metadata.format.toUpperCase()} | ${numeral(image.size).format('0,0.0b')}`) + img(src=`/image/${image._id}`, width= image.metadata.width, height= image.metadata.height, alt="Image attachment").image-attachment + + if Array.isArray(message.attachments.videos) && (message.attachments.videos.length > 0) + each video of message.attachments.videos + video( + data-video-id= video._id, + poster= video.thumbnail ? `/image/${video.thumbnail._id}` : false, + controls, disablepictureinpicture, disableremoteplayback, playsinline, + ).video-attachment + source(src=`/video/${video._id}/media`) + if Array.isArray(message.links) && (message.links.length > 0) each link in message.links div(class="uk-width-large").uk-margin-small diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug index db2cc16..c94b1a4 100644 --- a/app/views/chat/room/view.pug +++ b/app/views/chat/room/view.pug @@ -81,20 +81,22 @@ block view-content enctype="multipart/form-data" ).uk-form textarea(id="chat-input-text", name="content", rows=2).uk-textarea.uk-resize-none.uk-border-rounded - .uk-margin-small + .input-button-bar .uk-flex .uk-width-expand div(uk-grid).uk-grid-small .uk-width-auto .uk-form-custom - input(id="image-files", name="imageFiles[]", type="file") - button(type="button").uk-button.uk-button-default Image + input(id="image-files", name="imageFiles", type="file") + button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded + i.fa.fa-image .uk-width-auto .uk-form-custom - input(id="video-file", name="videoFile", type="file") - button(type="button").uk-button.uk-button-default Video + input(id="video-file", name="videoFiles", type="file") + button(type="button").uk-button.uk-button-default.uk-button-small.uk-border-rounded + i.fa.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.uk-light + 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 block viewjs diff --git a/app/workers/host-services.js b/app/workers/host-services.js index 3125307..1749ddf 100644 --- a/app/workers/host-services.js +++ b/app/workers/host-services.js @@ -154,7 +154,10 @@ class HostCacheTransaction { return this.dtp.manager.resolveTransaction(this, res); } catch (error) { if (error.code !== 'ENOENT') { - this.dtp.log.error('failed to stat requested object', { transaction: this, error }); + this.dtp.log.error('failed to stat requested object', { + transaction: this.tid, + error, + }); res.success = false; res.statusCode = 500; res.message = error.message; @@ -182,7 +185,10 @@ class HostCacheTransaction { return this.dtp.manager.resolveTransaction(this, res); } catch (error) { if (error.code !== 'NotFound') { - this.dtp.log.error('failed to fetch requested object from MinIO', { transaction: this, error }); + this.dtp.log.error('failed to fetch requested object from MinIO', { + transaction: this.tid, + error, + }); res.success = false; res.statusCode = 500; res.message = error.message; @@ -217,6 +223,12 @@ class HostCacheTransaction { }, }; + if (this.params.url.startsWith('//')) { + this.dtp.log.debug('correcting image URL', { original: this.params.url }); + this.params.url = `https:${this.params.url}`; + this.dtp.log.debug('correcting image URL', { corrected: this.params.url }); + } + const urlHash = cryptoService.createHash(this.params.url, 'sha256'); const basePath = path.join(process.env.HOST_CACHE_PATH, 'web-resource', urlHash.slice(0, 4)); await fs.promises.mkdir(basePath, { recursive: true }); @@ -240,7 +252,10 @@ class HostCacheTransaction { return this.dtp.manager.resolveTransaction(this, res); } catch (error) { if (error.code !== 'ENOENT') { - this.dtp.log.error('failed to stat requested object', { transaction: this, error }); + this.dtp.log.error('failed to stat requested object', { + transaction: this.tid, + error, + }); res.success = false; res.statusCode = 500; res.message = error.message; @@ -262,7 +277,10 @@ class HostCacheTransaction { if (!response.ok) { this.error = new Error('Failed to fetch URL'); this.flags.isError = true; - this.dtp.log.error(this.error.message, { transaction: this, status: response.status }); + this.dtp.log.error(this.error.message, { + transaction: this.tid, + status: response.status, + }); res.success = false; res.statusCode = response.status; @@ -280,10 +298,10 @@ class HostCacheTransaction { contentSize = parseInt(contentSize, 10); } - this.dtp.log.debug('writing initial meta file', { resourceMetaFilename }); + this.dtp.log.debug('writing initial meta file', { url: this.params.url, resourceMetaFilename }); await fs.promises.writeFile(resourceMetaFilename, JSON.stringify({ contentType, contentSize })); - this.dtp.log.info('writing web resource file', resourceFilename); + this.dtp.log.info('writing web resource file', { url: this.params.url, resourceFilename }); let writeStream = fs.createWriteStream(resourceFilename, { autoClose: true, encoding: 'binary', @@ -300,7 +318,7 @@ class HostCacheTransaction { contentSize = res.file.stats.size; res.file.meta = { contentType, contentSize }; - this.dtp.log.debug('writing meta file', { resourceMetaFilename }); + this.dtp.log.debug('writing meta file', { url: this.params.url, resourceMetaFilename }); await fs.promises.writeFile(resourceMetaFilename, JSON.stringify(res.file.meta)); this.flags.isFetched = true; @@ -314,7 +332,10 @@ class HostCacheTransaction { } catch (error) { this.error = error; this.flags.isError = true; - this.dtp.log.error(this.error.message, { transaction: this, error }); + this.dtp.log.error(this.error.message, { + transaction: this.tid, + error, + }); res.success = false; res.statusCode = error.statusCode || 500; diff --git a/client/css/dtp-site.less b/client/css/dtp-site.less index f51fbae..a505e23 100644 --- a/client/css/dtp-site.less +++ b/client/css/dtp-site.less @@ -1,3 +1,5 @@ +@import "site/uk-lightbox.less"; + @import "site/main.less"; @import "site/button.less"; @import "site/navbar.less"; diff --git a/client/css/site/stage.less b/client/css/site/stage.less index ee5c3f2..efbc276 100644 --- a/client/css/site/stage.less +++ b/client/css/site/stage.less @@ -25,7 +25,6 @@ background-color: @chat-sidebar-bgcolor; color: @chat-sidebar-color; - border-right: solid 1px @stage-border-color; overflow-y: auto; overflow-x: hidden; @@ -164,7 +163,7 @@ .message-timestamp { font-size: 0.9em; line-height: 1em; - color: @chat-message-timestamp-color; + color: @system-message-timestamp-color; } } @@ -202,6 +201,23 @@ } } } + + .message-attachments { + + img.image-attachment { + border-radius: 8px; + } + + video.video-attachment { + width: auto; + height: 240px; + border-radius: 8px; + + .controls .progress { + display: none; + } + } + } } } } @@ -217,6 +233,26 @@ background-color: @chat-input-panel-bgcolor; color: @chat-input-panel-color; + + .uk-button.uk-button-default { + border: none; + outline: none; + + background-color: aliceblue; + color: #071E22; + } + + .uk-button.uk-button-primary { + border: none; + outline: none; + + background-color: blanchedalmond; + color: #071E22; + } + + .input-button-bar { + margin-top: 5px; + } } } diff --git a/client/css/site/uikit-theme.dtp-dark.less b/client/css/site/uikit-theme.dtp-dark.less index cefcdea..4b73268 100644 --- a/client/css/site/uikit-theme.dtp-dark.less +++ b/client/css/site/uikit-theme.dtp-dark.less @@ -145,6 +145,7 @@ a.uk-button.uk-button-default { @system-message-bgcolor: #3a3a3a; @system-message-color: #a8a8a8; +@system-message-timestamp-color: #a8a8a8; @chat-input-panel-bgcolor: #1a1a1a; @chat-input-panel-color: #e8e8e8; diff --git a/client/css/site/uikit-theme.dtp-light.less b/client/css/site/uikit-theme.dtp-light.less index 3f3d1e0..abcbd59 100644 --- a/client/css/site/uikit-theme.dtp-light.less +++ b/client/css/site/uikit-theme.dtp-light.less @@ -43,29 +43,30 @@ @stage-border-color: #686868; -@stage-header-bgcolor: #585858; -@stage-header-color: #e5e6b9; +@stage-header-bgcolor: #071E22; +@stage-header-color: #FCF1E8; @stage-live-member-bgcolor: #1a1a1a; @stage-live-member-color: #8a8a8a; -@chat-sidebar-bgcolor: #3f5768; -@chat-sidebar-color: #e8e8e8; +@chat-sidebar-bgcolor: #7e7b62; +@chat-sidebar-color: #FCF1E8; -@chat-container-bgcolor: #ffffff; -@chat-container-color: #2a2a2a; +@chat-container-bgcolor: #e8e8e8; +@chat-container-color: #071E22; @chat-media-bgcolor: #4a4a4a; @chat-media-color: #e8e8e8; -@chat-message-bgcolor: #e8e8e8; -@chat-message-color: #1a1a1a; -@chat-message-timestamp-color: #a8a8a8; +@chat-message-bgcolor: #FCF1E8; +@chat-message-color: #071E22; +@chat-message-timestamp-color: #679289; -@system-message-bgcolor: #686868; -@system-message-color: #c8c8c8; +@system-message-bgcolor: #EE2E31; +@system-message-color: #FCF1E8; +@system-message-timestamp-color: #FCF1E8; -@chat-input-panel-bgcolor: #e8e8e8; +@chat-input-panel-bgcolor: #a5a17c; @chat-input-panel-color: #1a1a1a; @link-container-bgcolor: rgba(0, 0, 0, 0.1); diff --git a/client/css/site/uk-lightbox.less b/client/css/site/uk-lightbox.less new file mode 100644 index 0000000..8432837 --- /dev/null +++ b/client/css/site/uk-lightbox.less @@ -0,0 +1,4 @@ +.uk-lightbox-toolbar { + background-color: rgba(0,0,0, 0.85); + border-top: solid 1px #6a6a6a; +} \ No newline at end of file diff --git a/config/limiter.js b/config/limiter.js index bb6504e..b982012 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -109,8 +109,8 @@ export default { message: 'You are uploading images too quickly', }, getProxyImage: { - total: 500, - expire: ONE_SECOND * 10, + total: 50, + expire: ONE_SECOND * 6, message: 'You are requesting proxy images too quickly', }, getImage: { @@ -207,6 +207,17 @@ export default { }, }, + /* + * VideoController + */ + video: { + getVideoMedia: { + total: 60, + expire: ONE_HOUR, + message: 'You are loading videos too quickly', + }, + }, + /* * WelcomeController */ diff --git a/nodemon.json b/nodemon.json index 25a1602..44641af 100644 --- a/nodemon.json +++ b/nodemon.json @@ -5,6 +5,7 @@ "dist", "client/**/*", "lib/client/**/*", + "app/workers/**/*", "node_modules/**/*" ] } \ No newline at end of file