From 13838539218e10e951210860274e00c9567e1242 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 11 Apr 2024 20:56:14 -0400 Subject: [PATCH] wrote more code --- app/controllers/chat.js | 93 ++++++++++++ app/controllers/home.js | 20 ++- app/controllers/user.js | 16 ++- app/models/chat-message.js | 2 +- app/models/chat-room.js | 2 +- app/models/link.js | 79 ++++++++++ app/models/user.js | 3 + app/services/chat.js | 92 +++++++++++- app/services/user.js | 2 + app/views/chat/room/create.pug | 21 +++ app/views/chat/room/view.pug | 41 ++++++ app/views/components/navbar.pug | 2 +- app/views/home.pug | 47 ++++-- app/views/layout/main.pug | 2 +- app/views/user/settings.pug | 247 +++++++++++++++----------------- client/css/dtp-site.less | 1 + client/css/site/button.less | 0 client/css/site/main.less | 12 +- client/css/site/navbar.less | 4 + client/css/site/stage.less | 53 ++++++- client/js/chat-client.js | 45 +++--- config/limiter.js | 2 +- lib/site-ioserver.js | 9 +- 23 files changed, 606 insertions(+), 189 deletions(-) create mode 100644 app/controllers/chat.js create mode 100644 app/models/link.js create mode 100644 app/views/chat/room/create.pug create mode 100644 app/views/chat/room/view.pug create mode 100644 client/css/site/button.less create mode 100644 client/css/site/navbar.less diff --git a/app/controllers/chat.js b/app/controllers/chat.js new file mode 100644 index 0000000..c39ff86 --- /dev/null +++ b/app/controllers/chat.js @@ -0,0 +1,93 @@ +// auth.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import express from 'express'; + +import { SiteController, SiteError } from '../../lib/site-lib.js'; + +export default class ChatController extends SiteController { + + static get name ( ) { return 'ChatController'; } + static get slug ( ) { return 'chat'; } + + constructor (dtp) { + super(dtp, ChatController); + } + + async start ( ) { + const { + // csrfToken: csrfTokenService, + // limiter: limiterService, + session: sessionService, + } = this.dtp.services; + + const authRequired = sessionService.authCheckMiddleware({ requireLogin: true }); + + const router = express.Router(); + this.dtp.app.use('/chat', router); + + router.use( + async (req, res, next) => { + res.locals.currentView = 'auth'; + return next(); + }, + authRequired, + ); + + router.param('roomId', this.populateRoomId.bind(this)); + + router.post( + '/room', + // limiterService.create(limiterService.config.chat.postCreateRoom), + this.postCreateRoom.bind(this), + ); + + router.get( + '/room/create', + this.getRoomCreateView.bind(this), + ); + + router.get( + '/room/:roomId', + // limiterService.create(limiterService.config.chat.getRoomView), + this.getRoomView.bind(this), + ); + + return router; + } + + async populateRoomId (req, res, next, roomId) { + const { chat: chatService } = this.dtp.services; + try { + res.locals.room = await chatService.getRoomById(roomId); + if (!res.locals.room) { + throw new SiteError(404, "The chat room doesn't exist."); + } + return next(); + } catch (error) { + return next(error); + } + } + + async postCreateRoom (req, res, next) { + const { chat: chatService } = this.dtp.services; + try { + res.locals.room = await chatService.createRoom(req.user, req.body); + res.redirect(`/chat/room/${res.locals.room._id}`); + } catch (error) { + return next(error); + } + } + + async getRoomCreateView (req, res) { + res.render('chat/room/create'); + } + + async getRoomView (req, res) { + res.locals.currentView = 'chat-room'; + res.render('chat/room/view'); + } +} \ No newline at end of file diff --git a/app/controllers/home.js b/app/controllers/home.js index 451f907..b6eb82e 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -32,12 +32,24 @@ export default class HomeController extends SiteController { return router; } - async getHome (req, res) { + async getHome (req, res, next) { + const { chat: chatService } = this.dtp.services; + try { + res.locals.currentView = 'home'; + res.locals.pageDescription = 'DTP Chat Home'; + + res.locals.ownerRooms = await chatService.getRoomsForOwner(req.user); + + res.locals.pagination = this.getPaginationParameters(req, 10); + res.locals.memberRooms = await chatService.getRoomsForMember(req.user, res.locals.pagination); + res.locals.memberRooms = res.locals.memberRooms.filter((room) => !room.owner._id.equals(req.user._id)); + + res.render('home'); + } catch (error) { + return next(error); + } if (!req.user) { return res.redirect('/welcome'); } - res.locals.currentView = 'home'; - res.locals.pageDescription = 'DTP Chat Home'; - res.render('home'); } } \ No newline at end of file diff --git a/app/controllers/user.js b/app/controllers/user.js index 6c4c122..38b3e4d 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -257,12 +257,16 @@ export default class UserController extends SiteController { newUsername: req.body.username, }); - displayList.showNotification( - 'Member account settings updated successfully.', - 'success', - 'bottom-center', - 6000, - ); + if (req.user.ui.theme !== req.body.uiTheme) { + displayList.reloadView(); + } else { + displayList.showNotification( + 'Member account settings updated successfully.', + 'success', + 'bottom-center', + 6000, + ); + } res.status(200).json({ success: true, displayList }); } catch (error) { this.log.error('failed to update account settings', { error }); diff --git a/app/models/chat-message.js b/app/models/chat-message.js index fc7ac7d..34cb04e 100644 --- a/app/models/chat-message.js +++ b/app/models/chat-message.js @@ -7,7 +7,7 @@ import mongoose from 'mongoose'; const Schema = mongoose.Schema; -const CHANNEL_TYPE_LIST = ['ChatRoom']; +const CHANNEL_TYPE_LIST = ['User', 'ChatRoom']; const ChatMessageSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' }, diff --git a/app/models/chat-room.js b/app/models/chat-room.js index f4936ef..475f1ea 100644 --- a/app/models/chat-room.js +++ b/app/models/chat-room.js @@ -11,11 +11,11 @@ const Schema = mongoose.Schema; const ChatRoomSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: 1, expires: '7d' }, + lastActivity: { type: Date, index: -1 }, owner: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, name: { type: String, required: true }, topic: { type: String }, capacity: { type: Number, required: true, min: MIN_ROOM_CAPACITY, max: MAX_ROOM_CAPACITY }, - accessToken: { type: String, required: true }, invites: { type: [Schema.ObjectId], select: false }, members: { type: [Schema.ObjectId], select: false }, banned: { type: [Schema.ObjectId], select: false }, diff --git a/app/models/link.js b/app/models/link.js new file mode 100644 index 0000000..9fae136 --- /dev/null +++ b/app/models/link.js @@ -0,0 +1,79 @@ +// status.js +// Copyright (C) 2024 DTP Technologies, LLC +// All Rights Reserved + +'use strict'; + +import mongoose from 'mongoose'; +const Schema = mongoose.Schema; + +const LinkSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1, }, + lastShared: { type: Date, required: true, index: -1 }, + lastPreviewFetched: { type: Date }, + submittedBy: { type: [Schema.ObjectId], required: true, index: 1, ref: 'User' }, + domain: { type: String, lowercase: true, required: true }, + url: { type: String, required: true }, + title: { type: String }, + siteName: { type: String }, + description: { type: String }, + tags: { type: [String] }, + mediaType: { type: String }, + contentType: { type: String }, + images: { type: [String] }, + videos: { type: [String] }, + audios: { type: [String] }, + favicons: { type: [String] }, + oembed: { + href: { type: String }, + fetched: { type: Date, required: true }, + version: { type: String }, + type: { type: String }, + cache_age: { type: Number }, + title: { type: String }, + provider_name: { type: String }, + provider_url: { type: String }, + author_name: { type: String }, + author_url: { type: String }, + thumbnail_url: { type: String }, + thumbnail_width: { type: String }, + thumbnail_height: { type: String }, + html: { type: String }, + url: { type: String }, + width: { type: String }, + height: { type: String }, + }, + flags: { + isBlocked: { type: Boolean, default: false, required: true }, + havePreview: { type: Boolean, default: false, required: true }, + }, + stats: { + shareCount: { type: Number, default: 1, required: true }, + visitCount: { type: Number, default: 0, required: true }, + }, +}); + +LinkSchema.index({ + domain: 1, + url: 1, +}, { + unique: true, + name: 'link_domain_unique', +}); + +LinkSchema.index({ + title: 'text', + description: 'text', + siteName: 'text', + domain: 'text', +}, { + weights: { + title: 5, + siteName: 3, + domain: 2, + description: 1, + }, + name: 'link_text_idx', +}); + +export default mongoose.model('Link', LinkSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index f454e23..262a1ec 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -45,6 +45,9 @@ const UserSchema = new Schema({ small: { type: Schema.ObjectId, ref: 'Image' }, }, bio: { type: String }, + ui: { + theme: { type: String, default: 'chat-light', required: true }, + }, flags: { type: UserFlagsSchema, default: { }, required: true, select: false }, permissions: { type: UserPermissionsSchema, default: { }, required: true, select: false }, optIn: { type: UserOptInSchema, default: { }, required: true, select: false }, diff --git a/app/services/chat.js b/app/services/chat.js index c1fbf3e..18b1077 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -7,9 +7,11 @@ import mongoose from 'mongoose'; // const AuthToken = mongoose.model('AuthToken'); const ChatRoom = mongoose.model('ChatRoom'); -const ChatRoomInvite = mongoose.model('ChatRoomInvite'); +const ChatMessage = mongoose.model('ChatMessage'); +// const ChatRoomInvite = mongoose.model('ChatRoomInvite'); import { SiteService, SiteError } from '../../lib/site-lib.js'; +import { MAX_ROOM_CAPACITY } from '../models/lib/constants.js'; export default class ChatService extends SiteService { @@ -21,23 +23,103 @@ export default class ChatService extends SiteService { } async start ( ) { - + const { user: userService } = this.dtp.services; + this.populateChatRoom = [ + { + path: 'owner', + select: userService.USER_SELECT, + }, + ]; } async createRoom (owner, roomDefinition) { - const room = new ChatRoom(); + const { text: textService } = this.dtp.services; + const NOW = new Date(); + const room = new ChatRoom(); + room.created = NOW; + room.owner = owner._id; + room.name = textService.filter(roomDefinition.name); + if (roomDefinition.topic) { + room.topic = textService.filter(roomDefinition.topic); + } + room.capacity = MAX_ROOM_CAPACITY; + room.members = [owner._id]; + + await room.save(); + + return room.toObject(); } async destroyRoom (user, room) { - + if (user._id.equals(room.owner._id)) { + throw new SiteError(401, 'This is not your chat room'); + } + await this.removeMessagesForChannel(room); + await ChatRoom.deleteOne({ _id: room._id }); } async joinRoom (room, user) { - + await ChatRoom.updateOne( + { _id: room._id }, + { + $push: { members: user._id }, + }, + ); } async leaveRoom (room, user) { + await ChatRoom.updateOne( + { _id: room._id }, + { + $pull: { members: user._id }, + }, + ); + } + + async getRoomMemberList (room) { + const roomData = await ChatRoom.findOne({ _id: room._id }).select('members'); + if (!roomData) { + throw new SiteError(404, 'Room not found'); + } + return roomData.members; + } + + async getRoomBlockList (room) { + const roomData = await ChatRoom.findOne({ _id: room._id }).select('members'); + if (!roomData) { + throw new SiteError(404, 'Room not found'); + } + return roomData.banned; + } + + async getRoomById (roomId) { + const room = await ChatRoom + .findOne({ _id: roomId }) + .populate(this.populateChatRoom) + .lean(); + return room; + } + + async getRoomsForOwner (owner) { + const rooms = await ChatRoom + .find({ owner: owner._id }) + .populate(this.populateChatRoom) + .lean(); + return rooms; + } + + async getRoomsForMember (member, pagination) { + const rooms = await ChatRoom + .find({ members: member._id }) + .populate(this.populateChatRoom) + .skip(pagination.skip) + .limit(pagination.cpp) + .lean(); + return rooms; + } + async removeMessagesForChannel (channel) { + await ChatMessage.deleteMany({ channel: channel._id }); } } \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index 4def525..59ac230 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -561,6 +561,8 @@ export default class UserService extends SiteService { update.$set['flags.isCloaked'] = settings['flags.isCloaked'] === 'on'; } + update.$set['ui.theme'] = settings.uiTheme; + await User.updateOne({ _id: user._id }, update); /* diff --git a/app/views/chat/room/create.pug b/app/views/chat/room/create.pug new file mode 100644 index 0000000..1975588 --- /dev/null +++ b/app/views/chat/room/create.pug @@ -0,0 +1,21 @@ +extends ../../layout/main +block view-content + + .uk-section.uk-section-default + .uk-container + + form(method="POST", action="/chat/room").uk-form + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title Create Room + + .uk-card-body + .uk-margin + label(for="name") Room Name + input(id="name", name="name", type="text", placeholder="Enter room name").uk-input + .uk-margin + label(for="topic") Topic + input(id="topic", name="topic", type="text", placeholder="Enter room topic or leave blank").uk-input + + .uk-card-footer.uk-flex.uk-flex-right + button(type="submit").uk-button.uk-button-default.uk-border-rounded Create Room \ No newline at end of file diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug new file mode 100644 index 0000000..f8f4d42 --- /dev/null +++ b/app/views/chat/room/view.pug @@ -0,0 +1,41 @@ +extends ../../layout/main +block view-content + + .dtp-chat-stage + .chat-sidebar + .chat-stage-header Active Members + .sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status. + + .chat-stage-header Idle Members + .sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status. + + .chat-container + .chat-stage-header + div(uk-grid) + .uk-width-expand= room.name + + + .chat-content-panel + .chat-media + div(uk-grid) + .uk-width-1-4 + .live-member + video(src="/static/video/gdl-crush.mp4", autoplay, muted, loop, disablepictureinpicture, disableremoteplayback) + .uk-flex.live-meta + .live-username.uk-width-expand Rob Colbert + .uk-width-auto + i.fas.fa-volume-off + .uk-width-auto + .uk-margin-small-left + i.fas.fa-cog + + .chat-messages 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. + + .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 diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index d872b19..98ff96e 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -1,5 +1,5 @@ nav(style="background: #000000;").uk-navbar-container.uk-light - .uk-container.uk-container-expand + .dtp-navbar-container div(uk-navbar) .uk-navbar-left a(href="/", aria-label="Back to Home").uk-navbar-item.uk-logo.uk-padding-remove-left diff --git a/app/views/home.pug b/app/views/home.pug index 34400ae..88a4615 100644 --- a/app/views/home.pug +++ b/app/views/home.pug @@ -1,15 +1,38 @@ extends layout/main block view-content - .dtp-chat-stage - .chat-sidebar - .chat-stage-header Active Members - .sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status. - - .chat-stage-header Idle Members - .sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status. - - .chat-container - .chat-stage-header Chat Room and Host names go here. - .chat-content-panel 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. - .chat-input-panel This is the chat input panel. It will be where text is entered and sent, and contain some menu items, icons, and buttons. \ No newline at end of file + 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)' + + section.uk-section.uk-section-default + .uk-container + + .uk-margin-medium + div(uk-grid) + .uk-width-expand + .uk-text-lead YOUR ROOMS + .uk-width-auto + a(href="/chat/room/create").uk-button.uk-button-default.uk-border-rounded + span + i.fas.fa-plus + span.uk-margin-small-left Create Room + + if (Array.isArray(ownerRooms) && (ownerRooms.length > 0)) + ul.uk-list + each room in ownerRooms + li.uk-list-divider + +renderRoomListEntry(room) + else + p You don't own any rooms. + + .uk-margin-medium + .uk-text-lead ROOM MEMBERSHIPS + if (Array.isArray(memberRooms) && (memberRooms.length > 0)) + ul.uk-list + each room in memberRooms + li.uk-list-divider + +renderRoomListEntry(room) + else + p You haven't joined any rooms that you don't own. \ No newline at end of file diff --git a/app/views/layout/main.pug b/app/views/layout/main.pug index 79166dc..25cf0df 100644 --- a/app/views/layout/main.pug +++ b/app/views/layout/main.pug @@ -19,7 +19,7 @@ html(lang='en', data-obs-widget= obsWidget) block vendorcss - link(rel='stylesheet', href=`/dist/chat-light.css?v=${pkg.version}`) + link(rel='stylesheet', href=`/dist/${(user) ? user.ui.theme : 'chat-light'}.css?v=${pkg.version}`) link(rel='stylesheet', href=`/pretty-checkbox/pretty-checkbox.min.css?v=${pkg.version}`) block viewcss diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 2398360..1863c32 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -9,143 +9,134 @@ block view-content section.uk-section.uk-section-default .uk-container - h1 Settings - - - - var tabNames = { - "account": 0, - }; + div(uk-grid) + div(class="uk-width-1-1 uk-width-1-3@m") + - + var currentImage = null; + if (user.picture && user.picture.large) { + currentImage = user.picture.large; + } - ul(uk-tab={ active: tabNames[startTab], animation: false }) - li - a Account + .uk-margin + +renderFileUploadImage( + `/user/${user._id}/profile-photo`, + 'profile-picture-upload', + 'profile-picture-file', + 'streamray-profile-picture', + `/img/default-member.png`, + currentImage, + { aspectRatio: 1 }, + ) + + .uk-margin.uk-text-center + if hasOtpAccount + a(href=`/user/${user._id}/otp-disable`).uk-button.uk-button-danger.uk-border-rounded Disable 2FA + else + a(href=`/user/${user._id}/otp-setup`).uk-button.uk-button-secondary.uk-border-rounded Enable 2FA + + div(class="uk-width-1-1 uk-width-expand@m") + form( + method="POST", + action=`/user/${user._id}/settings`, + autocomplete= "off", + onsubmit="return dtp.app.submitForm(event, 'user account update');", + ).uk-form + input(type="hidden", name= csrfTokenAccountSettings.name, value= csrfTokenAccountSettings.token) - ul.uk-switcher - //- User account and billing - li - div(uk-grid) - div(class="uk-width-1-1 uk-width-1-3@m") - - - var currentImage = null; - if (user.picture && user.picture.large) { - currentImage = user.picture.large; - } + ul(uk-tab, uk-switcher={ connect: '#account-settings-tabs'}) + li + a(href) Profile + li + a(href) Password + li + a(href) Email + if user.flags && user.flags.isModerator + li + a(href) Moderator - .uk-margin - +renderFileUploadImage( - `/user/${user._id}/profile-photo`, - 'profile-picture-upload', - 'profile-picture-file', - 'streamray-profile-picture', - `/img/default-member.png`, - currentImage, - { aspectRatio: 1 }, - ) - - .uk-margin.uk-text-center - if hasOtpAccount - a(href=`/user/${user._id}/otp-disable`).uk-button.uk-button-danger.uk-border-rounded Disable 2FA - else - a(href=`/user/${user._id}/otp-setup`).uk-button.uk-button-secondary.uk-border-rounded Enable 2FA - - div(class="uk-width-1-1 uk-width-expand@m") - form( - method="POST", - action=`/user/${user._id}/settings`, - autocomplete= "off", - onsubmit="return dtp.app.submitForm(event, 'user account update');", - ).uk-form - input(type="hidden", name= csrfTokenAccountSettings.name, value= csrfTokenAccountSettings.token) + ul#account-settings-tabs.uk-switcher + li + fieldset + legend Profile + .uk-margin + label(for="username").uk-form-label Username + input(id="username", name="username", type="text", placeholder="Enter username", value= userProfile.username).uk-input + .uk-margin + label(for="display-name").uk-form-label Display Name + input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= userProfile.displayName).uk-input + .uk-margin + label(for="bio").uk-form-label Bio + textarea(id="bio", name="bio", rows="4", placeholder="Enter profile bio").uk-textarea.uk-resize-vertical= userProfile.bio + .uk-margin + label(for="ui-theme").uk-form-label UI Theme + select(id="ui-theme", name="uiTheme").uk-select + option(value="chat-light", selected= (user.ui.theme === 'chat-light')) Light + option(value="chat-dark", selected= (user.ui.theme === 'chat-dark')) Dark - ul(uk-tab, uk-switcher={ connect: '#account-settings-tabs'}) - li - a(href) Profile - li - a(href) Password - li - a(href) Email - if user.flags && user.flags.isModerator - li - a(href) Moderator - - ul#account-settings-tabs.uk-switcher - li - fieldset - legend Profile - .uk-margin - label(for="username").uk-form-label Username - input(id="username", name="username", type="text", placeholder="Enter username", value= userProfile.username).uk-input - .uk-margin - label(for="display-name").uk-form-label Display Name - input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= userProfile.displayName).uk-input - .uk-margin - label(for="bio").uk-form-label Bio - textarea(id="bio", name="bio", rows="4", placeholder="Enter profile bio").uk-textarea.uk-resize-vertical= userProfile.bio - - li - fieldset - legend Password - .uk-margin - div(uk-grid).uk-grid-small - .uk-width-1-2 - .uk-margin - label(for="password").uk-form-label New Password - input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input - .uk-width-1-2 - .uk-margin - label(for="passwordv").uk-form-label Verify New Password - input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input + li + fieldset + legend Password + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-1-2 + .uk-margin + label(for="password").uk-form-label New Password + input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input + .uk-width-1-2 + .uk-margin + label(for="passwordv").uk-form-label Verify New Password + input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input - li - fieldset - legend Email Preferences + li + fieldset + legend Email Preferences - .uk-margin - label(for="email").uk-form-label - span Email Address - if user.flags.isEmailVerified - span (verified) - div(uk-grid).uk-grid-small - div(class="uk-width-1-1 uk-width-expand@s") - .uk-margin-small - input(id="email", name="email", type="email", placeholder="Enter email address", value= userProfile.email).uk-input - if user.flags.isEmailVerified - .uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder! - else - .uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder! - div(class="uk-width-1-1 uk-width-auto@s") - button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.uk-button-secondary.uk-border-rounded Resend Welcome Email + .uk-margin + label(for="email").uk-form-label + span Email Address + if user.flags.isEmailVerified + span (verified) + div(uk-grid).uk-grid-small + div(class="uk-width-1-1 uk-width-expand@s") + .uk-margin-small + input(id="email", name="email", type="email", placeholder="Enter email address", value= userProfile.email).uk-input + if user.flags.isEmailVerified + .uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder! + else + .uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder! + div(class="uk-width-1-1 uk-width-auto@s") + button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.uk-button-secondary.uk-border-rounded Resend Welcome Email - .uk-margin - div(uk-grid).uk-grid-small - .uk-width-auto - .pretty.p-switch.p-slim - input(id="optin-system", name="optIn.system", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.system : false) - .state.p-success - label(for="optin-system") System Messages - .uk-width-auto - .pretty.p-switch.p-slim - input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.marketing : false) - .state.p-success - label(for="optin-marketing") Newsletter - .uk-width-auto - .pretty.p-switch.p-slim - input(id="email-verified", type="checkbox", checked= userProfile.flags ? userProfile.flags.isEmailVerified : false, disabled) - .state.p-success - label(for="email-verified") Email Verified + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-auto + .pretty.p-switch.p-slim + input(id="optin-system", name="optIn.system", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.system : false) + .state.p-success + label(for="optin-system") System Messages + .uk-width-auto + .pretty.p-switch.p-slim + input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.marketing : false) + .state.p-success + label(for="optin-marketing") Newsletter + .uk-width-auto + .pretty.p-switch.p-slim + input(id="email-verified", type="checkbox", checked= userProfile.flags ? userProfile.flags.isEmailVerified : false, disabled) + .state.p-success + label(for="email-verified") Email Verified - if user.flags && user.flags.isModerator - li - fieldset - legend Moderator Preferences - .uk-margin - .pretty.p-switch.p-slim - input(id="moderator-cloaked", name="flags.isCloaked", type="checkbox", checked= userProfile.flags ? userProfile.flags.isCloaked : false) - .state.p-success - label(for="moderator-cloaked") Enable Ghost Mode + if user.flags && user.flags.isModerator + li + fieldset + legend Moderator Preferences + .uk-margin + .pretty.p-switch.p-slim + input(id="moderator-cloaked", name="flags.isCloaked", type="checkbox", checked= userProfile.flags ? userProfile.flags.isCloaked : false) + .state.p-success + label(for="moderator-cloaked") Enable Ghost Mode - .uk-margin - button(type="submit").uk-button.uk-button-primary.uk-border-rounded Update account settings + .uk-margin + button(type="submit").uk-button.uk-button-primary.uk-border-rounded Update account settings block viewjs script. diff --git a/client/css/dtp-site.less b/client/css/dtp-site.less index aa6126a..780ef8c 100644 --- a/client/css/dtp-site.less +++ b/client/css/dtp-site.less @@ -1,3 +1,4 @@ @import "site/main.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 new file mode 100644 index 0000000..e69de29 diff --git a/client/css/site/main.less b/client/css/site/main.less index 3f6eac7..0ad75c5 100644 --- a/client/css/site/main.less +++ b/client/css/site/main.less @@ -1,8 +1,18 @@ html, body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - &[data-current-view="home"] { + &[data-current-view="chat-room"] { position: fixed; top: 0; right: 0; bottom: 0; left: 0; } +} + +.no-select { + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.uk-resize-none { + resize: none; } \ No newline at end of file diff --git a/client/css/site/navbar.less b/client/css/site/navbar.less new file mode 100644 index 0000000..8cae6f2 --- /dev/null +++ b/client/css/site/navbar.less @@ -0,0 +1,4 @@ +.dtp-navbar-container { + padding-left: 10px; + padding-right: 10px; +} \ No newline at end of file diff --git a/client/css/site/stage.less b/client/css/site/stage.less index 9ef57b1..1239aba 100644 --- a/client/css/site/stage.less +++ b/client/css/site/stage.less @@ -5,9 +5,6 @@ position: absolute; top: @site-navbar-height; right: 0; bottom: 0; left: 0; - width: 100%; - height: 100%; - display: flex; .chat-stage-header { @@ -41,6 +38,7 @@ } .chat-container { + box-sizing: border-box; display: flex; flex-grow: 1; flex-direction: column; @@ -49,11 +47,56 @@ color: #1a1a1a; .chat-content-panel { - padding: @stage-panel-padding; + box-sizing: border-box; flex-grow: 1; + display: flex; + + .chat-media { + box-sizing: border-box; + flex-grow: 1; + flex-shrink: 1; + + height: 100%; + padding: @stage-panel-padding; + + background-color: #4a4a4a; + + .live-member { + padding: 4px 4px 0 4px; + + border: solid 1px #e8e8e8; + border-radius: 4px; + + background-color: #1a1a1a; + + video { + display: block; + } + + .live-meta { + color: #e8e8e8; + + .live-username { + color: #00ff00; + } + } + } + } + + .chat-messages { + box-sizing: border-box; + flex-grow: 0; + flex-shrink: 0; + + width: 320px; + height: 100%; + padding: @stage-panel-padding; + + overflow-y: scroll; + } } + .chat-input-panel { - height: 200px; padding: @stage-panel-padding; background-color: #1a1a1a; color: #e8e8e8; diff --git a/client/js/chat-client.js b/client/js/chat-client.js index 645c0c5..01ff773 100644 --- a/client/js/chat-client.js +++ b/client/js/chat-client.js @@ -18,6 +18,13 @@ export class ChatApp extends DtpApp { super(DTP_COMPONENT_NAME, user); this.loadSettings(); this.log.info('DTP app client online'); + + window.addEventListener('dtp-load', this.onDtpLoad.bind(this)); + } + + async onDtpLoad ( ) { + this.log.info('dtp-load event received. Connecting to platform.'); + await this.connect(); } async confirmNavigation (event) { @@ -122,25 +129,6 @@ export class ChatApp extends DtpApp { }); } - loadSettings ( ) { - this.settings = { tutorials: { } }; - if (window.localStorage) { - if (window.localStorage.settings) { - this.settings = JSON.parse(window.localStorage.settings); - } else { - this.saveSettings(); - } - this.mutedUsers = window.localStorage.mutedUsers ? JSON.parse(window.localStorage.mutedUsers) : [ ]; - this.filterChatView(); - } - this.settings.tutorials = this.settings.tutorials || { }; - } - - saveSettings ( ) { - if (!window.localStorage) { return; } - window.localStorage.settings = JSON.stringify(this.settings); - } - async initSettingsView ( ) { this.log.info('initSettingsView', 'settings', { settings: this.settings }); @@ -181,6 +169,25 @@ export class ChatApp extends DtpApp { } } + loadSettings ( ) { + this.settings = { tutorials: { } }; + if (window.localStorage) { + if (window.localStorage.settings) { + this.settings = JSON.parse(window.localStorage.settings); + } else { + this.saveSettings(); + } + this.mutedUsers = window.localStorage.mutedUsers ? JSON.parse(window.localStorage.mutedUsers) : [ ]; + this.filterChatView(); + } + this.settings.tutorials = this.settings.tutorials || { }; + } + + saveSettings ( ) { + if (!window.localStorage) { return; } + window.localStorage.settings = JSON.stringify(this.settings); + } + async selectImageFile (event) { event.preventDefault(); diff --git a/config/limiter.js b/config/limiter.js index d56610f..bb6504e 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -151,7 +151,7 @@ export default { message: 'You are updating your profile photo too quickly', }, postUpdateSettings: { - total: 6, + total: 12, expire: ONE_MINUTE, message: 'You are updating account settings too quickly', }, diff --git a/lib/site-ioserver.js b/lib/site-ioserver.js index b5a8d40..0b6595b 100644 --- a/lib/site-ioserver.js +++ b/lib/site-ioserver.js @@ -7,6 +7,7 @@ import { createAdapter } from '@socket.io/redis-adapter'; import mongoose from 'mongoose'; +const { ObjectId } = mongoose.Types; const ConnectToken = mongoose.model('ConnectToken'); const Image = mongoose.model('Image'); // jshint ignore:line @@ -329,7 +330,7 @@ export class SiteIoServer extends SiteCommon { const channelId = message.channelId; const channelComponents = channelId.split(':'); - const parsedChannelId = mongoose.Types.ObjectId((channelComponents[0] === 'broadcast') ? channelComponents[1] : channelComponents[0]); + const parsedChannelId = ObjectId.createFromHexString((channelComponents[0] === 'broadcast') ? channelComponents[1] : channelComponents[0]); if (parsedChannelId.equals(session.user._id)) { this.log.debug('user joins their own channel', { @@ -355,7 +356,7 @@ export class SiteIoServer extends SiteCommon { const { channelId } = message; const channelComponents = channelId.split(':'); - const parsedChannelId = mongoose.Types.ObjectId((channelComponents[0] === 'broadcast') ? channelComponents[1] : channelComponents[0]); + const parsedChannelId = ObjectId.createFromHexString((channelComponents[0] === 'broadcast') ? channelComponents[1] : channelComponents[0]); session.channel = await channelService.getChannelById(parsedChannelId, { withPasscode: true }); if (!session.channel) { return; } @@ -431,7 +432,7 @@ export class SiteIoServer extends SiteCommon { } try { - const roomId = mongoose.Types.ObjectId(message.channelId); + const roomId = ObjectId.createFromHexString(message.channelId); const room = await chatService.getRoomById(roomId); if (!room) { session.socket.emit('join-result', { authResult: 'room-invalid', message: 'The chat room does not exist' }); @@ -469,7 +470,7 @@ export class SiteIoServer extends SiteCommon { } try { - const threadId = mongoose.Types.ObjectId(message.channelId); + const threadId = ObjectId.createFromHexString(message.channelId); const thread = await chatService.getPrivateMessageById(threadId); if (!thread) { session.socket.emit('join-result', { authResult: 'room-invalid', message: 'The chat room does not exist' });