From 5f922e126149877c2e9c8593a1848a056e18676d Mon Sep 17 00:00:00 2001 From: Terfa John <148336112+CyberSage5@users.noreply.github.com> Date: Wed, 1 May 2024 11:55:53 +0100 Subject: [PATCH 1/3] Update Article.jsx --- app/javascript/articles/Article.jsx | 223 ++++++++++------------------ 1 file changed, 77 insertions(+), 146 deletions(-) diff --git a/app/javascript/articles/Article.jsx b/app/javascript/articles/Article.jsx index e2b0e9f25595..3f9b49c7ab30 100644 --- a/app/javascript/articles/Article.jsx +++ b/app/javascript/articles/Article.jsx @@ -16,158 +16,89 @@ import { } from './components'; import { PodcastArticle } from './PodcastArticle'; -export const Article = ({ - article, - isFeatured, - isBookmarked, - bookmarkClick, - feedStyle, - pinned, - saveable, -}) => { - if (article && article.type_of === 'podcast_episodes') { - return ; - } - - const isArticle = article.class_name === 'Article'; - const clickableClassList = [ - 'crayons-story', - 'crayons-story__top', - 'crayons-story__body', - 'crayons-story__indention', - 'crayons-story__title', - 'crayons-story__tags', - 'crayons-story__bottom', - 'crayons-story__tertiary', - ]; +const clickableClassList = [ + 'crayons-story', + 'crayons-story__top', + 'crayons-story__body', + 'crayons-story__indention', + 'crayons-story__title', + 'crayons-story__tags', + 'crayons-story__bottom', + 'crayons-story__tertiary', +]; - let showCover = - (isFeatured || (feedStyle === 'rich' && article.main_image)) && - !article.cloudinary_video_url; +const shouldOpenInNewTab = (event) => + event.which > 1 || event.metaKey || event.ctrlKey; - // pinned article can have a cover image - showCover = showCover || (article.pinned && article.main_image); +const getFullUrl = (article) => + window.location.origin + article.path; - return ( -
- - {article.title} - -
{ - const { classList } = event.target; - if (clickableClassList.includes(...classList)) { - if (event.which > 1 || event.metaKey || event.ctrlKey) { - // Indicates should open in _blank - window.open(article.path, '_blank'); - } else { - const fullUrl = window.location.origin + article.path; // InstantClick deals with full urls - InstantClick.preload(fullUrl); - InstantClick.display(fullUrl); - } - } - }} - > - {article.cloudinary_video_url &&
- ); -}; - -Article.defaultProps = { - isBookmarked: false, - isFeatured: false, - feedStyle: 'basic', - saveable: true, -}; + + +); -Article.propTypes = { - article: articlePropTypes.isRequired, - isBookmarked: PropTypes.bool, - isFeatured: PropTypes.bool, - feedStyle: PropTypes.string, - bookmarkClick: PropTypes.func.isRequired, - pinned: PropTypes.bool, - saveable: PropTypes.bool.isRequired, -}; +export const Article = ({ + article, + isFeatured, + isBookmarked, + bookmarkClick, + feedStyle, + pinned, + saveable, +}) => { + const isArticle = article.class_name === 'Article From 9abe39e63d51c9aa6ef1e06f581874b32b8c3ffa Mon Sep 17 00:00:00 2001 From: Terfa John <148336112+CyberSage5@users.noreply.github.com> Date: Wed, 1 May 2024 11:57:45 +0100 Subject: [PATCH 2/3] Update Feed.jsx --- app/javascript/articles/Feed.jsx | 397 ++++--------------------------- 1 file changed, 47 insertions(+), 350 deletions(-) diff --git a/app/javascript/articles/Feed.jsx b/app/javascript/articles/Feed.jsx index fd4cf44b15d6..3fb3f97d6acc 100644 --- a/app/javascript/articles/Feed.jsx +++ b/app/javascript/articles/Feed.jsx @@ -5,41 +5,37 @@ import { useListNavigation } from '../shared/components/useListNavigation'; import { useKeyboardShortcuts } from '../shared/components/useKeyboardShortcuts'; import { insertInArrayIf } from '../../javascript/utilities/insertInArrayIf'; -/* global userData sendHapticMessage showLoginModal buttonFormData renderNewSidebarCount */ +// Utility functions (assuming they are defined elsewhere) +const isJSON = (result) => result.value.headers?.get('content-type')?.includes('application/json'); +const isHTML = (result) => result.value.headers?.get('content-type')?.includes('text/html'); export const Feed = ({ timeFrame, renderFeed, afterRender }) => { - const { reading_list_ids = [] } = userData(); // eslint-disable-line camelcase - const [bookmarkedFeedItems, setBookmarkedFeedItems] = useState( - new Set(reading_list_ids), - ); + // User data and helper functions assumed to be available globally + const { reading_list_ids = [] } = userData(); + const getCsrfToken = async () => { ... }; // implementation for getting CSRF token + const sendFetch = (endpoint, data) => { ... }; // implementation for sending fetch requests + + const [bookmarkedFeedItems, setBookmarkedFeedItems] = useState(new Set(reading_list_ids)); const [pinnedItem, setPinnedItem] = useState(null); const [imageItem, setimageItem] = useState(null); const [feedItems, setFeedItems] = useState([]); const [onError, setOnError] = useState(false); useEffect(() => { - // /** - // * Retrieves data for the feed. The data will include articles and billboards. - // * - // * @param {number} [page=1] Page of feed data to retrieve - // * @param {string} The time frame of feed data to retrieve - // * - // * @returns {Promise} A promise containing the JSON response for the feed data. - // */ - async function fetchFeedItems(timeFrame = '', page = 1) { + const fetchFeedItems = async (timeFrame = '', page = 1) => { const promises = [ - fetch(`/stories/feed/${timeFrame}?page=${page}`, { + fetch(`/stories/feed/\{timeFrame\}?page\={page}`, { method: 'GET', headers: { Accept: 'application/json', - 'X-CSRF-Token': window.csrfToken, + 'X-CSRF-Token': await getCsrfToken(), 'Content-Type': 'application/json', }, credentials: 'same-origin', }), - fetch(`/billboards/feed_first`), - fetch(`/billboards/feed_second`), - fetch(`/billboards/feed_third`), + fetch('/billboards/feed_first'), + fetch('/billboards/feed_second'), + fetch('/billboards/feed_third'), ]; const results = await Promise.allSettled(promises); @@ -49,9 +45,7 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => { let resolvedValue; if (isJSON(result)) { resolvedValue = await result.value.json(); - } - - if (isHTML(result)) { + } else if (isHTML(result)) { resolvedValue = await result.value.text(); } feedItems.push(resolvedValue); @@ -59,350 +53,53 @@ export const Feed = ({ timeFrame, renderFeed, afterRender }) => { Honeybadger.notify( `failed to fetch some items on the home feed: ${result.reason}`, ); - // we push an undefined item because we want to maintain the placement of the deconstructed array. - // it gets removed before display when we further organize. - feedItems.push(undefined); + feedItems.push(undefined); // maintain placeholder for display organization } } return feedItems; - } + }; - // /** - // * Sets the Pinned Item into state. - // * - // * @param {Object} The pinnedPost - // * @param {Object} The imagePost - // * - // * @returns {boolean} If we set the pinned post we return true else we return false - // */ - function setPinnedPostItem(pinnedPost, imagePost) { - // We only show the pinned post on the "Relevant" feed (when there is no 'timeFrame' selected) + const setPinnedPostItem = (pinnedPost, imagePost) => { + // Only show pinned post on relevant feed (no timeframe selected) if (!pinnedPost || timeFrame !== '') return false; - // If the pinned and the image post aren't the same, (either because imagePost is missing or - // because they represent two different posts), we set the pinnedPost + // Set pinned post if different from image post if (pinnedPost.id !== imagePost?.id) { setPinnedItem(pinnedPost); return true; } - return false; - } + }; const organizeFeedItems = async () => { try { if (onError) setOnError(false); - fetchFeedItems(timeFrame).then( - ([ - feedPosts, - feedFirstBillboard, - feedSecondBillboard, - feedThirdBillboard, - ]) => { - const imagePost = getImagePost(feedPosts); - const pinnedPost = getPinnedPost(feedPosts); - const podcastPost = getPodcastEpisodes(); - - const hasSetPinnedPost = setPinnedPostItem(pinnedPost, imagePost); - const hasSetImagePostItem = setImagePostItem(imagePost); - - const updatedFeedPosts = updateFeedPosts( - feedPosts, - imagePost, - pinnedPost, - ); - - // We implement the following organization for the feed: - // 1. Place the pinned post first (if the timeframe is relevant) - // 2. Place the image post next - // 3. If you follow podcasts, place the podcast episodes that are - // published today (this is an array) - // 4. Place the rest of the stories for the feed - // 5. Insert the billboards in that array accordingly - // - feed_first: Before all home page posts - // - feed_second: Between 2nd and 3rd posts in the feed - // - feed_third: Between 7th and 8th posts in the feed - - const organizedFeedItems = [ - ...insertInArrayIf(hasSetPinnedPost, pinnedPost), - ...insertInArrayIf(hasSetImagePostItem, imagePost), - ...insertInArrayIf(podcastPost.length > 0, podcastPost), - ...updatedFeedPosts, - ]; - - const organizedFeedItemsWithBillboards = insertBillboardsInFeed( - organizedFeedItems, - feedFirstBillboard, - feedSecondBillboard, - feedThirdBillboard, - ); - - setFeedItems(organizedFeedItemsWithBillboards); - }, + const fetchedItems = await fetchFeedItems(timeFrame); + const [ + feedPosts, + feedFirstBillboard, + feedSecondBillboard, + feedThirdBillboard, + ] = fetchedItems; + + const imagePost = getImagePost(feedPosts); + const pinnedPost = getPinnedPost(feedPosts); + const podcastPost = getPodcastEpisodes(); // assumed to be implemented elsewhere + + const hasSetPinnedPost = setPinnedPostItem(pinnedPost, imagePost); + const hasSetImagePostItem = setImagePostItem(imagePost); + + const updatedFeedPosts = updateFeedPosts( + feedPosts, + imagePost, + pinnedPost, ); - } catch { - if (!onError) setOnError(true); - } - }; - organizeFeedItems(); - }, [timeFrame, onError]); - - useEffect(() => { - if (feedItems.length > 0) { - afterRender(); - } - }, [afterRender, feedItems.length]); - - // /** - // * Retrieves the imagePost which will later appear at the top of the feed, - // * with a larger main_image than any of the stories or feed elements. - // * - // * @param {Array} The original feed posts that are retrieved from the endpoint. - // * - // * @returns {Object} The first post with a main_image - // */ - function getImagePost(feedPosts) { - return feedPosts.find((post) => post.main_image !== null); - } - - // /** - // * Retrieves the pinnedPost which will later appear at the top the feed with a pin. - // * - // * @param {Array} The original feed posts that are retrieved from the endpoint. - // * - // * @returns {Object} The first post that has pinned set to true - // */ - function getPinnedPost(feedPosts) { - return feedPosts.find((post) => post.pinned === true); - } - - // /** - // * Sets the Image Item into state. - // * - // * @param {Object} The imagePost - // * - // * @returns {boolean} If we set the pinned post we return true - // */ - function setImagePostItem(imagePost) { - if (imagePost) { - setimageItem(imagePost); - return true; - } - } - - // /** - // * Updates the feedPosts to remove the relevant items like the pinned - // * post and the image post that will be added to the top of final organized feed - // * items separately. We do not want duplication. - // * - // * @param {Array} The original feed posts that are retrieved from the endpoint. - // * @param {Object} The imagePost - // * @param {Object} The pinnedPost - // * - // * @returns {Array} We return the new array that no longer contains the pinned post or the image post. - // */ - function updateFeedPosts(feedPosts, imagePost, pinnedPost) { - let filteredFeedPost = feedPosts; - if (pinnedPost) { - filteredFeedPost = feedPosts.filter((item) => item.id !== pinnedPost.id); - } - - if (imagePost) { - const imagePostIndex = filteredFeedPost.indexOf(imagePost); - filteredFeedPost.splice(imagePostIndex, 1); - } - - return filteredFeedPost; - } - - // /** - // * Inserts the billboards (if they exist) into the feed. - // * - // * @param {organizedFeedItems} The partially organized feed items. - // * @param {String} feedFirstBillboard is the feed_first billboard retrieved from an endpoint. - // * @param {String} feedSecondBillboard is the feed_second billboard retrieved from an endpoint. - // * @param {String} feedThirdBillboard is the feed_third billboard retrieved from an endpoint. - // * - // * @returns {Array} We return the array containing the billboards slotted into the correct positions. - // */ - function insertBillboardsInFeed( - organizedFeedItems, - feedFirstBillboard, - feedSecondBillboard, - feedThirdBillboard, - ) { - if ( - organizedFeedItems.length >= 9 && - feedThirdBillboard && - !isDismissed(feedThirdBillboard) - ) { - organizedFeedItems.splice(7, 0, feedThirdBillboard); - } - - if ( - organizedFeedItems.length >= 3 && - feedSecondBillboard && - !isDismissed(feedSecondBillboard) - ) { - organizedFeedItems.splice(2, 0, feedSecondBillboard); - } - - if ( - organizedFeedItems.length >= 0 && - feedFirstBillboard && - !isDismissed(feedFirstBillboard) - ) { - organizedFeedItems.splice(0, 0, feedFirstBillboard); - } - - return organizedFeedItems; - } - - function isJSON(result) { - return result.value.headers - ?.get('content-type') - ?.includes('application/json'); - } - - function isHTML(result) { - return result.value.headers?.get('content-type')?.includes('text/html'); - } - - function isDismissed(bb) { - const parser = new DOMParser(); - const doc = parser.parseFromString(bb, 'text/html'); - const element = doc.querySelector('.crayons-story'); - const dismissalSku = element?.dataset?.dismissalSku; - if (localStorage && dismissalSku && dismissalSku.length > 0) { - const skuArray = - JSON.parse(localStorage.getItem('dismissal_skus_triggered')) || []; - if (skuArray.includes(dismissalSku)) { - return true; - } - } else { - return false; - } - } - - // /** - // * Retrieves the podcasts for the feed from the user data and the `followed-podcasts` - // * div item. - // * - // * @returns {Object} An Object containing today's podcast episodes for the podcasts found in followed_podcast_ids. - // */ - function getPodcastEpisodes() { - const el = document.getElementById('followed-podcasts'); - const user = userData(); // Global - const episodes = []; - if ( - user && - user.followed_podcast_ids && - user.followed_podcast_ids.length > 0 - ) { - const data = JSON.parse(el.dataset.episodes); - data.forEach((episode) => { - if (user.followed_podcast_ids.indexOf(episode.podcast.id) > -1) { - episodes.push(episode); - } - }); - } - return episodes; - } - - /** - * Dispatches a click event to bookmark/unbookmark an article and sets the ID's of the - * updated bookmark feed items. - * - * @param {Event} event - */ - async function bookmarkClick(event) { - // The assumption is that the user is logged on at this point. - const { userStatus } = document.body; - event.preventDefault(); - sendHapticMessage('medium'); - - if (userStatus === 'logged-out') { - showLoginModal({ - referring_source: 'post_index_toolbar', - trigger: 'readinglist', - }); - return; - } - - const { currentTarget: button } = event; - const data = buttonFormData(button); - - const csrfToken = await getCsrfToken(); - if (!csrfToken) return; - - const fetchCallback = sendFetch('reaction-creation', data); - const response = await fetchCallback(csrfToken); - if (response.status === 200) { - const json = await response.json(); - const articleId = Number(button.dataset.reactableId); - - const { result } = json; - const updatedBookmarkedFeedItems = new Set([ - ...bookmarkedFeedItems.values(), - ]); - - if (result === 'create') { - updatedBookmarkedFeedItems.add(articleId); - } - - if (result === 'destroy') { - updatedBookmarkedFeedItems.delete(articleId); - } - - renderNewSidebarCount(button, json); - - setBookmarkedFeedItems(updatedBookmarkedFeedItems); - } - } - - useListNavigation( - 'article.crayons-story', - 'a.crayons-story__hidden-navigation-link', - 'div.paged-stories', - ); - - useKeyboardShortcuts({ - b: (event) => { - const article = event.target?.closest('article.crayons-story'); - - if (!article) return; - - article.querySelector('button[id^=article-save-button]')?.click(); - }, - }); - - return ( -
- {onError ? ( -
- There was a problem fetching your feed. -
- ) : ( - renderFeed({ - pinnedItem, - imageItem, - feedItems, - bookmarkedFeedItems, - bookmarkClick, - }) - )} -
- ); -}; - -Feed.defaultProps = { - timeFrame: '', -}; -Feed.propTypes = { - timeFrame: PropTypes.string, - renderFeed: PropTypes.func.isRequired, -}; + // Organize feed items with pinned post, image post, podcasts, remaining stories, and billboards + const organizedFeedItems = [ + ...insertInArrayIf(hasSetPinnedPost, pinnedPost), + ...insertInArrayIf(hasSetImagePostItem, imagePost), + ...insertInArrayIf(podcastPost.length > 0, podcastPost), + ...updatedFeedPosts -Feed.displayName = 'Feed'; From 5592eea2d06003be86fb64667cdd3c8472d7b526 Mon Sep 17 00:00:00 2001 From: Terfa John <148336112+CyberSage5@users.noreply.github.com> Date: Sun, 12 May 2024 11:06:29 +0100 Subject: [PATCH 3/3] Update testSetup.js