From db96a23445721216fba1cb9bdf5b72d55a5a9a29 Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 14:58:02 +0200 Subject: [PATCH 1/9] Update script.js - moved some Settings to config.json --- script.js | 300 +++++++++++++++++++++++------------------------------- 1 file changed, 129 insertions(+), 171 deletions(-) diff --git a/script.js b/script.js index 24f0264..1fd9609 100644 --- a/script.js +++ b/script.js @@ -1,47 +1,5 @@ let existingPosts = []; -// Function to calculate relative time -const timeAgo = function(date) { - let seconds = Math.floor((new Date() - date) / 1000); - let interval = seconds / 31536000; - if (interval > 1) { - return Math.floor(interval) + " years ago"; - } - interval = seconds / 2592000; - if (interval > 1) { - return Math.floor(interval) + " months ago"; - } - interval = seconds / 86400; - if (interval > 1) { - return Math.floor(interval) + " days ago"; - } - interval = seconds / 3600; - if (interval > 1) { - return Math.floor(interval) + " hours ago"; - } - interval = seconds / 60; - if (interval > 1) { - return Math.floor(interval) + " minutes ago"; - } - return Math.floor(seconds) + " seconds ago"; -} - -// Function to update times -const updateTimes = function() { - // Find each timestamp element in the DOM - $('.card-text a').each(function() { - // Get the original date of the post - let date = new Date($(this).attr('data-time')); - - // Calculate the new relative time - let newTimeAgo = timeAgo(date); - - // Update the timestamp with the new relative time - $(this).text(newTimeAgo); - }); -}; - -// Function to get a parameter by name from URL function getUrlParameter(name) { name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); @@ -49,163 +7,163 @@ function getUrlParameter(name) { return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); } -// Get hashtags from URL parameters -let hashtags = getUrlParameter('hashtags'); +const secondsAgo = date => Math.floor((new Date() - date) / 1000); +const timeAgo = function(seconds) { + const intervals = [ + { limit: 31536000, text: 'years' }, + { limit: 2592000, text: 'months' }, + { limit: 86400, text: 'days' }, + { limit: 3600, text: 'hours' }, + { limit: 60, text: 'minutes' } + ]; -// Split the hashtags string into an array -let hashtagsArray = hashtags.split(','); + for (let interval of intervals) { + if (seconds >= interval.limit) { + return Math.floor(seconds / interval.limit) + ` ${interval.text} ago`; + } + } + return Math.floor(seconds) + " seconds ago"; +}; - -// Get server from URL parameters or use default -let server = getUrlParameter('server') || 'https://mastodon.social'; - -// Function to fetch posts for a given hashtag -const getPosts = function(hashtag) { - return $.get(`${server}/api/v1/timelines/tag/${hashtag}?limit=20`); +const fetchConfig = async function() { + try { + const config = await $.getJSON('config.json'); + $('#navbar-brand').text(config.navbarBrandText); + return config.defaultServerUrl; + } catch (error) { + console.error("Error loading config.json:", error); + } } +const fetchPosts = async function(serverUrl, hashtag) { + try { + const posts = await $.get(`${serverUrl}/api/v1/timelines/tag/${hashtag}?limit=20`); + return posts; + } catch (error) { + console.error(`Error loading posts for hashtag #${hashtag}:`, error); + } +}; -// Function to fetch and display posts -const fetchAndDisplayPosts = function() { - - // Fetch posts for each hashtag - $.when(...hashtagsArray.map(hashtag => getPosts(hashtag))).then(function(...hashtagPosts) { - let allPosts; - - // Check if there are multiple hashtags or just one - if (hashtagsArray.length > 1) { - // If there are multiple hashtags, `hashtagPosts` is an array of arrays - // We use Array.prototype.flat() to combine them into one array - allPosts = hashtagPosts.map(postData => postData[0]).flat(); - } else { - // If there's only one hashtag, `hashtagPosts` is a single array - allPosts = hashtagPosts[0]; - } - - // Sort the posts by date/time - allPosts.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - - // Loop through the sorted posts - $.each(allPosts, function(i, post) { - // Check if the post is not already displayed and is not a mention - if (!existingPosts.includes(post.id) && post.in_reply_to_id === null) { - // Add the post id to existingPosts - existingPosts.push(post.id); - - let cardHTML = ` -
-
- -

${post.content}

- ${post.media_attachments.length > 0 ? `Image` : ''} -

${timeAgo(new Date(post.created_at))}

-
-
- `; - - // Convert the HTML string into a jQuery object - let $card = $(cardHTML); - - // Prepend the new card to the wall - $('#wall').prepend($card); - - // Refresh Masonry layout after all new cards have been added - $('.masonry-grid').masonry('prepended', $card); - } - }); +const updateTimesOnPage = function() { + $('.card-text a').each(function() { + const date = new Date($(this).attr('data-time')); + const newTimeAgo = timeAgo(secondsAgo(date)); + $(this).text(newTimeAgo); }); }; -$(document).ready(function() { - // Initialize Masonry +const displayPost = function(post) { + if (existingPosts.includes(post.id) || post.in_reply_to_id !== null) return; + + existingPosts.push(post.id); + + let cardHTML = ` +
+
+
+ +

${post.account.display_name}

+
+ ${post.media_attachments[0] ? `` : ''} +

${post.content}

+ ${post.spoiler_text ? `

${post.spoiler_text}

` : ''} +

${timeAgo(secondsAgo(new Date(post.created_at)))}

+
+
+ `; + + let $card = $(cardHTML); + $('#wall').prepend($card); + $('.masonry-grid').masonry('prepended', $card); +}; + +const updateWall = function(posts) { + if (!posts || posts.length === 0) return; + + posts.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + posts.forEach(post => displayPost(post)); +}; + +const updateHashtagsOnPage = function(hashtagsArray) { + $('#hashtag-display').text(hashtagsArray.length > 0 ? `${hashtagsArray.map(hashtag => `#${hashtag}`).join(' ')}` : 'No hashtags set'); +}; + +const handleHashtagDisplayClick = function(serverUrl) { + $('#app-content').addClass('d-none'); + $('#zero-state').removeClass('d-none'); + + const currentHashtags = getUrlParameter('hashtags').split(','); + + for (let i = 0; i < currentHashtags.length; i++) { + $(`#hashtag${i+1}`).val(currentHashtags[i]); + } + + $('#serverUrl').val(serverUrl); +}; + +const handleHashtagFormSubmit = function(e, hashtagsArray) { + e.preventDefault(); + + let hashtags = [ + $('#hashtag1').val(), + $('#hashtag2').val(), + $('#hashtag3').val() + ]; + + hashtags = hashtags.filter(function(hashtag) { + return hashtag !== '' && /^[\w]+$/.test(hashtag); + }); + + let serverUrl = $('#serverUrl').val(); + + if (!/^https:\/\/[\w.\-]+\/?$/.test(serverUrl)) { + alert('Invalid server URL.'); + return; + } + + const newUrl = window.location.origin + window.location.pathname + `?hashtags=${hashtags.join(',')}&server=${serverUrl}`; + + window.location.href = newUrl; +}; + +$(document).ready(async function() { + const defaultServerUrl = await fetchConfig(); $('.masonry-grid').masonry({ itemSelector: '.col-sm-3', columnWidth: '.col-sm-3', percentPosition: true }); - // Re-arrange Masonry layout every 30 seconds setInterval(function() { $('.masonry-grid').masonry('layout'); }, 10000); - // Event listener for clicking on the hashtags + const hashtags = getUrlParameter('hashtags'); + const hashtagsArray = hashtags ? hashtags.split(',') : []; + const serverUrl = getUrlParameter('server') || defaultServerUrl; + $('#hashtag-display').on('click', function() { - // Hide the main app content - $('#app-content').addClass('d-none'); - - // Show the form screen - $('#zero-state').removeClass('d-none'); - - // Get the current hashtags - let currentHashtags = $(this).text().split(' '); - - // Pre-fill the form fields with the current hashtags - for (let i = 0; i < currentHashtags.length; i++) { - $(`#hashtag${i+1}`).val(currentHashtags[i].substring(1)); // Remove the leading '#' - } - - // Pre-fill the server field with the current server - $('#serverUrl').val(server); + handleHashtagDisplayClick(serverUrl); }); - // Check if hashtags are provided - if (hashtagsArray[0] !== '') { - // Fetch posts for each hashtag on page load - fetchAndDisplayPosts(); - - // Fetch posts for each hashtag every 10 seconds - setInterval(fetchAndDisplayPosts, 10000); + if (hashtagsArray.length > 0 && hashtagsArray[0] !== '') { + const allPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag))); + updateWall(allPosts.flat()); + setInterval(async function() { + const newPosts = await Promise.all(hashtagsArray.map(hashtag => fetchPosts(serverUrl, hashtag))); + updateWall(newPosts.flat()); + }, 10000); } else { - // Show the zero state and hide the app content $('#zero-state').removeClass('d-none'); $('#app-content').addClass('d-none'); } - // Update the navbar info with the provided hashtags - $('#hashtag-display').text(`${hashtagsArray.map(hashtag => `#${hashtag}`).join(' ')}`); + updateHashtagsOnPage(hashtagsArray); - - // Handle the form submit event $('#hashtag-form').on('submit', function(e) { - // Prevent the default form submission - e.preventDefault(); - - // Get the entered hashtags - let hashtags = [ - $('#hashtag1').val(), - $('#hashtag2').val(), - $('#hashtag3').val() - ]; - - // Filter out any empty strings and validate hashtag format - hashtags = hashtags.filter(function(hashtag) { - return hashtag !== '' && /^[\w]+$/.test(hashtag); - }); - - // Get the entered server URL - let serverUrl = $('#serverUrl').val(); - - // Validate server URL format - if (!/^https:\/\/[\w.\-]+\/?$/.test(serverUrl)) { - alert('Invalid server URL.'); - return; - } - - // Create a new URL with the entered hashtags and server URL - let newUrl = window.location.origin + window.location.pathname + `?hashtags=${hashtags.join(',')}&server=${serverUrl}`; - - // Reload the page with the new URL - window.location.href = newUrl; + handleHashtagFormSubmit(e, hashtagsArray); }); - - // Update the times once when the page loads - updateTimes(); - - // Then update every 60 seconds - setInterval(updateTimes, 60000); + updateTimesOnPage(); + setInterval(updateTimesOnPage, 60000); }); From 1a9e6b7037885981b383d9e14a2d8bf7b9c8fd20 Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 14:58:33 +0200 Subject: [PATCH 2/9] Update styles.css --- styles.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/styles.css b/styles.css index 8b62c86..0b55c9a 100644 --- a/styles.css +++ b/styles.css @@ -114,6 +114,11 @@ body { } } +.avatar-img { + width: 50px; + height: 50px; +} + .container { max-width: 2000px !important; } From 7499ce065d1ec9142a4b368f0b618fcafec4ff5b Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 14:58:57 +0200 Subject: [PATCH 3/9] Update index.html --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 48d6cf3..0920b9f 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@ From 8a96ca349b34da3c1363b0a334e858185804feef Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 14:59:59 +0200 Subject: [PATCH 4/9] Config file - First iteration of a simple config file --- config.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 config.json diff --git a/config.json b/config.json new file mode 100644 index 0000000..b0d179b --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "navbarBrandText": "Mastodon Wall", + "defaultServerUrl": "https://mastodon.social", + "navbarBackgroundColor": "#222222" +} From 037868492bb2936425ed20848b69749d7844556f Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 16:15:04 +0200 Subject: [PATCH 5/9] Update config.json - option to set the header bar color - option to also display replies --- config.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index b0d179b..b99b0c2 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,6 @@ { - "navbarBrandText": "Mastodon Wall", + "navbarBrandText": "Mastowall 1.0 - written by ChatGPT4 - Prompting: Ralf Stockmann (rstockm)", "defaultServerUrl": "https://mastodon.social", - "navbarBackgroundColor": "#222222" + "navbarColor": "#333355", + "includeReplies": false } From 6fde7f73b62c46d0c0e641227d154e57c4430f78 Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 16:15:54 +0200 Subject: [PATCH 6/9] Update index.html --- index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/index.html b/index.html index 0920b9f..8a9c8af 100644 --- a/index.html +++ b/index.html @@ -44,8 +44,6 @@ - -
@@ -55,7 +53,7 @@ Host your own Mastowall - check GitHub - + From b6252bf6b67d41f402eac6d229f019e47eee850d Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 16:16:49 +0200 Subject: [PATCH 7/9] Update script.js - option for also displaying replies - more comments --- script.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index 1fd9609..4ed0a8e 100644 --- a/script.js +++ b/script.js @@ -1,5 +1,7 @@ +// The existingPosts array is used to track already displayed posts let existingPosts = []; +// getUrlParameter helps to fetch URL parameters function getUrlParameter(name) { name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); @@ -7,8 +9,12 @@ function getUrlParameter(name) { return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); } +// secondsAgo calculates how many seconds have passed since the provided date const secondsAgo = date => Math.floor((new Date() - date) / 1000); + +// timeAgo formats the time elapsed in a human readable format const timeAgo = function(seconds) { + // An array of intervals for years, months, days, hours, and minutes. const intervals = [ { limit: 31536000, text: 'years' }, { limit: 2592000, text: 'months' }, @@ -17,6 +23,7 @@ const timeAgo = function(seconds) { { limit: 60, text: 'minutes' } ]; + // Loop through the intervals to find which one is the best fit. for (let interval of intervals) { if (seconds >= interval.limit) { return Math.floor(seconds / interval.limit) + ` ${interval.text} ago`; @@ -25,16 +32,22 @@ const timeAgo = function(seconds) { return Math.floor(seconds) + " seconds ago"; }; +let includeReplies; + +// fetchConfig fetches the configuration from the config.json file const fetchConfig = async function() { try { const config = await $.getJSON('config.json'); $('#navbar-brand').text(config.navbarBrandText); + $('.navbar').css('background-color', config.navbarColor); + includeReplies = config.includeReplies; return config.defaultServerUrl; } catch (error) { console.error("Error loading config.json:", error); } } +// fetchPosts fetches posts from the server using the given hashtag const fetchPosts = async function(serverUrl, hashtag) { try { const posts = await $.get(`${serverUrl}/api/v1/timelines/tag/${hashtag}?limit=20`); @@ -44,6 +57,7 @@ const fetchPosts = async function(serverUrl, hashtag) { } }; +// updateTimesOnPage updates the time information displayed for each post const updateTimesOnPage = function() { $('.card-text a').each(function() { const date = new Date($(this).attr('data-time')); @@ -52,8 +66,9 @@ const updateTimesOnPage = function() { }); }; +// displayPost creates and displays a post const displayPost = function(post) { - if (existingPosts.includes(post.id) || post.in_reply_to_id !== null) return; + if (existingPosts.includes(post.id) || (!includeReplies && post.in_reply_to_id !== null)) return; existingPosts.push(post.id); @@ -77,6 +92,7 @@ const displayPost = function(post) { $('.masonry-grid').masonry('prepended', $card); }; +// updateWall displays all posts const updateWall = function(posts) { if (!posts || posts.length === 0) return; @@ -84,10 +100,12 @@ const updateWall = function(posts) { posts.forEach(post => displayPost(post)); }; +// updateHashtagsOnPage updates the displayed hashtags const updateHashtagsOnPage = function(hashtagsArray) { $('#hashtag-display').text(hashtagsArray.length > 0 ? `${hashtagsArray.map(hashtag => `#${hashtag}`).join(' ')}` : 'No hashtags set'); }; +// handleHashtagDisplayClick handles the event when the hashtag display is clicked const handleHashtagDisplayClick = function(serverUrl) { $('#app-content').addClass('d-none'); $('#zero-state').removeClass('d-none'); @@ -101,6 +119,7 @@ const handleHashtagDisplayClick = function(serverUrl) { $('#serverUrl').val(serverUrl); }; +// handleHashtagFormSubmit handles the submission of the hashtag form const handleHashtagFormSubmit = function(e, hashtagsArray) { e.preventDefault(); @@ -126,6 +145,7 @@ const handleHashtagFormSubmit = function(e, hashtagsArray) { window.location.href = newUrl; }; +// On document ready, the script configures Masonry, handles events, fetches and displays posts $(document).ready(async function() { const defaultServerUrl = await fetchConfig(); $('.masonry-grid').masonry({ From 33925ad9f9c950fda26c522a473fae6187edb20b Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 16:17:17 +0200 Subject: [PATCH 8/9] Update styles.css --- styles.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles.css b/styles.css index 0b55c9a..a7c56a8 100644 --- a/styles.css +++ b/styles.css @@ -65,13 +65,13 @@ } .navbar-brand { - color: #ffbbbb !important; /* change the text color */ + color: rgba(255, 255, 255, 0.8) !important; /* change the text color */ margin: 0 auto; /* center the brand name */ font-size: 0.9em; } .navbar-info { - color: #dddddd !important; /* change the text color */ + color: rgba(255, 255, 255, 0.8) !important; /* change the text color */ margin: 0 auto; /* center the brand name */ font-size: 1.2em; display: block !important; From 8a038f180ec37af1555be09e29770252c77688e4 Mon Sep 17 00:00:00 2001 From: Ralf Stockmann Date: Fri, 19 May 2023 16:32:11 +0200 Subject: [PATCH 9/9] Update README.md 1.0 notes --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cad4871..f71283f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -# Mastowall 0.2 +# Mastowall 1.0 Mastowall is a social wall application that displays posts from the [Mastodon](https://joinmastodon.org/) social network based on specified hashtags. It has been updated with new features to improve its usability and appearance. -image +image Try it live: [Mastowall for the BiblioCon conference]([https://rstockm.github.io/mastowall/?hashtags=111bibliocon,bibliocon,bibliocon23&server=https://openbiblio.social)) Use your own hashtags and server: -image +image + +JSON config file: + +image + ## Features @@ -27,6 +32,10 @@ Use your own hashtags and server: - **Navbar Hashtag Navigation:** Clicking on the hashtags in the navbar takes you to the form screen, allowing you to change the existing hashtags easily. +- **Navbar Color Customization:** The color of the navigation bar can now be customized via 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. + ## Technology Stack Mastowall is built using the following technologies: @@ -57,7 +66,7 @@ Enjoy using Mastowall 0.2! ## AI-Guided Development: A Proof of Concept -Mastowall 0.2 serves as an example of how artificial intelligence can aid and accelerate the software development process. The development of this version of the app was guided by OpenAI's GPT-4, a large language model. +Mastowall may serve as an example of how artificial intelligence can aid and accelerate the software development process. The development of this version of the app was guided by OpenAI's GPT-4, a large language model. In this process, the human developer posed problems, asked questions, and described the desired features and functionalities of the application. GPT-4 then provided solutions, answered queries, generated code snippets, and suggested optimal ways to implement these features.