Browse Source

Newsroom Kiosk (wip)

develop
Rob Colbert 5 months ago
parent
commit
2151e21c17
  1. 96
      app/controllers/newsroom.js
  2. 67
      app/services/feed.js
  3. 8
      app/views/layouts/main.pug
  4. 16
      app/views/newsroom/components/kiosk-carousel.pug
  5. 16
      app/views/newsroom/kiosk/feed.pug
  6. 16
      app/views/newsroom/kiosk/home.pug
  7. 62
      app/views/newsroom/layouts/kiosk.pug
  8. BIN
      client/img/default-kiosk-poster.png
  9. 167
      client/less/site/kiosk.less
  10. 7
      client/less/site/main.less
  11. 1
      client/less/style.common.less
  12. 15
      config/limiter.js

96
app/controllers/newsroom.js

@ -19,6 +19,8 @@ class NewsroomController extends SiteController {
const { limiter: limiterService } = dtp.services;
const limiterConfig = limiterService.config.newsroom;
const kioskMiddleware = this.kioskMiddleware.bind(this);
const router = express.Router();
dtp.app.use('/newsroom', router);
@ -28,6 +30,26 @@ class NewsroomController extends SiteController {
});
router.param('feedId', this.populateFeedId.bind(this));
router.param('feedEntryId', this.populateFeedEntryId.bind(this));
router.get(
'/kiosk/:feedId/article/:feedEntryId/image',
limiterService.createMiddleware(limiterConfig.getKioskFeedEntryImage),
kioskMiddleware,
this.getKioskFeedEntryImage.bind(this),
);
router.get(
'/kiosk/:feedId',
limiterService.createMiddleware(limiterConfig.getKioskFeed),
kioskMiddleware,
this.getKioskFeed.bind(this),
);
router.get(
'/kiosk',
limiterService.createMiddleware(limiterConfig.getKiosk),
kioskMiddleware,
this.getKiosk.bind(this),
);
router.get(
'/search',
@ -76,6 +98,80 @@ class NewsroomController extends SiteController {
}
}
async populateFeedEntryId (req, res, next, feedEntryId) {
const {
feed: feedService,
logan: loganService,
} = this.dtp.services;
try {
res.locals.entry = await feedService.getFeedEntry(res.locals.feed, feedEntryId);
if (!res.locals.entry) {
throw new SiteError(404, 'Feed entry not found');
}
return next();
} catch (error) {
loganService.sendRequestEvent(module.exports, req, {
level: 'error',
event: 'populateFeedEntryId',
message: error.message,
data: { feedEntryId, error },
});
return next(error);
}
}
async kioskMiddleware (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.viewMode = 'focused';
res.locals.feedList = await feedService.getFeeds({ p: 1, cpp: 500 });
return next();
} catch (error) {
this.log.error('failed to present Kiosk middleware', { error });
return next(error);
}
}
async getKioskFeedEntryImage (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
const image = await feedService.getFeedEntryImage(res.locals.feed, res.locals.entry);
if (!image) {
throw new SiteError(500, 'Failed to resolve feed entry preview image');
}
res.set('Content-Type', image.contentType);
res.status(200);
image.stream.pipe(res);
} catch (error) {
this.log.error('failed to serve Newsroom Kiosk feed entry image', { error });
return next(error);
}
}
async getKioskFeed (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.entries = await feedService.getFeedEntries(res.locals.feed, res.locals.pagination);
res.render('newsroom/kiosk/feed');
} catch (error) {
this.log.error('failed to serve Newsroom Kiosk feed view', { error });
return next(error);
}
}
async getKiosk (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.entries = await feedService.getNewsfeed(res.locals.pagination);
res.render('newsroom/kiosk/home');
} catch (error) {
this.log.error('failed to serve Newsroom Kiosk unified feed view', { error });
return next(error);
}
}
async getSearch (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {

67
app/services/feed.js

@ -141,6 +141,9 @@ class FeedService extends SiteService {
}
async getFeedEntries (feed, pagination) {
if (!feed) {
throw new SiteError(400, 'Must specify feed');
}
pagination = Object.assign({ skip: 0, cpp: 10 }, pagination);
const entries = await FeedEntry
.find({ feed: feed._id })
@ -153,6 +156,21 @@ class FeedService extends SiteService {
return { entries, totalFeedEntryCount };
}
async getFeedEntry (feed, entryId) {
if (!feed) {
throw new SiteError(400, 'Must specify feed');
}
if (!entryId) {
throw new SiteError(400, 'Must specify feed entry ID');
}
const entry = await FeedEntry
.findOne({ _id: entryId })
.populate(this.populateFeedEntry)
.lean();
return entry;
}
async getNewsfeed (pagination) {
pagination = Object.assign({ skip: 0, cpp: 5 }, pagination);
const entries = await FeedEntry
@ -294,6 +312,55 @@ class FeedService extends SiteService {
});
}
async resolveFeedEntryPreview (feed, entry) {
const ONE_HOUR = 60 * 60;
const { cache: cacheService } = this.dtp.services;
const cacheKey = `fep:${feed._id}:${entry._id}`;
let preview = await cacheService.getObject(cacheKey);
if (!preview) {
preview = await getLinkPreview(entry.link, {
headers: {
'user-agent': this.userAgent.toString(),
'Accept-Language': 'en-US',
},
followRedirects: true,
resolveDNSHost: module.resolveHost,
timeout: 15000,
});
await cacheService.setObjectEx(cacheKey, ONE_HOUR * 4, preview, );
}
return preview;
}
async getFeedEntryImage (feed, entry) {
const path = require('path');
const fs = require('fs');
const { Readable } = require('stream');
const preview = await this.resolveFeedEntryPreview(feed, entry);
if (!preview || !Array.isArray(preview.images) || (preview.images.length === 0)) {
const stream = fs.createReadStream(path.join(this.dtp.config.root, 'client', 'img', 'default-kiosk-poster.png'));
return { contentType: 'image/png', stream };
}
const imageUrl = preview.images[0];
this.log.info('fetching image', { imageUrl });
const response = await fetch(imageUrl);
if (!response || !response.ok) {
const stream = fs.createReadStream(path.join(this.dtp.config.root, 'client', 'img', 'default-kiosk-poster.png'));
return { contentType: 'image/png', stream };
}
this.log.info('serving image', { type: response.headers.get('content-type') });
return {
stream: Readable.fromWeb(response.body),
contentType: response.headers.get('content-type'),
};
}
async search (query, pagination) {
const search = {
$text: { $search: query },

8
app/views/layouts/main.pug

@ -55,7 +55,13 @@ html(lang='en')
}
}
body.dtp(class= user ? user.theme : DEFAULT_THEME, data-dtp-env= process.env.NODE_ENV, data-dtp-domain= site.domainKey, data-current-view= currentView)
body.dtp(
class= user ? user.theme : DEFAULT_THEME,
data-dtp-env= process.env.NODE_ENV,
data-dtp-domain= site.domainKey,
data-view-mode= viewMode || 'main',
data-current-view= currentView,
)
include ../components/site-link

16
app/views/newsroom/components/kiosk-carousel.pug

@ -0,0 +1,16 @@
mixin renderKioskCarouselFeedEntry (entry)
.uk-panel
img(src= `/newsroom/kiosk/${entry.feed._id}/article/${entry._id}/image`).feed-entry-image
.feed-entry-summary.uk-position-bottom
.feed-entry-title.uk-text-truncate= entry.title
.feed-entry-description.uk-margin-small!= marked.parse(entry.description)
.uk-text-small.uk-text-muted= moment(entry.published).format('MMMM D, YYYY [at] h:mm a')
mixin renderKioskFeedCarousel (entries)
div(tabindex="-1", uk-slider="center: true; autoplay: true;").uk-position-relative.uk-visible-toggle.uk-light
ul.uk-slider-items.uk-grid
each entry in entries
li.uk-width-4-5
+renderKioskCarouselFeedEntry(entry)
a(href, uk-slidenav-previous, uk-slider-item="previous").uk-position-center-left.uk-position-small.uk-hidden-hover
a(href, uk-slidenav-next, uk-slider-item="next").uk-position-center-right.uk-position-small.uk-hidden-hover

16
app/views/newsroom/kiosk/feed.pug

@ -0,0 +1,16 @@
extends ../layouts/kiosk
block kiosk-content
include ../components/kiosk-carousel
.feed-header.uk-margin
div(uk-grid).uk-flex-bottom
.uk-width-expand
h1.uk-margin-remove= feed.title
div(class={ 'uk-text-muted': !feed.description }).uk-text-small= feed.description || `A news feed curated by ${site.name}`
.uk-width-auto
label.uk-form-label Last updated
.uk-text-bold= moment(feed.published).format('MMM D [at] h:mm a')
.feed-content.uk-margin-medium
+renderKioskFeedCarousel(entries.entries)

16
app/views/newsroom/kiosk/home.pug

@ -0,0 +1,16 @@
extends ../layouts/kiosk
block kiosk-content
include ../components/kiosk-carousel
.feed-header
div(uk-grid).uk-flex-middle
.uk-width-expand
h1.uk-margin-remove #{site.name} Unified Feed
.uk-text-small A blend of articles from the Unified feed.
.uk-width-auto
label.uk-form-label.sr-only Today's Date
.uk-text-bold= moment().format('MMMM D, YYYY')
.feed-content.uk-margin-medium
+renderKioskFeedCarousel(entries.entries)

62
app/views/newsroom/layouts/kiosk.pug

@ -0,0 +1,62 @@
extends ../../layouts/focused
block content
include ../../components/pagination-bar
section.uk-section.uk-section-default.uk-section-small.dtp-kiosk
.uk-container.uk-container-expand
div(uk-grid)
div(class="uk-visible@s").uk-width-1-5.kiosk-sidebar
div(uk-grid).uk-grid-small.uk-flex-middle.sidebar-header
.uk-width-expand
.header-site= site.name
.header-title Newsroom
.uk-width-auto
img(src=`/img/icon/${site.domainKey}/icon-72x72.png`)
ul.uk-list.sidebar-feed-list
li
div(class={ 'item-active': !feed }).feed-list-item
a(href='/newsroom/kiosk')
div(uk-grid).uk-grid-small.uk-flex-middle
div(class="uk-visible@s").uk-width-auto
span
i.fas.fa-home
.uk-width-expand
.uk-text-bold.uk-text-truncate Unified Feed
li  
if feedList && Array.isArray(feedList.feeds) && (feedList.feeds.length > 0)
each feedItem in feedList.feeds
li
div(, class={ 'item-active': (feed && feed._id.equals(feedItem._id)) }).feed-list-item
a(href=`/newsroom/kiosk/${feedItem._id}`)
div(uk-grid).uk-grid-small.uk-flex-middle
div(class="uk-visible@s").uk-width-auto
img(src= feedItem.favicons[0]).feed-icon
.uk-width-expand
.uk-text-bold.uk-text-truncate= feedItem.title
.uk-width-auto
.uk-text-small.uk-text-muted= moment(feedItem.published).format('h:mm a')
div(class="uk-width-1-1 uk-width-expand@s").kiosk-content
block kiosk-content
if entries && Array.isArray(entries.entries) && (entries.entries.length > 0)
div(class="uk-width-1-1 uk-width-1-5@s").kiosk-sidebar
ul.uk-list.sidebar-feed-list
each entry of entries.entries
li
.feed-entry
a(href=`/newsroom/kiosk/${entry.feed._id}/article/${entry._id}`)
div(uk-grid).uk-grid-small
.uk-width-expand
.uk-text-bold.uk-text-truncate= entry.title
a(href=`/newsroom/kiosk/${entry.feed._id}`)
.uk-flex.uk-flex-middle
div(class="uk-visible@s").uk-width-auto
img(src= entry.feed.favicons[0]).feed-icon
.uk-width-expand
.uk-text-truncate= entry.feed.title

BIN
client/img/default-kiosk-poster.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

167
client/less/site/kiosk.less

@ -0,0 +1,167 @@
.dtp-kiosk {
.kiosk-sidebar {
.sidebar-header {
font-weight: bold;
.header-site {
font-size: 18px;
line-height: 1;
}
.header-title {
font-size: 42px;
line-height: 1;
}
}
.sidebar-feed-list {
li .feed-list-item,
li .feed-entry {
padding: 4px 8px;
display: block;
background: #1a1a1a;
overflow: hidden;
border: solid 1px;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
border-right: none;
border-color: transparent;
transition: background 0.5s;
&.item-active {
background: rgb(0,0,0);
background: linear-gradient(270deg, rgba(0,0,0,0) 0%, rgba(70,5,5,0.9528186274509804) 48%, rgba(121,9,9,1) 83%, rgba(255,0,19,1) 100%);
border-color: #c8c8c8;
img.feed-icon {
border-color: #c8c8c8;
}
}
a {
color: inherit;
text-decoration: none;
}
&:hover {
background: rgb(0,0,0);
background: linear-gradient(270deg, rgba(0,0,0,0) 0%, rgb(133, 133, 133) 100%);
color: inherit;
text-decoration: inherit;
border-color: #808080;
img.feed-icon {
border-color: #c8c8c8;
}
}
&:active {
background: rgb(0,0,0);
background: linear-gradient(270deg, rgba(0,0,0,0) 0%, rgb(90, 36, 36) 100%);
color: inherit;
text-decoration: inherit;
border-color: #808080;
img.feed-icon {
border-color: #c8c8c8;
}
}
}
li .feed-list-item {
img.feed-icon {
box-sizing: border-box;
display: block;
width: 24px;
height: 24px;
background: rgba(0,0,0, 0.8);
border: solid 1px;
border-color: transparent;
object-fit: cover;
object-position: center center;
}
a {
font-size: 24px;
-webkit-text-stroke-width: 0.25px;
-webkit-text-stroke-color: black;
color: #e8e8e8;
text-decoration: none;
}
}
li .feed-entry {
margin-left: @global-small-gutter;
img.feed-icon {
box-sizing: border-box;
display: block;
width: 16px;
height: 16px;
margin-right: 4px;
background: rgba(0,0,0, 0.8);
border: solid 1px;
border-color: transparent;
object-fit: cover;
object-position: center center;
}
a {
font-size: 16px;
-webkit-text-stroke-width: 0.25px;
-webkit-text-stroke-color: black;
color: #e8e8e8;
text-decoration: none;
}
}
}
}
.kiosk-content {
.feed-entry-image {
width: 100%;
aspect-ratio: 16 / 9;
object-position: center;
object-fit: cover;
overflow: hidden;
}
.feed-entry-summary {
padding: 8px 16px;
margin: 8px;
background-color: rgba(0,0,0, 0.75);
color: #ffffff;
.feed-entry-title {
font-size: 32px;
line-height: 1;
color: inherit;
}
.feed-entry-description {
font-size: 16px;
line-height: 1.2em;
color: inherit;
:last-child {
margin-bottom: 0;
}
}
}
}
}

7
client/less/site/main.less

@ -12,6 +12,13 @@ body {
color: inherit;
}
/*
* Added to support Newsroom Kiosk
*/
&[data-view-mode="focused"] {
padding-top: 0;
}
&[data-current-view="chat"] {
position: fixed;
top: @navbar-nav-item-height; right: 0; bottom: 0; left: 0;

1
client/less/style.common.less

@ -15,6 +15,7 @@
@import "site/core-node.less";
@import "site/dashboard.less";
@import "site/form.less";
@import "site/kiosk.less";
@import "site/markdown.less";
@import "site/section.less";
@import "site/sidebar.less";

15
config/limiter.js

@ -293,6 +293,21 @@ module.exports = {
* NewsroomController
*/
newsroom: {
getKioskFeedEntryImage: {
total: 60,
expire: ONE_MINUTE,
message: 'You are loading Newsroom kiosk feed entry images too quickly',
},
getKioskFeed: {
total: 15,
expire: ONE_MINUTE,
message: 'You are loading Newsroom kiosk feeds too quickly',
},
getKiosk: {
total: 15,
expire: ONE_MINUTE,
message: 'You are loading the Newsroom kiosk too quickly',
},
getSearch: {
total: 15,
expire: ONE_MINUTE,

Loading…
Cancel
Save