diff --git a/app/controllers/chat.js b/app/controllers/chat.js index e675b4a..8290275 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -168,16 +168,11 @@ class ChatController extends SiteController { switch (response) { case 'accept': await chatService.acceptRoomInvite(res.locals.invite); - displayList.showNotification( - `Chat room invite accepted`, - 'success', - 'top-center', - 5000, - ); + displayList.navigateTo(`/chat/room/${res.locals.invite.room._id}`); break; case 'reject': - await chatService.acceptRoomInvite(res.locals.invite); + await chatService.rejectRoomInvite(res.locals.invite); displayList.showNotification( `Chat room invite rejected`, 'success', diff --git a/app/controllers/home.js b/app/controllers/home.js index ada8647..b19484f 100644 --- a/app/controllers/home.js +++ b/app/controllers/home.js @@ -64,10 +64,11 @@ class HomeController extends SiteController { } async getHome (req, res, next) { - const { announcement: announcementService, post: postService } = this.dtp.services; + const { announcement: announcementService, hive: hiveService, post: postService } = this.dtp.services; try { res.locals.announcements = await announcementService.getLatest(req.user); res.locals.featuredPosts = await postService.getFeaturedPosts(3); + res.locals.constellationTimeline = await hiveService.getConstellationTimeline(req.user, { skip: 0, cpp: 5 }); res.locals.pagination = this.getPaginationParameters(req, 20); res.locals.posts = await postService.getPosts(res.locals.pagination); diff --git a/app/models/kaleidoscope-event.js b/app/models/kaleidoscope-event.js index ad02b90..c24e98e 100644 --- a/app/models/kaleidoscope-event.js +++ b/app/models/kaleidoscope-event.js @@ -7,10 +7,13 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; +const RECIPIENT_TYPE_LIST = ['User','CoreUser']; const EMITTER_TYPE_LIST = ['User','CoreUser','OAuth2Client']; const KaleidoscopeEventSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' }, + recipientType: { type: String, enum: RECIPIENT_TYPE_LIST }, + recipient: { type: Schema.ObjectId, index: 1, refPath: 'recipientType' }, action: { type: String, required: true, lowercase: true }, label: { type: String }, content: { type: String }, diff --git a/app/services/chat.js b/app/services/chat.js index 6194919..5bdcc56 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -265,6 +265,7 @@ class ChatService extends SiteService { skip: 0, cpp: 50 }, pagination); + const totalPublicRooms = await ChatRoom.countDocuments({ visibility: 'public' }); const rooms = await ChatRoom .find({ visibility: 'public' }) .sort({ lastActivity: -1, created: -1 }) @@ -272,7 +273,7 @@ class ChatService extends SiteService { .limit(pagination.cpp) .populate(this.populateChatRoom) .lean(); - return rooms; + return { rooms, totalPublicRooms }; } async getRoomById (roomId) { @@ -367,6 +368,8 @@ class ChatService extends SiteService { room.owner.type = room.ownerType; const event = { + recipientType: member.type, + recipient: member._id, action: 'room-invite-create', emitter: room.owner, label: 'Chat Room Invitation', @@ -396,6 +399,19 @@ class ChatService extends SiteService { } async acceptRoomInvite (invite) { + if ((invite.status === 'accepted') || + (invite.room.members.find((member) => member.member._id.equals(invite.member._id)))) { + throw SiteError(400, "You have already accepted membership in this room."); + } + + this.log.debug('updating chat invite', { inviteId: invite._id, status: 'accepted' }); + await ChatRoomInvite.updateOne( + { _id: invite._id }, + { + $set: { status: 'accepted' }, + }, + ); + this.log.info('accepting invite to chat room', { roomId: invite.room._id, memberId: invite.member._id, @@ -403,7 +419,7 @@ class ChatService extends SiteService { await ChatRoom.updateOne( { _id: invite.room._id }, { - $addToSet: { + $push: { members: { memberType: invite.memberType, member: invite.member._id, @@ -411,14 +427,6 @@ class ChatService extends SiteService { }, }, ); - - this.log.info('updating chat invite', { inviteId: invite._id, status: 'accepted' }); - await ChatRoomInvite.updateOne( - { _id: invite._id }, - { - $set: { status: 'accepted' }, - }, - ); } async rejectRoomInvite (invite) { diff --git a/app/services/hive.js b/app/services/hive.js index 4e517c9..3b3007d 100644 --- a/app/services/hive.js +++ b/app/services/hive.js @@ -188,6 +188,11 @@ class HiveService extends SiteService { const event = new KaleidoscopeEvent(); event.created = NOW; + if (eventDefinition.recipientType && eventDefinition.recipient) { + event.recipientType = eventDefinition.recipientType; + event.recipient = mongoose.Types.ObjectId(eventDefinition.recipient); + } + event.action = striptags(eventDefinition.action.trim().toLowerCase()); if (eventDefinition.label) { event.label = striptags(eventDefinition.label.trim()); @@ -246,6 +251,28 @@ class HiveService extends SiteService { return event.toObject(); } + + async getConstellationTimeline (user, pagination) { + const totalEventCount = await KaleidoscopeEvent.estimatedDocumentCount(); + const job = { }; + if (user) { + job.search = { + $or: [ + { recipient: { $exists: false } }, + { recipient: user._id }, + ], + }; + } else { + job.search = { recipient: { $exists: false } }; + } + const events = await KaleidoscopeEvent + .find(job.search) + .sort({ created: -1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .lean(); + return { events, totalEventCount }; + } } module.exports = { diff --git a/app/services/user.js b/app/services/user.js index b0cb474..d697f0f 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -277,26 +277,38 @@ class UserService extends SiteService { } async updateSettings (user, userDefinition) { + const { crypto: cryptoService } = this.dtp.services; + + const updateOp = { $set: { }, $unset: { } }; + // strip characters we don't want to allow in username - userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '')); - const username_lc = userDefinition.username.toLowerCase(); + updateOp.$set.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '')); + if (!updateOp.$set.username || (updateOp.$set.username.length === 0)) { + throw new SiteError(400, 'Must include a username'); + } + updateOp.$set.username_lc = updateOp.$set.username.toLowerCase(); - userDefinition.displayName = striptags(userDefinition.displayName.trim()); - userDefinition.bio = striptags(userDefinition.bio.trim()); + if (userDefinition.displayName && (userDefinition.displayName.length > 0)) { + updateOp.$set.displayName = striptags(userDefinition.displayName.trim()); + } else { + updateOp.$unset.displayName = 1; + } - this.log.info('updating user settings', { userDefinition }); - await User.updateOne( - { _id: user._id }, - { - $set: { - username: userDefinition.username, - username_lc, - displayName: userDefinition.displayName, - bio: userDefinition.bio, - theme: userDefinition.theme || 'dtp-light', - }, - }, - ); + if (userDefinition.bio && (userDefinition.bio.length > 0)) { + updateOp.$set.bio = striptags(userDefinition.bio.trim()); + } else { + updateOp.$unset.bio = 1; + } + + if (userDefinition.password && userDefinition.password.length > 0) { + updateOp.$set.passwordSalt = uuidv4(); + updateOp.$set.password = cryptoService.maskPassword(updateOp.$set.passwordSalt, userDefinition.password); + } + + updateOp.$set.theme = userDefinition.theme || 'dtp-light'; + + this.log.info('updating user settings', { userId: user._id }); + await User.updateOne({ _id: user._id }, updateOp); } async authenticate (account, options) { diff --git a/app/views/chat/index.pug b/app/views/chat/index.pug index b51e8d6..48c1653 100644 --- a/app/views/chat/index.pug +++ b/app/views/chat/index.pug @@ -5,11 +5,13 @@ block content #site-chat-container.uk-flex.uk-flex-column.uk-height-1-1 .chat-menubar.uk-padding-small - div(uk-grid).uk-grid-small + div(uk-grid).uk-grid-small.uk-flex-middle .uk-width-auto img(src=`/img/icon/${site.domainKey}/icon-48x48.png`, alt=`${site.name} icon`) .uk-width-expand h1.uk-margin-remove #{site.name} Chat Timeline + .uk-width-auto + a(href='/chat/room').uk-button.uk-button-secondary.uk-button-small.uk-border-rounded Public Rooms .chat-content-wrapper #chat-message-list-wrapper.uk-height-1-1 @@ -17,6 +19,4 @@ block content each message in timeline +renderChatMessage(message, { includeRoomInfo: true }) .chat-message-menu - button(type="button", onclick="return dtp.app.chat.resumeChatScroll(event);").chat-scroll-return Resume scrolling - - //- pre= JSON.stringify(userTimeline, null, 2) \ No newline at end of file + button(type="button", onclick="return dtp.app.chat.resumeChatScroll(event);").chat-scroll-return Resume scrolling \ No newline at end of file diff --git a/app/views/chat/room/index.pug b/app/views/chat/room/index.pug index 0711e78..961f67d 100644 --- a/app/views/chat/room/index.pug +++ b/app/views/chat/room/index.pug @@ -1,29 +1,40 @@ extends ../layouts/room block content + include ../../components/pagination-bar + mixin renderRoomTile (room) - div(data-room-id= room._id, data-room-name= room.name).uk-tile.uk-tile-default.uk-tile-small - .uk-tile-body - div(uk-grid).uk-grid-small - .uk-width-auto - .uk-width-expand - .uk-margin-small - div(title= room.name).uk-text-bold.uk-text-truncate= room.name - .uk-text-small.uk-text-truncate= room.description - div(uk-grid).uk-grid-small.uk-text-small.uk-text-muted.no-select - .uk-width-expand - a(href= getUserProfileUrl(room.owner))= room.owner.username - .uk-width-auto - span - i.fas.fa-users - span.uk-margin-small-left= formatCount(room.members.length) + a(href=`/chat/room/${room._id}`).uk-display-block.uk-link-reset + div(data-room-id= room._id, data-room-name= room.name).uk-tile.uk-tile-default.uk-tile-small + .uk-tile-body + div(uk-grid).uk-grid-small + .uk-width-auto + +renderProfileIcon(room.owner) + .uk-width-expand + .uk-margin-small + div(title= room.name).uk-text-bold.uk-text-truncate= room.name + .uk-text-small.uk-text-truncate= room.description + div(uk-grid).uk-grid-small.uk-text-small.uk-text-muted.no-select + .uk-width-expand + a(href= getUserProfileUrl(room.owner))= room.owner.username + .uk-width-auto + span + i.fas.fa-users + span.uk-margin-small-left= formatCount(room.members.length) .uk-height-1-1.uk-overflow-auto + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title Public Chat Rooms - h1 Public Rooms - div(uk-grid) - each room in publicRooms - .uk-width-1-3 - +renderRoomTile(room) + .uk-card-body + if Array.isArray(publicRooms.rooms) && (publicRooms.rooms.length > 0) + div(uk-grid) + each room in publicRooms.rooms + div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@l") + +renderRoomTile(room) + else + div #{site.name} has no public rooms. - pre= JSON.stringify(publicRooms, null, 2) \ No newline at end of file + .uk-card-footer + +renderPaginationBar('/chat/room', publicRooms.totalRoomCount) \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index a2f2db6..bdbd20a 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -16,6 +16,9 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top li(class={ 'uk-active': currentView === 'home' }) a(href="/", title= "Home") +renderButtonIcon('fa-home', 'Home') + li(class={ 'uk-active': currentView === 'chat' }) + a(href="/chat", title= "chat") + +renderButtonIcon('fa-comment-alt', 'Chat') if site.shingWidgetKey && site.shingChannelSlug li(class={ 'uk-active': currentView === 'venue' })