Browse Source

large changelist

- Comment composer
- Comment renderer
- Comment create service
- Display list and display engine enhancements
- Added emoji picker for comments
- Admin for site settings
- added main-sidebar layout
develop
Rob Colbert 2 years ago
parent
commit
d534b7950e
  1. 1
      app/controllers/admin.js
  2. 56
      app/controllers/admin/settings.js
  3. 46
      app/controllers/post.js
  4. 22
      app/models/comment.js
  5. 173
      app/services/comment.js
  6. 14
      app/services/display-engine.js
  7. 2
      app/services/gab-tv.js
  8. 4
      app/views/admin/components/menu.pug
  9. 10
      app/views/admin/post/editor.pug
  10. 15
      app/views/admin/settings/editor.pug
  11. 3
      app/views/comment/components/comment-standalone.pug
  12. 47
      app/views/comment/components/comment.pug
  13. 1
      app/views/components/library.pug
  14. 10
      app/views/components/navbar.pug
  15. 39
      app/views/components/page-sidebar.pug
  16. 8
      app/views/components/section-title.pug
  17. 69
      app/views/index.pug
  18. 13
      app/views/layouts/main-sidebar.pug
  19. 3
      app/views/layouts/main.pug
  20. 27
      app/views/page/view.pug
  21. 117
      app/views/post/view.pug
  22. 42
      app/views/welcome/login.pug
  23. 7
      app/views/welcome/signup.pug
  24. 22
      client/js/site-app.js
  25. 3
      client/less/site/image.less
  26. 19
      lib/client/js/dtp-display-engine.js
  27. 5
      lib/site-platform.js
  28. 1
      package.json
  29. 122
      yarn.lock

1
app/controllers/admin.js

@ -49,6 +49,7 @@ class AdminController extends SiteController {
router.use('/newsletter',await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/page',await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post',await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings',await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));
router.get('/', this.getHomeView.bind(this));

56
app/controllers/admin/settings.js

@ -0,0 +1,56 @@
// admin/settings.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'admin:settings';
const express = require('express');
const { SiteController } = require('../../../lib/site-lib');
class SettingsController extends SiteController {
constructor (dtp) {
super(dtp, DTP_COMPONENT_NAME);
}
async start ( ) {
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'settings';
return next();
});
router.post('/', this.postUpdateSettings.bind(this));
router.get('/', this.getSettingsView.bind(this));
return router;
}
async postUpdateSettings (req, res, next) {
const { cache: cacheService } = this.dtp.services;
try {
const settingsKey = `settings:${this.dtp.config.site.domainKey}:site`;
await cacheService.setObject(settingsKey, req.body);
res.redirect('/admin/settings');
} catch (error) {
return next(error);
}
}
async getSettingsView (req, res, next) {
try {
res.render('admin/settings/editor');
} catch (error) {
return next(error);
}
}
}
module.exports = async (dtp) => {
let controller = new SettingsController(dtp);
return controller;
};

46
app/controllers/post.js

@ -7,6 +7,7 @@
const DTP_COMPONENT_NAME = 'post';
const express = require('express');
const multer = require('multer');
const { SiteController, SiteError } = require('../../lib/site-lib');
@ -20,6 +21,8 @@ class PostController extends SiteController {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const upload = multer({ dest: `/tmp/dtp-sites/${this.dtp.config.site.domainKey}`});
const router = express.Router();
dtp.app.use('/post', router);
@ -31,6 +34,8 @@ class PostController extends SiteController {
router.param('postSlug', this.populatePostSlug.bind(this));
router.post('/:postSlug/comment', upload.none(), this.postComment.bind(this));
router.get('/:postSlug',
limiterService.create(limiterService.config.post.getView),
this.getView.bind(this),
@ -56,10 +61,49 @@ class PostController extends SiteController {
}
}
async postComment (req, res) {
const {
comment: commentService,
displayEngine: displayEngineService,
} = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('add-recipient');
res.locals.comment = await commentService.create(req.user, 'Post', res.locals.post, req.body);
displayList.setInputValue('textarea#content', '');
displayList.setTextContent('#comment-character-count', '0');
let viewModel = Object.assign({ }, req.app.locals);
viewModel = Object.assign(viewModel, res.locals);
const html = await commentService.templates.comment(viewModel);
displayList.addElement('ul#post-comment-list', 'afterBegin', html);
displayList.showNotification(
'Comment created',
'success',
'bottom-center',
4000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
res.status(error.statusCode || 500).json({ success: false, message: error.message });
}
}
async getView (req, res, next) {
const { resource: resourceService } = this.dtp.services;
const { comment: commentService, resource: resourceService } = this.dtp.services;
try {
await resourceService.recordView(req, 'Post', res.locals.post._id);
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.comments = await commentService.getForResource(
res.locals.post,
['published', 'mod-warn'],
res.locals.pagination,
);
res.render('post/view');
} catch (error) {
this.log.error('failed to service post view', { postId: res.locals.post._id, error });

22
app/models/comment.js

@ -9,16 +9,36 @@ const path = require('path');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CommentHistorySchema = new Schema({
created: { type: Date },
content: { type: String, maxlength: 3000 },
});
const { CommentStats, CommentStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js'));
const COMMENT_STATUS_LIST = ['published', 'removed', 'mod-warn', 'mod-removed'];
const CommentSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 },
resourceType: { type: String, enum: ['Post', 'Page', 'Newsletter'], required: true },
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' },
replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' },
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' },
status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true },
content: { type: String, required: true, maxlength: 3000 },
contentHistory: { type: [CommentHistorySchema], select: false },
stats: { type: CommentStats, default: CommentStatsDefaults, required: true },
});
/*
* An index to optimize finding replies to a specific comment
*/
CommentSchema.index({
resource: 1,
replyTo: 1,
}, {
partialFilterExpression: { $exists: { replyTo: 1 } },
name: 'comment_replies',
});
module.exports = mongoose.model('Comment', CommentSchema);

173
app/services/comment.js

@ -0,0 +1,173 @@
// comment.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const mongoose = require('mongoose');
const Comment = mongoose.model('Comment'); // jshint ignore:line
const pug = require('pug');
const striptags = require('striptags');
const { SiteService, SiteError } = require('../../lib/site-lib');
class CommentService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
this.populateComment = [
{
path: 'author',
select: '',
},
{
path: 'replyTo',
},
];
}
async start ( ) {
this.templates = { };
this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug'));
}
async create (author, resourceType, resource, commentDefinition) {
const NOW = new Date();
let comment = new Comment();
comment.created = NOW;
comment.resourceType = resourceType;
comment.resource = resource._id;
comment.author = author._id;
if (commentDefinition.replyTo) {
comment.replyTo = mongoose.Types.ObjectId(commentDefinition.replyTo);
}
if (commentDefinition.content) {
comment.content = striptags(commentDefinition.content.trim());
}
await comment.save();
const model = mongoose.model(resourceType);
await model.updateOne(
{ _id: resource._id },
{
$inc: { 'stats.commentCount': 1 },
},
);
/*
* increment the reply count of every parent comment until you reach a
* comment with no parent.
*/
let replyTo = comment.replyTo;
while (replyTo) {
await Comment.updateOne(
{ _id: replyTo },
{
$inc: { 'stats.replyCount': 1 },
},
);
let parent = await Comment.findById(replyTo).select('replyTo').lean();
if (parent.replyTo) {
replyTo = parent.replyTo;
} else {
replyTo = false;
}
}
comment = comment.toObject();
comment.author = author;
return comment;
}
async update (comment, commentDefinition) {
const updateOp = { $set: { } };
if (!commentDefinition.content || (commentDefinition.content.length === 0)) {
throw new SiteError(406, 'The comment cannot be empty');
}
updateOp.$set.content = striptags(commentDefinition.content.trim());
updateOp.$push = {
contentHistory: {
created: new Date(),
content: comment.content,
},
};
this.log.info('updating comment content', { commentId: comment._id });
await Comment.updateOne({ _id: comment._id }, updateOp);
}
async setStatus (comment, status) {
await Comment.updateOne({ _id: comment._id }, { $set: { status } });
}
/**
* Pushes the current comment content to the contentHistory array, sets the
* content field to 'Content removed' and updates the comment status to the
* status provided. This preserves the comment content, but removes it from
* public view.
* @param {Document} comment
* @param {String} status
*/
async remove (comment, status = 'removed') {
await Comment.updateOne(
{ _id: comment._id },
{
$set: {
status,
content: 'Comment removed',
},
$push: {
contentHistory: {
created: new Date(),
content: comment.content,
},
},
},
);
}
async getForResource (resource, statuses, pagination) {
const comments = await Comment
.find({ resource: resource._id, status: { $in: statuses } })
.sort({ created: 1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateComment)
.lean();
return comments;
}
async getContentHistory (comment, pagination) {
/*
* Extract a page from the contentHistory using $slice on the array
*/
const fullComment = await Comment
.findOne(
{ _id: comment._id },
{
contentHistory: {
$sort: { created: 1 },
$slice: [pagination.skip, pagination.cpp],
},
}
)
.select('contentHistory').lean();
if (!fullComment) {
throw new SiteError(404, 'Comment not found');
}
return fullComment.contentHistory || [ ];
}
}
module.exports = {
slug: 'comment',
name: 'comment',
create: (dtp) => { return new CommentService(dtp); },
};

14
app/services/display-engine.js

@ -26,6 +26,13 @@ class DisplayList {
});
}
showModal (html) {
this.commands.push({
action: 'showModal',
params: { html },
});
}
addElement (selector, where, html) {
this.commands.push({
selector, action: 'addElement',
@ -40,6 +47,13 @@ class DisplayList {
});
}
setInputValue (selector, value) {
this.commands.push({
selector, action: 'setInputValue',
params: { value },
});
}
replaceElement (selector, html) {
this.commands.push({
selector, action: 'replaceElement',

2
app/services/gab-tv.js

@ -6,7 +6,7 @@
const fetch = require('node-fetch'); // jshint ignore:line
const CACHE_DURATION = 1000 * 60 * 5;
const CACHE_DURATION = 60 * 5;
const { SiteService } = require('../../lib/site-lib');

4
app/views/admin/components/menu.pug

@ -6,6 +6,10 @@ ul.uk-nav.uk-nav-default
span.nav-item-icon
i.fas.fa-home
span.uk-margin-small-left Home
a(href="/admin/settings")
span.nav-item-icon
i.fas.fa-cog
span.uk-margin-small-left Settings
li.uk-nav-divider

10
app/views/admin/post/editor.pug

@ -18,11 +18,13 @@ block content
label(for="slug").uk-form-label URL slug
-
var postSlug;
postSlug = post.slug.split('-');
postSlug.pop();
postSlug = postSlug.join('-');
if (post) {
postSlug = post.slug.split('-');
postSlug.pop();
postSlug = postSlug.join('-');
}
input(id="slug", name="slug", type="text", placeholder= "Enter post URL slug", value= post ? postSlug : undefined).uk-input
.uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post.slug || 'your-slug-here'}
.uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post ? post.slug : 'your-slug-here'}
.uk-margin
label(for="summary").uk-form-label Post summary
textarea(id="summary", name="summary", rows="4", placeholder= "Enter post summary (text only, no HTML)").uk-textarea= post ? post.summary : undefined

15
app/views/admin/settings/editor.pug

@ -0,0 +1,15 @@
extends ../layouts/main
block content
form(method="POST", action="/admin/settings").uk-form
.uk-margin
label(for="name").uk-form-label Site name
input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input
.uk-margin
label(for="description").uk-form-label Site description
input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input
.uk-margin
label(for="company").uk-form-label Company name
input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).uk-input
button(type="submit").uk-button.dtp-button-primary Save Settings

3
app/views/comment/components/comment-standalone.pug

@ -0,0 +1,3 @@
include ../../components/library
include comment
+renderComment(comment)

47
app/views/comment/components/comment.pug

@ -0,0 +1,47 @@
mixin renderComment (comment)
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded
.uk-card-body
div(uk-grid).uk-grid-small
.uk-width-auto
img(src="/img/default-member.png").site-profile-picture.sb-small
.uk-width-expand
div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small
if comment.author.displayName
.uk-width-auto
span= comment.author.displayName
.uk-width-auto= comment.author.username
.uk-width-auto= moment(comment.created).fromNow()
div!= marked.parse(comment.content)
div(uk-grid).uk-grid-small.uk-text-small
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.upvoteComment(event);",
title="Upvote this comment",
).uk-button.uk-button-link
+renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.downvoteComment(event);",
title="Downvote this comment",
).uk-button.uk-button-link
+renderButtonIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.openReplies(event);",
title="Load replies to this comment",
).uk-button.uk-button-link
+renderButtonIcon('fa-comment', formatCount(comment.stats.replyCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.openReplyComposer(event);",
title="Write a reply to this comment",
).uk-button.uk-button-link
+renderButtonIcon('fa-reply', 'reply')

1
app/views/components/library.pug

@ -1,6 +1,7 @@
//- common routines for all views everywhere
include button-icon
include section-title
-
function formatCount(value) {

10
app/views/components/navbar.pug

@ -26,11 +26,15 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
div.no-select
if user.picture_url
img(
src= user.picture_url || '/img/default-member.png',
src= user.picture_url,
title="Member Menu",
).profile-navbar
).site-profile-picture.sb-navbar
else
include missing-profile-icon
img(
src= "/img/default-member.png",
title="Member Menu",
).site-profile-picture.sb-navbar
div(uk-dropdown={ mode: 'click' }).uk-navbar-dropdown
ul.uk-nav.uk-navbar-dropdown-nav(style="z-index: 1024;")
li.uk-nav-heading.uk-text-center= user.displayName || user.username

39
app/views/components/page-sidebar.pug

@ -13,29 +13,28 @@ mixin renderSidebarEpisode(episode)
mixin renderPageSidebar ( )
//- Gab TV 3 Most Recent Episodes
.uk-margin
.dtp-border-bottom
h3.uk-heading-bullet
a(href= gabTvChannel.home_page_url, target= "_blank", title= `${gabTvChannel.title} on Gab`).uk-link-reset Gab TV
+renderSectionTitle('Gab TV', {
label: 'Visit Channel',
title: gabTvChannel.title,
url: gabTvChannel.home_page_url,
})
ul.uk-list
each episode in gabTvChannel.items.slice(0, 3)
li
+renderSidebarEpisode(episode)
//- Newsletter Signup
//- TODO Add sticky
.uk-margin
.dtp-border-bottom.uk-margin
h3.uk-heading-bullet Mailing List
form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form
.uk-card.uk-card-secondary.uk-card-small
.uk-card-body
p Join the #{site.name} FREE newsletter to get show updates in your inbox.
.uk-margin
label(for="email").uk-form-label.sr-only Email Address
input(id="email", name="email", type="email", placeholder="[email protected]").uk-input
.uk-card-footer
button(type="submit").uk-button.uk-button-primary Sign Up
div(uk-sticky={ offset: 60, bottom: '#dtp-content-grid' })
+renderSectionTitle('Mailing List')
.uk-margin
form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form
.uk-card.uk-card-secondary.uk-card-small
.uk-card-body
p Join the #{site.name} FREE newsletter to get show updates in your inbox.
.uk-margin
label(for="email").uk-form-label.sr-only Email Address
input(id="email", name="email", type="email", placeholder="[email protected]").uk-input
.uk-card-footer
button(type="submit").uk-button.dtp-button-primary.uk-button-small Sign Up

8
app/views/components/section-title.pug

@ -0,0 +1,8 @@
mixin renderSectionTitle (title, barButton)
.dtp-border-bottom
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-expand
h3.uk-heading-bullet.uk-margin-small= title
if barButton
.uk-width-auto
a(href= barButton.url, target= "_blank", title= barButton.title).uk-button.uk-button-link.uk-button-small= barButton.label

69
app/views/index.pug

@ -1,4 +1,4 @@
extends layouts/main
extends layouts/main-sidebar
block content
include components/page-sidebar
@ -7,16 +7,16 @@ block content
a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset
div(uk-grid).uk-grid-small
div(class={
div(class='uk-visible@m', class={
'uk-flex-first': ((postIndex % postIndexModulus) === 0),
'uk-flex-last': ((postIndex % postIndexModulus) !== 0),
}).uk-width-1-3
img(src="/img/default-poster.jpg").responsive
div(class={
div(class='uk-width-1-1 uk-width-2-3@m', class={
'uk-flex-first': ((postIndex % postIndexModulus) !== 0),
'uk-flex-last': ((postIndex % postIndexModulus) === 0),
}).uk-width-2-3
})
article.uk-article
h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title
.uk-text-truncate= post.summary
@ -26,41 +26,28 @@ block content
if post.updated
.uk-width-auto updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")}
.uk-padding
.uk-container
//- Main Content Grid
div(uk-grid)
//- Main Content Column
div(class="uk-width-1-1 uk-width-2-3@m")
section.uk-section.uk-section-default.uk-padding-remove
.dtp-border-bottom
h3.uk-heading-bullet Featured
//- Featured Block
.uk-margin
div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%")
iframe(
src="https://tv.gab.com/channel/mrjoeprich/embed/what-is-just-joe-radio-61ad9b2165a83d20e95a465d",
width="960",
height="540",
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;",
)
//- Blog Posts
.uk-section.uk-section-default.uk-section-small
.dtp-border-bottom.uk-margin
h3.uk-heading-bullet Blog Posts
if Array.isArray(posts) && (posts.length > 0)
- var postIndex = 1;
ul.uk-list.uk-list-divider.uk-list-small
each post in posts
li
+renderBlogPostListItem(post, postIndex, 2)
- postIndex += 1;
else
div There are no posts at this time. Please check back later!
+renderSectionTitle('Featured')
.uk-margin
div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%")
iframe(
src="https://tv.gab.com/channel/mrjoeprich/embed/what-is-just-joe-radio-61ad9b2165a83d20e95a465d",
width="960",
height="540",
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;",
)
//- pre= JSON.stringify(gabTvChannel, null, 2)
//- Sidebar
div(class="uk-width-1-1 uk-width-1-3@m")
+renderPageSidebar()
if featuredEmbed
div.dtp-featured-embed!= featuredEmbed
//- Blog Posts
+renderSectionTitle('Blog Posts')
if Array.isArray(posts) && (posts.length > 0)
- var postIndex = 1;
ul.uk-list.uk-list-divider.uk-list-small
each post in posts
li
+renderBlogPostListItem(post, postIndex, 2)
- postIndex += 1;
else
div There are no posts at this time. Please check back later!

13
app/views/layouts/main-sidebar.pug

@ -0,0 +1,13 @@
extends main
block content-container
section.uk-section.uk-section-default
.uk-container
div(uk-grid)#dtp-content-grid
div(class="uk-width-1-1 uk-width-2-3@m")
block content
div(class="uk-width-1-1 uk-width-1-3@m")
+renderPageSidebar()
block page-footer
include ../components/page-footer

3
app/views/layouts/main.pug

@ -1,4 +1,5 @@
include ../components/library
include ../components/page-sidebar
doctype html
html(lang='en')
head
@ -24,7 +25,7 @@ html(lang='en')
block js
script(src=`/uikit/js/uikit.min.js?v=${pkg.version}`)
script(src=`/uikit/js/uikit-icons.min.js?v=${pkg.version}`)
script(src=`/fontawesome/js/fontawesome.min.js?v=${pkg.version}`)
//- script(src=`/fontawesome/js/fontawesome.min.js?v=${pkg.version}`)
block pwa-support
include ../components/pwa-support

27
app/views/page/view.pug

@ -1,22 +1,15 @@
extends ../layouts/main
extends ../layouts/main-sidebar
block content
include ../components/page-sidebar
section.uk-section.uk-section-default.uk-section-small
.uk-container
article(dtp-page-id= page._id)
.uk-margin
div(uk-grid)
.uk-width-2-3
article(dtp-page-id= page._id)
.uk-margin
div(uk-grid)
.uk-width-expand
h1.article-title= page.title
if user && user.flags.isAdmin
.uk-width-auto
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT
.uk-margin
!= page.content
.uk-width-1-3
+renderPageSidebar()
.uk-width-expand
h1.article-title= page.title
if user && user.flags.isAdmin
.uk-width-auto
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT
.uk-margin
!= page.content

117
app/views/post/view.pug

@ -1,54 +1,79 @@
extends ../layouts/main
extends ../layouts/main-sidebar
block content
include ../components/page-sidebar
include ../comment/components/comment
section.uk-section.uk-section-default.uk-section-small
.uk-container
article(dtp-post-id= post._id)
.uk-margin
div(uk-grid)
.uk-width-2-3
article(dtp-post-id= post._id)
.uk-margin
div(uk-grid)
.uk-width-expand
h1.article-title= post.title
if user && user.flags.isAdmin
.uk-width-auto
a(href=`/admin/post/${post._id}`).uk-button.dtp-button-text EDIT
.uk-text-lead= post.summary
.uk-margin
.uk-article-meta
div(uk-grid).uk-grid-small.uk-flex-top
.uk-width-expand
div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()}
.uk-width-auto
+renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount))
.uk-width-auto
+renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount))
.uk-width-auto
+renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount))
.uk-width-auto
+renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount))
.uk-margin
!= post.content
.uk-width-expand
h1.article-title= post.title
.uk-text-lead= post.summary
.uk-margin
.uk-article-meta
div(uk-grid).uk-grid-small.uk-flex-top
.uk-width-expand
div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()}
if user && user.flags.isAdmin
.uk-width-auto
a(href=`/admin/post/${post._id}`)
+renderButtonIcon('fa-pen', 'edit')
.uk-width-auto
+renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount))
.uk-width-auto
+renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount))
.uk-width-auto
+renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount))
.uk-width-auto
+renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount))
.uk-margin
!= post.content
if post.updated
.uk-margin
.uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}.
if post.updated
.uk-margin
.uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}.
if user && post.flags.enableComments
.dtp-border-bottom
h3.uk-heading-bullet Comments
.uk-margin
form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event);").uk-form
.uk-margin
textarea(id="content", name="content", rows="4", placeholder="Enter comment").uk-textarea
.uk-text-small
div(uk-grid).uk-flex-between
.uk-width-auto You are commenting as: #{user.username}
.uk-width-auto 0 of 3000
if user && post.flags.enableComments
+renderSectionTitle('Add a comment')
.uk-margin
form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form
.uk-card.uk-card-secondary.uk-card-small
.uk-card-body
textarea(
id="content",
name="content",
rows="4",
maxlength="3000",
placeholder="Enter comment",
oninput="return dtp.app.onCommentInput(event);",
).uk-textarea.uk-resize-vertical
.uk-text-small
div(uk-grid).uk-flex-between
.uk-width-auto You are commenting as: #{user.username}
.uk-width-auto #[span#comment-character-count 0] of 3,000
.uk-card-footer
div(uk-grid).uk-flex-between
.uk-width-expand
button(
type="button",
data-target-element="content",
title="Add an emoji",
onclick="return dtp.app.showEmojiPicker(event);",
).uk-button.dtp-button-default
span
i.far.fa-smile
.uk-width-auto
button(type="submit").uk-button.dtp-button-primary Post comment
.uk-margin
+renderSectionTitle('Comments')
.uk-width-1-3
+renderPageSidebar()
.uk-margin
if Array.isArray(comments) && (comments.length > 0)
ul#post-comment-list.uk-list
each comment in comments
+renderComment(comment)
else
ul#post-comment-list.uk-list
div There are no comments at this time. Please check back later.

42
app/views/welcome/login.pug

@ -4,21 +4,29 @@ block content
form(method="POST", action="/auth/login").uk-form
section.uk-section.uk-section-default
.uk-container
fieldset.uk-fieldset
legend(class="uk-text-center uk-text-left@m").uk-legend Member Login
if loginResult
div(uk-alert).uk-alert.uk-alert-danger= loginResult
.uk-margin
label(for="username", class="uk-visible@m").uk-form-label Username or email address
input(id="username", name="username", type="text", placeholder="Enter username or email address").uk-input
.uk-card.uk-card-secondary.uk-card-small.uk-width-xlarge.uk-margin-auto.uk-border-rounded
.uk-card-header
+renderSectionTitle('Member Login')
.uk-card-body
fieldset.uk-fieldset
.uk-margin-small
div(uk-grid)
.uk-width-1-3
img(src=`/img/icon/${site.domainKey}.png`).responsive
.uk-width-expand
if loginResult
div(uk-alert).uk-alert.uk-alert-danger= loginResult
.uk-margin
label(for="username", class="uk-visible@m").uk-form-label Username or email address
input(id="username", name="username", type="text", placeholder="Enter username or email address").uk-input
.uk-margin
label(for="password", class="uk-visible@m").uk-form-label Password
input(id="password", name="password", type="password", placeholder="Enter password").uk-input
.uk-text-muted.uk-text-small.uk-margin-small-top Remember that password we said you shouldn't forget? Type that here.
section.uk-section.uk-section-secondary.uk-section-xsmall
.uk-container
.uk-margin
.uk-flex.uk-flex-center
button(type="submit").uk-button.dtp-button-primary Login
.uk-margin
label(for="password", class="uk-visible@m").uk-form-label Password
input(id="password", name="password", type="password", placeholder="Enter password").uk-input
.uk-card-footer
.uk-flex.uk-flex-right.uk-flex-middle
.uk-width-expand
a(href="/").uk-text-muted Forgot password
.uk-width-auto
button(type="submit").uk-button.dtp-button-primary Login

7
app/views/welcome/signup.pug

@ -4,7 +4,7 @@ block content
form(method="POST", action="/user").uk-form
section.uk-section.uk-section-muted.uk-section
.uk-container.uk-container-small
p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here].
p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here]. An account is required to comment on posts and use other site features.
.uk-margin
label(for="email").uk-form-label Email
@ -35,6 +35,11 @@ block content
input(id="passwordv", name="passwordv", type="password", placeholder="Verify password").uk-input
.uk-text-small.uk-text-muted.uk-margin-small-top(class="uk-visible@m") Please enter your password again to prove you're not an idiot.
.uk-margin
label
input(type="checkbox", checked).uk-checkbox
| Join #{site.name}'s Newsletter
section.uk-section.uk-section-secondary.uk-section
.uk-container.uk-container-small
.uk-margin-large

22
client/js/site-app.js

@ -13,6 +13,8 @@ import UIkit from 'uikit';
import QRCode from 'qrcode';
import Cropper from 'cropperjs';
import { EmojiButton } from '@joeattardi/emoji-button';
export default class DtpSiteApp extends DtpApp {
constructor (user) {
@ -27,6 +29,10 @@ export default class DtpSiteApp extends DtpApp {
input: document.querySelector('#chat-input-text'),
isAtBottom: true,
};
this.emojiPicker = new EmojiButton({ theme: 'dark' });
this.emojiPicker.on('emoji', this.onEmojiSelected.bind(this));
if (this.chat.messageList) {
this.chat.messageList.addEventListener('scroll', this.onChatMessageListScroll.bind(this));
}
@ -472,6 +478,22 @@ export default class DtpSiteApp extends DtpApp {
UIkit.modal.alert(`Failed to remove image: ${error.message}`);
}
}
async onCommentInput (event) {
const label = document.getElementById('comment-character-count');
label.textContent = numeral(event.target.value.length).format('0,0');
}
async showEmojiPicker (event) {
const targetElementName = (event.currentTarget || event.target).getAttribute('data-target-element');
this.emojiTargetElement = document.getElementById(targetElementName);
this.emojiPicker.togglePicker(this.emojiTargetElement);
}
async onEmojiSelected (selection) {
this.emojiTargetElement.value += selection.emoji;
}
}
dtp.DtpSiteApp = DtpSiteApp;

3
client/less/site/image.less

@ -54,6 +54,9 @@ img.site-profile-picture {
max-width: 48px;
border-radius: 6px;
}
&.sb-navbar {
max-width: 48px;
}
&.sb-small {
max-width: 64px;
}

19
lib/client/js/dtp-display-engine.js

@ -57,7 +57,7 @@ export default class DtpDisplayEngine {
/*
* action: addElement
* selector: Specifies the container element to insertAdjacentHtml
* selector: Specifies the container element to insertAdjacentHTML
* where: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
* html: the HTML content to insert at the container as specified
*/
@ -67,7 +67,7 @@ export default class DtpDisplayEngine {
console.debug('displayList.addElement has failed', { command });
return;
}
container.insertAdjacentHtml(command.params.where, command.params.html);
container.insertAdjacentHTML(command.params.where, command.params.html);
}
async setTextContent (displayList, command) {
@ -81,6 +81,17 @@ export default class DtpDisplayEngine {
});
}
async setInputValue (displayList, command) {
const elements = document.querySelectorAll(command.selector);
if (!elements || (elements.length === 0)) {
this.log.error('setInputValue', 'failed to find target elements', { command });
return;
}
elements.forEach((element) => {
element.value = command.params.value;
});
}
/*
* action: replaceElement
* selector: Specifies the element to be replaced
@ -196,4 +207,8 @@ export default class DtpDisplayEngine {
async showNotification (displayList, command) {
UIkit.notification(command.params);
}
async showModal (displayList, command) {
UIkit.modal.dialog(command.html);
}
}

5
lib/site-platform.js

@ -166,7 +166,6 @@ module.exports.startWebServer = async (dtp) => {
* Expose useful modules and information
*/
module.app.locals.DTP_SCRIPT_DEBUG = (process.env.NODE_ENV === 'local');
module.app.locals.site = dtp.config.site;
module.app.locals.pkg = require(path.join(dtp.config.root, 'package.json'));
module.app.locals.moment = require('moment');
module.app.locals.numeral = require('numeral');
@ -270,6 +269,7 @@ module.exports.startWebServer = async (dtp) => {
* Application logic middleware
*/
module.app.use(async (req, res, next) => {
const { cache: cacheService } = dtp.services;
try {
res.locals.dtp = {
request: req,
@ -291,6 +291,9 @@ module.exports.startWebServer = async (dtp) => {
icon: 'fa-instagram'
},
];
const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
res.locals.site = (await cacheService.getObject(settingsKey)) || dtp.config.site;
return next();
} catch (error) {
module.log.error('failed to populate general request data', { error });

1
package.json

@ -12,6 +12,7 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@joeattardi/emoji-button": "^4.6.2",
"@socket.io/redis-adapter": "^7.1.0",
"anchorme": "^2.1.2",
"argv": "^0.0.2",

122
yarn.lock

@ -885,11 +885,54 @@
"@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0"
"@fortawesome/fontawesome-common-types@^0.2.36":
version "0.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903"
integrity sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg==
"@fortawesome/fontawesome-free@^5.15.4":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==
"@fortawesome/fontawesome-svg-core@^1.2.28":
version "1.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz#4f2ea6f778298e0c47c6524ce2e7fd58eb6930e3"
integrity sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-regular-svg-icons@^5.13.0":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz#b97edab436954333bbeac09cfc40c6a951081a02"
integrity sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@fortawesome/free-solid-svg-icons@^5.13.0":
version "5.15.4"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5"
integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36"
"@joeattardi/emoji-button@^4.6.2":
version "4.6.2"
resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-4.6.2.tgz#75baf4ce27324e4d6fb90292f8b248235f638ad0"
integrity sha512-FhuzTmW3nVHLVp2BJfNX17CYV77fqtKZlx328D4h6Dw3cPTT1gJRNXN0jV7BvHgsl6Q/tN8DIQQxTUIO4jW3gQ==
dependencies:
"@fortawesome/fontawesome-svg-core" "^1.2.28"
"@fortawesome/free-regular-svg-icons" "^5.13.0"
"@fortawesome/free-solid-svg-icons" "^5.13.0"
"@popperjs/core" "^2.4.0"
"@types/twemoji" "^12.1.1"
escape-html "^1.0.3"
focus-trap "^5.1.0"
fuzzysort "^1.1.4"
tiny-emitter "^2.1.0"
tslib "^2.0.0"
twemoji "^13.0.0"
"@otplib/core@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d"
@ -928,6 +971,11 @@
"@otplib/plugin-crypto" "^12.0.1"
"@otplib/plugin-thirty-two" "^12.0.1"
"@popperjs/core@^2.4.0":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7"
integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==
"@rollup/plugin-babel@^5.2.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879"
@ -1065,6 +1113,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/twemoji@^12.1.1":
version "12.1.2"
resolved "https://registry.yarnpkg.com/@types/twemoji/-/twemoji-12.1.2.tgz#52578fd22665311e6a78d04f800275449d51c97e"
integrity sha512-3eMyKenMi0R1CeKzBYtk/Z2JIHsTMQrIrTah0q54o45pHTpWVNofU2oHx0jS8tqsDRhis2TbB6238WP9oh2l2w==
"@types/webidl-conversions@*":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e"
@ -3139,7 +3192,7 @@ escape-goat@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
escape-html@~1.0.3:
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
@ -3530,6 +3583,14 @@ flush-write-stream@^1.0.2:
inherits "^2.0.3"
readable-stream "^2.3.6"
focus-trap@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
dependencies:
tabbable "^4.0.0"
xtend "^4.0.1"
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.14.5"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381"
@ -3592,6 +3653,15 @@ [email protected]:
jsonfile "^3.0.0"
universalify "^0.1.0"
fs-extra@^8.0.1:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^9.0.1:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
@ -3633,6 +3703,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
fuzzysort@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba"
integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ==
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -4895,6 +4970,22 @@ jsonfile@^3.0.0:
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
dependencies:
universalify "^0.1.2"
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -7552,6 +7643,11 @@ systeminformation@^5.9.16:
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.9.16.tgz#097d5b585401b209b3448d1fe84551a5a582b904"
integrity sha512-GDqen5wR9p3GVrTlyFYKbtQIE9eEhqd6Ya9Jr6HReSbDYJuYqhUgYTLuEt45qpSgNj1hKonUe/IzzdFXFmRBeg==
tabbable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
tapable@^2.1.1, tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@ -7667,6 +7763,11 @@ time-stamp@^1.0.0:
resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=
tiny-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-inflate@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
@ -7789,7 +7890,7 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tslib@^2.3.0:
tslib@^2.0.0, tslib@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@ -7801,6 +7902,21 @@ tunnel-agent@^0.6.0:
dependencies:
safe-buffer "^5.0.1"
[email protected]:
version "13.1.0"
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4"
integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==
twemoji@^13.0.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-13.1.0.tgz#65bb71e966dae56f0d42c30176f04cbdae109913"
integrity sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew==
dependencies:
fs-extra "^8.0.1"
jsonfile "^5.0.0"
twemoji-parser "13.1.0"
universalify "^0.1.2"
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@ -8644,7 +8760,7 @@ xmlhttprequest-ssl@~1.6.2:
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==

Loading…
Cancel
Save