Browse Source

wrote more code

develop
Rob Colbert 1 year ago
parent
commit
1383853921
  1. 93
      app/controllers/chat.js
  2. 20
      app/controllers/home.js
  3. 4
      app/controllers/user.js
  4. 2
      app/models/chat-message.js
  5. 2
      app/models/chat-room.js
  6. 79
      app/models/link.js
  7. 3
      app/models/user.js
  8. 90
      app/services/chat.js
  9. 2
      app/services/user.js
  10. 21
      app/views/chat/room/create.pug
  11. 41
      app/views/chat/room/view.pug
  12. 2
      app/views/components/navbar.pug
  13. 47
      app/views/home.pug
  14. 2
      app/views/layout/main.pug
  15. 19
      app/views/user/settings.pug
  16. 1
      client/css/dtp-site.less
  17. 0
      client/css/site/button.less
  18. 12
      client/css/site/main.less
  19. 4
      client/css/site/navbar.less
  20. 53
      client/css/site/stage.less
  21. 45
      client/js/chat-client.js
  22. 2
      config/limiter.js
  23. 9
      lib/site-ioserver.js

93
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');
}
}

20
app/controllers/home.js

@ -32,12 +32,24 @@ export default class HomeController extends SiteController {
return router;
}
async getHome (req, res) {
if (!req.user) {
return res.redirect('/welcome');
}
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');
}
}
}

4
app/controllers/user.js

@ -257,12 +257,16 @@ export default class UserController extends SiteController {
newUsername: req.body.username,
});
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 });

2
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' },

2
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 },

79
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);

3
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 },

90
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 { 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 });
}
}

2
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);
/*

21
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

41
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

2
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

47
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.
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.

2
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

19
app/views/user/settings.pug

@ -9,20 +9,6 @@ block view-content
section.uk-section.uk-section-default
.uk-container
h1 Settings
-
var tabNames = {
"account": 0,
};
ul(uk-tab={ active: tabNames[startTab], animation: false })
li
a Account
ul.uk-switcher
//- User account and billing
li
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@m")
-
@ -81,6 +67,11 @@ block view-content
.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
li
fieldset

1
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";

0
client/css/site/button.less

12
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;
}

4
client/css/site/navbar.less

@ -0,0 +1,4 @@
.dtp-navbar-container {
padding-left: 10px;
padding-right: 10px;
}

53
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;

45
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();

2
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',
},

9
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' });

Loading…
Cancel
Save