mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 17:01:08 -04:00
## [2.27.0] - 2024-04-01 Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser, @khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and @vppencilsharpener. This release marks the first release without Michael Teeuw (@michmich). A very special thanks to him for creating MagicMirror and leading the project for so many years. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349) - [chore] Add `eslint-plugin-package-json` to lint the `package.json` files (#3368) - [weather] `showHumidity` config is now a string describing where to show this element. Supported values: "wind", "temp", "feelslike", "below", "none". (#3330) - electron-rebuild test suite for electron and 3rd party modules compatibility (#3392) - Create MM² icon and attach it to electron process (#3407) ### Updated - Update updatenotification (update_helper.js): Recode with pm2 library (#3332) - Removing lodash dependency by replacing merge by spread operator (#3339) - Use node prefix for build-in modules (#3340) - Rework logging colors (#3350) - Update pm2 to v5.3.1 with no allow-ghsas (#3364) - [chore] Update husky and let lint-staged fix ESLint issues - [chore] Update dependencies including electron to v29 (#3357) and node-ical - Update translations for estonian (#3371) - Update electron to v29 and update other dependencies - [calendar] fullDay events over several days now show the left days from the first day on and 'today' on the last day - Update layout of current weather indoor values ### Fixed - Correct apibase of weathergov weatherprovider to match documentation (#2926) - Worked around several issues in the RRULE library that were causing deleted calender events to still show, some initial and recurring events to not show, and some event times to be off an hour. (#3291) - Skip changelog requirement when running tests for dependency updates (#3320) - Display precipitation probability when it is 0% instead of blank/empty (#3345) - [newsfeed] Suppress unsightly animation cases when there are 0 or 1 active news items (#3336) - [newsfeed] Always compute the feed item URL using the same helper function (#3336) - Ignore all custom css files (#3359) - [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361) - Changed `log.debug` to `log.log` in `app.js` where logLevel is not set because config is not loaded at this time (#3353) - [calendar] deny fetch interval < 60000 and set 60000 in this case (prevent fetch loop failed) (#3382) - added message in case where config.js is missing the module.export line PR #3383 - Fixed an issue where recurring events could extend past their recurrence end date (#3393) - Don't display any `npm WARN <....>` on install (#3399) - Fixed move suncalc dependency to production from dev, as it is used by clock module - [compliments] Fix mirror not responding anymore when no compliments are to be shown (#3385) ### Deleted - Unneeded file headers (#3358) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
428 lines
12 KiB
JavaScript
428 lines
12 KiB
JavaScript
Module.register("newsfeed", {
|
|
// Default module config.
|
|
defaults: {
|
|
feeds: [
|
|
{
|
|
title: "New York Times",
|
|
url: "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
|
encoding: "UTF-8" //ISO-8859-1
|
|
}
|
|
],
|
|
showAsList: false,
|
|
showSourceTitle: true,
|
|
showPublishDate: true,
|
|
broadcastNewsFeeds: true,
|
|
broadcastNewsUpdates: true,
|
|
showDescription: false,
|
|
showTitleAsUrl: false,
|
|
wrapTitle: true,
|
|
wrapDescription: true,
|
|
truncDescription: true,
|
|
lengthDescription: 400,
|
|
hideLoading: false,
|
|
reloadInterval: 5 * 60 * 1000, // every 5 minutes
|
|
updateInterval: 10 * 1000,
|
|
animationSpeed: 2.5 * 1000,
|
|
maxNewsItems: 0, // 0 for unlimited
|
|
ignoreOldItems: false,
|
|
ignoreOlderThan: 24 * 60 * 60 * 1000, // 1 day
|
|
removeStartTags: "",
|
|
removeEndTags: "",
|
|
startTags: [],
|
|
endTags: [],
|
|
prohibitedWords: [],
|
|
scrollLength: 500,
|
|
logFeedWarnings: false,
|
|
dangerouslyDisableAutoEscaping: false
|
|
},
|
|
|
|
getUrlPrefix (item) {
|
|
if (item.useCorsProxy) {
|
|
return `${location.protocol}//${location.host}/cors?url=`;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
// Define required scripts.
|
|
getScripts () {
|
|
return ["moment.js"];
|
|
},
|
|
|
|
//Define required styles.
|
|
getStyles () {
|
|
return ["newsfeed.css"];
|
|
},
|
|
|
|
// Define required translations.
|
|
getTranslations () {
|
|
// The translations for the default modules are defined in the core translation files.
|
|
// Therefor we can just return false. Otherwise we should have returned a dictionary.
|
|
// If you're trying to build your own module including translations, check out the documentation.
|
|
return false;
|
|
},
|
|
|
|
// Define start sequence.
|
|
start () {
|
|
Log.info(`Starting module: ${this.name}`);
|
|
|
|
// Set locale.
|
|
moment.locale(config.language);
|
|
|
|
this.newsItems = [];
|
|
this.loaded = false;
|
|
this.error = null;
|
|
this.activeItem = 0;
|
|
this.scrollPosition = 0;
|
|
|
|
this.registerFeeds();
|
|
|
|
this.isShowingDescription = this.config.showDescription;
|
|
},
|
|
|
|
// Override socket notification handler.
|
|
socketNotificationReceived (notification, payload) {
|
|
if (notification === "NEWS_ITEMS") {
|
|
this.generateFeed(payload);
|
|
|
|
if (!this.loaded) {
|
|
if (this.config.hideLoading) {
|
|
this.show();
|
|
}
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
|
|
this.loaded = true;
|
|
this.error = null;
|
|
} else if (notification === "NEWSFEED_ERROR") {
|
|
this.error = this.translate(payload.error_type);
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
//Override fetching of template name
|
|
getTemplate () {
|
|
if (this.config.feedUrl) {
|
|
return "oldconfig.njk";
|
|
} else if (this.config.showFullArticle) {
|
|
return "fullarticle.njk";
|
|
}
|
|
return "newsfeed.njk";
|
|
},
|
|
|
|
//Override template data and return whats used for the current template
|
|
getTemplateData () {
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.activeItemCount = this.newsItems.length;
|
|
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
|
if (this.config.showFullArticle) {
|
|
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
|
|
return {
|
|
url: this.getActiveItemURL()
|
|
};
|
|
}
|
|
if (this.error) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
error: this.error
|
|
};
|
|
}
|
|
if (this.newsItems.length === 0) {
|
|
this.activeItemHash = undefined;
|
|
return {
|
|
empty: true
|
|
};
|
|
}
|
|
|
|
const item = this.newsItems[this.activeItem];
|
|
this.activeItemHash = item.hash;
|
|
|
|
const items = this.newsItems.map(function (item) {
|
|
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
|
return item;
|
|
});
|
|
|
|
return {
|
|
loaded: true,
|
|
config: this.config,
|
|
sourceTitle: item.sourceTitle,
|
|
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
|
title: item.title,
|
|
url: this.getActiveItemURL(),
|
|
description: item.description,
|
|
items: items
|
|
};
|
|
},
|
|
|
|
getActiveItemURL () {
|
|
const item = this.newsItems[this.activeItem];
|
|
if (item) {
|
|
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Registers the feeds to be used by the backend.
|
|
*/
|
|
registerFeeds () {
|
|
for (let feed of this.config.feeds) {
|
|
this.sendSocketNotification("ADD_FEED", {
|
|
feed: feed,
|
|
config: this.config
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Generate an ordered list of items for this configured module.
|
|
* @param {object} feeds An object with feeds returned by the node helper.
|
|
*/
|
|
generateFeed (feeds) {
|
|
let newsItems = [];
|
|
for (let feed in feeds) {
|
|
const feedItems = feeds[feed];
|
|
if (this.subscribedToFeed(feed)) {
|
|
for (let item of feedItems) {
|
|
item.sourceTitle = this.titleForFeed(feed);
|
|
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
|
|
newsItems.push(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
newsItems.sort(function (a, b) {
|
|
const dateA = new Date(a.pubdate);
|
|
const dateB = new Date(b.pubdate);
|
|
return dateB - dateA;
|
|
});
|
|
|
|
if (this.config.maxNewsItems > 0) {
|
|
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
|
}
|
|
|
|
if (this.config.prohibitedWords.length > 0) {
|
|
newsItems = newsItems.filter(function (item) {
|
|
for (let word of this.config.prohibitedWords) {
|
|
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}, this);
|
|
}
|
|
newsItems.forEach((item) => {
|
|
//Remove selected tags from the beginning of rss feed items (title or description)
|
|
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.title.slice(0, startTag.length) === startTag) {
|
|
item.title = item.title.slice(startTag.length, item.title.length);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
|
if (this.isShowingDescription) {
|
|
for (let startTag of this.config.startTags) {
|
|
if (item.description.slice(0, startTag.length) === startTag) {
|
|
item.description = item.description.slice(startTag.length, item.description.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Remove selected tags from the end of rss feed items (title or description)
|
|
if (this.config.removeEndTags) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.title.slice(-endTag.length) === endTag) {
|
|
item.title = item.title.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
|
|
if (this.isShowingDescription) {
|
|
for (let endTag of this.config.endTags) {
|
|
if (item.description.slice(-endTag.length) === endTag) {
|
|
item.description = item.description.slice(0, -endTag.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// get updated news items and broadcast them
|
|
const updatedItems = [];
|
|
newsItems.forEach((value) => {
|
|
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
|
|
// Add item to updated items list
|
|
updatedItems.push(value);
|
|
}
|
|
});
|
|
|
|
// check if updated items exist, if so and if we should broadcast these updates, then lets do so
|
|
if (this.config.broadcastNewsUpdates && updatedItems.length > 0) {
|
|
this.sendNotification("NEWS_FEED_UPDATE", { items: updatedItems });
|
|
}
|
|
|
|
this.newsItems = newsItems;
|
|
},
|
|
|
|
/**
|
|
* Check if this module is configured to show this feed.
|
|
* @param {string} feedUrl Url of the feed to check.
|
|
* @returns {boolean} True if it is subscribed, false otherwise
|
|
*/
|
|
subscribedToFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns title for the specific feed url.
|
|
* @param {string} feedUrl Url of the feed
|
|
* @returns {string} The title of the feed
|
|
*/
|
|
titleForFeed (feedUrl) {
|
|
for (let feed of this.config.feeds) {
|
|
if (feed.url === feedUrl) {
|
|
return feed.title || "";
|
|
}
|
|
}
|
|
return "";
|
|
},
|
|
|
|
/**
|
|
* Schedule visual update.
|
|
*/
|
|
scheduleUpdateInterval () {
|
|
this.updateDom(this.config.animationSpeed);
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
|
|
// #2638 Clear timer if it already exists
|
|
if (this.timer) clearInterval(this.timer);
|
|
|
|
this.timer = setInterval(() => {
|
|
|
|
/*
|
|
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
|
|
* (Animating from a headline to itself is unsightly.)
|
|
* Cases:
|
|
*
|
|
* Number of items | Number of items | Display
|
|
* at last update | right now | Behaviour
|
|
* ----------------------------------------------------
|
|
* 0 | 0 | do not update
|
|
* 0 | >0 | update
|
|
* 1 | 0 or >1 | update
|
|
* 1 | 1 | update only if item details (hash value) changed
|
|
* >1 | any | update
|
|
*
|
|
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
|
|
*/
|
|
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
|
|
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
|
|
this.updateDom(this.config.animationSpeed);
|
|
}
|
|
|
|
// Broadcast NewsFeed if needed
|
|
if (this.config.broadcastNewsFeeds) {
|
|
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
|
}
|
|
}, this.config.updateInterval);
|
|
},
|
|
|
|
resetDescrOrFullArticleAndTimer () {
|
|
this.isShowingDescription = this.config.showDescription;
|
|
this.config.showFullArticle = false;
|
|
this.scrollPosition = 0;
|
|
// reset bottom bar alignment
|
|
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
|
|
if (!this.timer) {
|
|
this.scheduleUpdateInterval();
|
|
}
|
|
},
|
|
|
|
notificationReceived (notification, payload, sender) {
|
|
const before = this.activeItem;
|
|
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
|
this.hide();
|
|
} else if (notification === "ARTICLE_NEXT") {
|
|
this.activeItem++;
|
|
if (this.activeItem >= this.newsItems.length) {
|
|
this.activeItem = 0;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_PREVIOUS") {
|
|
this.activeItem--;
|
|
if (this.activeItem < 0) {
|
|
this.activeItem = this.newsItems.length - 1;
|
|
}
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
|
|
this.updateDom(100);
|
|
}
|
|
// if "more details" is received the first time: show article summary, on second time show full article
|
|
else if (notification === "ARTICLE_MORE_DETAILS") {
|
|
// full article is already showing, so scrolling down
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition += this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling down`);
|
|
Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_SCROLL_UP") {
|
|
if (this.config.showFullArticle === true) {
|
|
this.scrollPosition -= this.config.scrollLength;
|
|
window.scrollTo(0, this.scrollPosition);
|
|
Log.debug(`${this.name} - scrolling up`);
|
|
Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
|
|
}
|
|
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
Log.debug(`${this.name} - showing only article titles again`);
|
|
this.updateDom(100);
|
|
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
|
if (this.config.showFullArticle) {
|
|
this.activeItem++;
|
|
this.resetDescrOrFullArticleAndTimer();
|
|
} else {
|
|
this.showFullArticle();
|
|
}
|
|
} else if (notification === "ARTICLE_INFO_REQUEST") {
|
|
this.sendNotification("ARTICLE_INFO_RESPONSE", {
|
|
title: this.newsItems[this.activeItem].title,
|
|
source: this.newsItems[this.activeItem].sourceTitle,
|
|
date: this.newsItems[this.activeItem].pubdate,
|
|
desc: this.newsItems[this.activeItem].description,
|
|
url: this.getActiveItemURL()
|
|
});
|
|
}
|
|
},
|
|
|
|
showFullArticle () {
|
|
this.isShowingDescription = !this.isShowingDescription;
|
|
this.config.showFullArticle = !this.isShowingDescription;
|
|
// make bottom bar align to top to allow scrolling
|
|
if (this.config.showFullArticle === true) {
|
|
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
|
|
}
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
|
|
this.updateDom(100);
|
|
}
|
|
});
|