Compare commits

..

No commits in common. "main" and "for-pi2" have entirely different histories.

11 changed files with 29 additions and 67 deletions

View file

@ -1,10 +1,9 @@
# Mastowall 1.1 # Mastowall 1.1
Forked from [https://github.com/rstockm/mastowall](https://github.com/rstockm/mastowall)
Mastowall is a social wall application that displays posts from the [Mastodon](https://joinmastodon.org/) social network based on specified hashtags. It was written entirely by [ChatGPT4](https://openai.com/product/gpt-4), guided only by text prompts. Mastowall is a social wall application that displays posts from the [Mastodon](https://joinmastodon.org/) social network based on specified hashtags. It was written entirely by [ChatGPT4](https://openai.com/product/gpt-4), guided only by text prompts.
<img width="1348" alt="image" src="https://git.verdigado.com/NB-Public/mastowall/raw/branch/main/screenshot.jpg"> <img width="1348" alt="image" src="https://github.com/rstockm/mastowall/assets/3195116/7060536e-4847-4e38-801e-3c0312b8b194">
Try it live: [Mastowall for BDK23](https://tretkowski.de/mastowall/?hashtags=bdk23,netzbegruenung&server=https://gruene.social) Try it live: [Mastowall for BDK23](https://tretkowski.de/mastowall/?hashtags=bdk23,netzbegruenung&server=https://gruene.social)
@ -38,8 +37,6 @@ JSON config file:
- **Including Replies:** By default, replies are excluded from the wall. However, this behavior can be changed by setting includeReplies to true in the `config.json` file. - **Including Replies:** By default, replies are excluded from the wall. However, this behavior can be changed by setting includeReplies to true in the `config.json` file.
- **Configurable Overlay:** By default only the MastoWall is shown. For use with large displays eg during trade shows, conferences, booths, you can enable the Carousel with enlarged display of the 10 most recent posts. Just add `nbstand=1` to the argument in the URL.
## Technology Stack ## Technology Stack
Mastowall is built using the following technologies: Mastowall is built using the following technologies:
@ -66,7 +63,7 @@ Mastowall is built using the following technologies:
## Sharing via URL ## Sharing via URL
Mastowall supports URL parameters to easily share specific hashtag configurations and the Mastodon server. Simply append the desired hashtags and the server URL to the URL following this format: `?hashtags=hashtag1,hashtag2,hashtag3&server=serverUrl&nbstand=0` Mastowall supports URL parameters to easily share specific hashtag configurations and the Mastodon server. Simply append the desired hashtags and the server URL to the URL following this format: `?hashtags=hashtag1,hashtag2,hashtag3&server=serverUrl`
Enjoy using Mastowall! Enjoy using Mastowall!

View file

@ -6,10 +6,7 @@
"refreshDuration": 30, "refreshDuration": 30,
"maxAge": 604800, "maxAge": 604800,
"extraCards": [ "extraCards": [
"<img src='sharepics/Slide1.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>", "<img src='sharepic.jpg' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>"
"<img src='sharepics/Slide2.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide3.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide4.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>"
], ],
"includeReplies": true "includeReplies": true
} }

View file

@ -38,11 +38,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="hashtag2">Hashtag 2:</label> <label for="hashtag2">Hashtag 2:</label>
<input type="text" class="form-control" id="hashtag2" placeholder="Enter a hashtag" value="bdk24"> <input type="text" class="form-control" id="hashtag2" placeholder="Enter a hashtag" value="bdk23">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="hashtag3">Hashtag 3:</label> <label for="hashtag3">Hashtag 3:</label>
<input type="text" class="form-control" id="hashtag3" placeholder="Enter a hashtag" value="wasjetztzählt"> <input type="text" class="form-control" id="hashtag3" placeholder="Enter a hashtag">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="serverUrl">Server:</label> <label for="serverUrl">Server:</label>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

View file

@ -55,8 +55,6 @@ let duration;
let refresh; let refresh;
// extra cards text // extra cards text
let extraCards; let extraCards;
// toggle Carousel
let withCarousel=false;
// fetchConfig fetches the configuration from the config.json file // fetchConfig fetches the configuration from the config.json file
const fetchConfig = async function() { const fetchConfig = async function() {
@ -79,10 +77,7 @@ const fetchConfig = async function() {
duration = 10000; duration = 10000;
refresh = 30000; refresh = 30000;
extraCards = [ extraCards = [
"<img src='sharepics/Slide1.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>", "<div><img src='sharepic.jpg' style='max-width: 100%;max-height: 100%'></div>"
"<img src='sharepics/Slide2.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide3.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>",
"<img src='sharepics/Slide4.png' style='max-width: 100%;max-height: 100%;vertical-align: middle;'>"
]; ];
return "https://gruene.social"; return "https://gruene.social";
} }
@ -109,14 +104,6 @@ const updateTimesOnPage = function() {
}); });
}; };
// replace certain emojies in some text with images
const replaceEmojies = (text, emojis) => {
emojis.forEach(emoji => {
text = text.replaceAll(`:${emoji.shortcode}:`, `<img class="emoji" src="${emoji.static_url}">`);
});
return text;
};
// displayPost creates and displays a post // displayPost creates and displays a post
const displayPost = function(post) { const displayPost = function(post) {
if (existingPosts.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) return 0; if (existingPosts.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) return 0;
@ -128,14 +115,10 @@ const displayPost = function(post) {
<div class="card m-2 p-2"> <div class="card m-2 p-2">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<img src="${post.account.avatar}" class="avatar-img rounded-circle mr-2"> <img src="${post.account.avatar}" class="avatar-img rounded-circle mr-2">
<p class="m-0">${replaceEmojies(DOMPurify.sanitize(post.account.display_name), post.account.emojis)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p> <p class="m-0">${DOMPurify.sanitize(post.account.display_name)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p>
</div> </div>
${post.media_attachments[0] ? ${post.media_attachments[0] ? `<img src="${post.media_attachments[0].url}" class="card-img-top mb-2">` : ''}
(post.media_attachments[0].url.endsWith('.mp4') ? <p class="card-text">${DOMPurify.sanitize(post.content)}</p>
`<video src="${post.media_attachments[0].url}" controls autoplay muted loop></video>` :
`<img src="${post.media_attachments[0].url}" class="card-img-top mb-2">`) :
''}
<p class="card-text">${replaceEmojies(DOMPurify.sanitize(post.content), post.emojis)}</p>
${post.spoiler_text ? `<p class="card-text text-muted spoiler">${DOMPurify.sanitize(post.spoiler_text)}</p>` : ''} ${post.spoiler_text ? `<p class="card-text text-muted spoiler">${DOMPurify.sanitize(post.spoiler_text)}</p>` : ''}
<p class="card-text text-right"><small class="text-muted"><a class="time" href="${post.url}" target="_blank" data-time="${post.created_at}">${timeAgo(secondsAgo(new Date(post.created_at)))}</a></small></p> <p class="card-text text-right"><small class="text-muted"><a class="time" href="${post.url}" target="_blank" data-time="${post.created_at}">${timeAgo(secondsAgo(new Date(post.created_at)))}</a></small></p>
</div> </div>
@ -150,7 +133,7 @@ const displayPost = function(post) {
const processPosts = function(posts) { const processPosts = function(posts) {
posts = posts.filter((post) => { posts = posts.filter((post) => {
return secondsAgo(new Date(post.created_at)) < maxAge && post.content.indexOf("nitter.") === -1 && post.spoiler_text === "" return secondsAgo(new Date(post.created_at)) < maxAge && post.content.indexOf("nitter.") === -1
}); });
return posts; return posts;
@ -202,22 +185,17 @@ const updateCarousel = function(slides, posts) {
else { else {
newHTML += `<div class="carousel-item" data-interval="${duration}" data-pause="false">`; newHTML += `<div class="carousel-item" data-interval="${duration}" data-pause="false">`;
} }
const postContent = replaceEmojies(DOMPurify.sanitize(post.content), post.emojis); const postContent = DOMPurify.sanitize(post.content);
newHTML += ` newHTML += `
<div class="card-big"> <div class="card-big">
<div class="d-flex align-items-center mb-4"> <div class="d-flex align-items-center mb-4">
<img src="${post.account.avatar}" class="avatar-img-big rounded-circle mr-4"> <img src="${post.account.avatar}" class="avatar-img-big rounded-circle mr-4">
<p class="avatar-name">${replaceEmojies(DOMPurify.sanitize(post.account.display_name), post.account.emojis)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p> <p class="avatar-name">${DOMPurify.sanitize(post.account.display_name)} <span class="user-name">@${DOMPurify.sanitize(post.account.acct)}</span></p>
</div> </div>
<hr> <hr>
<div class="row align-items-center vertical-align-center"> <div class="row align-items-center vertical-align-center">
<div class="${post.media_attachments[0] ? `col-md-6` : `col-md-12`}"><div class="card-text" ${strip(postContent).length > 700 ? `style="font-size: ${strip(postContent).length > 1000 ? `0.5`:`0.9`}em;"`: ``}>${postContent}</div></div> <div class="${post.media_attachments[0] ? `col-md-6` : `col-md-12`}"><div class="card-text" ${strip(postContent).length > 800 ? `style="font-size: 0.9em;"`: ``}>${postContent}</div></div>
${post.media_attachments[0] ? ${post.media_attachments[0] ? `<div class="col-md-6"><img src="${post.media_attachments[0].url}" class="card-img-bottom" align="center"> </div>` : ''}
(post.media_attachments[0].url.endsWith('.mp4') ?
`<div class="col-md-6"><video src="${post.media_attachments[0].url}" controls autoplay muted loop class="card-img-bottom" align="center"></video></div>` :
`<div class="col-md-6"><img src="${post.media_attachments[0].url}" class="card-img-bottom" align="center"> </div>`) :
''}
</div> </div>
<hr> <hr>
<p class="card-text text-right"> <p class="card-text text-right">
@ -232,7 +210,7 @@ const updateCarousel = function(slides, posts) {
newHTML += '</div>'; newHTML += '</div>';
} }
for( let i = 0; i < extraCards.length; i++ ) { for( let i = 0; i < extraCards.length; i++ ) {
newHTML += `<div class="carousel-item" data-interval="${duration * 2}" data-pause="false"> newHTML += `<div class="carousel-item" data-mdb-interval="${duration * 2}" data-mdb-pause="false">
<div className="card-big"> <div className="card-big">
<div class="d-flex align-items-center mb-4"> <div class="d-flex align-items-center mb-4">
${extraCards[i]} ${extraCards[i]}
@ -247,7 +225,6 @@ const updateCarousel = function(slides, posts) {
const showCarousel = function() { const showCarousel = function() {
// show popover // show popover
document.getElementById('popover').style.opacity = '1'; document.getElementById('popover').style.opacity = '1';
document.getElementById('popover').style.display = 'block';
// Activate Carousel // Activate Carousel
$('#myCarousel').carousel("cycle"); $('#myCarousel').carousel("cycle");
} }
@ -258,8 +235,8 @@ const strip = function(html) {
} }
const hideCarousel = function() { const hideCarousel = function() {
// hide popover // show popover
document.getElementById('popover').style.display = 'none'; document.getElementById('popover').style.opacity = '0';
// Activate Carousel // Activate Carousel
} }
@ -286,7 +263,7 @@ const handleHashtagDisplayClick = function(serverUrl) {
const currentHashtags = getUrlParameter('hashtags').split(','); const currentHashtags = getUrlParameter('hashtags').split(',');
if ( currentHashtags = null ) { if ( currentHashtags = null ) {
currentHasttags = "netzbegruenung,bdk24,wasjetztzählt".split(',') currentHasttags = "netzbegruenung,bdk23".split(',')
} }
for (let i = 0; i < currentHashtags.length; i++) { for (let i = 0; i < currentHashtags.length; i++) {
@ -337,16 +314,11 @@ $(document).ready(async function() {
let hashtags = getUrlParameter('hashtags'); let hashtags = getUrlParameter('hashtags');
if ( hashtags == '' ) { if ( hashtags == '' ) {
hashtags = "netzbegruenung,bdk24,wasjetztzählt"; hashtags = "netzbegruenung,bdk23";
} }
const hashtagsArray = hashtags ? hashtags.split(',') : []; const hashtagsArray = hashtags ? hashtags.split(',') : [];
const serverUrl = getUrlParameter('server') || defaultServerUrl; const serverUrl = getUrlParameter('server') || defaultServerUrl;
const enableCarousel = getUrlParameter('nbstand' );
if ( enableCarousel == '1' )
withCarousel = true;
console.log("show carousel: "+withCarousel);
$('#hashtag-display').on('click', function() { $('#hashtag-display').on('click', function() {
handleHashtagDisplayClick(serverUrl); handleHashtagDisplayClick(serverUrl);
}); });
@ -362,16 +334,16 @@ $(document).ready(async function() {
$('.masonry-grid').masonry('layout'); $('.masonry-grid').masonry('layout');
}, 2000); }, 2000);
if ( withCarousel) {
updateCarousel(slides, allPosts.flat()); updateCarousel(slides, allPosts.flat());
setTimeout(async function() { showCarousel(); }, duration);
} setTimeout(async function() { showCarousel(); }, duration)
else setTimeout(async function() { hideCarousel(); }, duration);
setInterval(async function() { setInterval(async function() {
const newPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag))); const newPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag)));
let updated = updateWall(newPosts.flat()); let updated = updateWall(newPosts.flat());
if ( withCarousel && updated > 0 ) { if ( updated > 0 ) {
hideCarousel()
updateCarousel(slides, newPosts.flat()); updateCarousel(slides, newPosts.flat());
setTimeout(async function() { showCarousel(); }, duration)
} }
}, refresh); }, refresh);
} else { } else {

BIN
sharepic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View file

@ -51,9 +51,9 @@
} }
.carousel .card-text { .carousel .card-text {
font-size: 1.6em; font-size: 1.4em;
overflow: hidden; overflow: hidden;
max-height: 50vh; max-height: 60vh;
} }
.card { .card {
@ -208,7 +208,7 @@ body {
.text-muted { .text-muted {
color:#6c757d !important; color:#6c757d !important;
font-size: 1em; font-size: 1.2em;
text-align: right; text-align: right;
} }
@ -224,7 +224,3 @@ body {
.footer a { .footer a {
color: rgba(255, 255, 255, 0.8) !important; color: rgba(255, 255, 255, 0.8) !important;
} }
.emoji {
height: 1em;
}