diff --git a/src/app/assets/stylesheets/app.scss b/src/app/assets/stylesheets/app.scss index 4a30f6d53d..07120a4dfb 100644 --- a/src/app/assets/stylesheets/app.scss +++ b/src/app/assets/stylesheets/app.scss @@ -154,6 +154,10 @@ h1, h2, h3, h4, h5, h6 { line-height: 1.2 !important; } +h6 { + clear: both; +} + .c-sidebar { width: 100%; max-width: 240px; diff --git a/src/app/components/App.jsx b/src/app/components/App.jsx index 2a9cf1556b..c9246e7ab1 100644 --- a/src/app/components/App.jsx +++ b/src/app/components/App.jsx @@ -87,6 +87,13 @@ class App extends React.Component { this.setState({ showBanner: false }); }; + handleScrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }; + render() { const { params, @@ -209,6 +216,13 @@ class App extends React.Component { ) : null} {callout} {children} + diff --git a/src/app/components/App.scss b/src/app/components/App.scss index 95753e98f4..63658a14de 100644 --- a/src/app/components/App.scss +++ b/src/app/components/App.scss @@ -5,7 +5,6 @@ .App__content { margin-top: 1rem; - overflow: hidden; margin-bottom: 60px; padding-bottom: 30px; @@ -237,3 +236,35 @@ } } } + +.scroll-to-top { + display: none; + cursor: pointer; + position: fixed; + bottom: 1em; + right: 0; + width: auto; + padding: 16px; + font-weight: bold; + transition: 0.2s ease; + border-radius: 3px; + user-select: none; + opacity: 30%; + z-index: 100; + @include font-size(18px); + @include themify($themes) { + background-color: themed('buttonBackground'); + color: themed('buttonText'); + } + @include MQ(M) { + display: block; + bottom: 1em; + } +} +.scroll-to-top:hover { + @include themify($themes) { + background-color: themed('buttonBackgroundHover'); + color: themed('buttonTextHover'); + opacity: 80%; + } +} \ No newline at end of file diff --git a/src/app/components/cards/PostFull.jsx b/src/app/components/cards/PostFull.jsx index caadfe97f2..3e81deb123 100644 --- a/src/app/components/cards/PostFull.jsx +++ b/src/app/components/cards/PostFull.jsx @@ -28,8 +28,8 @@ import userIllegalContent from 'app/utils/userIllegalContent'; import ImageUserBlockList from 'app/utils/ImageUserBlockList'; import LoadingIndicator from 'app/components/elements/LoadingIndicator'; import { allowDelete } from 'app/utils/StateFunctions'; -import ContentEditedWrapper from '../elements/ContentEditedWrapper'; import { Role } from 'app/utils/Community'; +import ContentEditedWrapper from '../elements/ContentEditedWrapper'; function TimeAuthorCategory({ post }) { return ( @@ -268,6 +268,51 @@ class PostFull extends React.Component { if (process.env.BROWSER && title) document.title = title + ' — ' + APP_NAME; + if (process.env.BROWSER) { + const canonicalLink = document.getElementById('canonicalUrlID'); + if (!canonicalLink) { + const newCanonicalUrlID = document.createElement('link'); + newCanonicalUrlID.rel = 'canonical'; + newCanonicalUrlID.key = 'canonical'; + newCanonicalUrlID.id = 'canonicalUrlID'; + document.head.appendChild(newCanonicalUrlID); + } + if (canonicalLink) { + let tempCategory = category || ''; + const tags = content.json_metadata.tags || []; + + // Leave Options 1 and 2 uncommented for a combination of both approaches + // Option 1: Replace hive-xxxxxx in the URL with the community name. This doesn't impact non-community posts + const communityTitle = + content.community_title || `#${tempCategory}` || ''; + const sanitizedTitle = communityTitle + .replace(/[^a-zA-Z0-9 ]/g, '') + .trim(); + const urlFriendlyTitle = sanitizedTitle + .replace(/\s+/g, '-') + .toLowerCase(); + if (urlFriendlyTitle) { + tempCategory = urlFriendlyTitle; + } + + // Option 2: Replace hive-xxxxxx in the URL with the first tag of a post + if (tempCategory.startsWith('hive-') && tags.length > 0) { + const firstTag = tags[0].startsWith('#') + ? tags[0].substring(1) + : tags[0]; + tempCategory = + firstTag.startsWith('hive-') && tags.length > 1 + ? tags[1] + : firstTag; // Sometimes the first tag is still the community & need to check if there's a second tag + tempCategory = tempCategory.startsWith('#') + ? tempCategory.substring(1) + : tempCategory; + } + const canonicalURL = `/${tempCategory}/@${author}/${permlink}`; + canonicalLink.href = 'https://' + APP_DOMAIN + canonicalURL; + } + } + let content_body = post.get('body'); const bDMCAStop = DMCAList.includes(link); const bIllegalContentUser = userIllegalContent.includes(author); diff --git a/src/app/components/cards/PostSummary.jsx b/src/app/components/cards/PostSummary.jsx index b31be86e2f..9b9823734f 100644 --- a/src/app/components/cards/PostSummary.jsx +++ b/src/app/components/cards/PostSummary.jsx @@ -97,7 +97,39 @@ class PostSummary extends React.Component { const author = post.get('author'); const permlink = post.get('permlink'); const category = post.get('category'); - const post_url = `/${category}/@${author}/${permlink}`; + + let tempCategory = category || ''; + const tags = post.getIn(['json_metadata', 'tags']) || []; + + // Leave Options 1 and 2 uncommented for a combination of both approaches + // Option 1: Replace hive-xxxxxx in the URL with the community name. This doesn't impact non-community posts + const communityTitle = + post.get('community_title', '#' + tempCategory) || ''; + const sanitizedTitle = communityTitle + .replace(/[^a-zA-Z0-9 ]/g, '') + .trim(); + const urlFriendlyTitle = sanitizedTitle + .replace(/\s+/g, '-') + .toLowerCase(); + if (urlFriendlyTitle) { + tempCategory = urlFriendlyTitle; + } + + // Option 2: Replace hive-xxxxxx in the URL with the first tag of a post + if (tempCategory.startsWith('hive-') && tags && tags.size > 0) { + const firstTag = tags.get(0).startsWith('#') + ? tags.get(0).substring(1) + : tags.get(0); + tempCategory = + firstTag.startsWith('hive-') && tags.size > 1 + ? tags.get(1) + : firstTag; // Sometimes the first tag is still the community & need to check if there's a second tag + tempCategory = tempCategory.startsWith('#') + ? tempCategory.substring(1) + : tempCategory; + } + + const post_url = `/${tempCategory}/@${author}/${permlink}`; const summary = extractBodySummary(post.get('body'), isReply); const keyWord = process.env.BROWSER diff --git a/src/app/components/cards/PostsList.jsx b/src/app/components/cards/PostsList.jsx index 2ec48ff2d0..856c40aaae 100644 --- a/src/app/components/cards/PostsList.jsx +++ b/src/app/components/cards/PostsList.jsx @@ -131,12 +131,19 @@ class PostsList extends React.Component { } updateSlide(index) { + const screenWidth = window.innerWidth; const filteredPosts = this.props.posts.filter(post => post.getIn(['stats', 'is_pinned'], false) ); - const totalSlides = filteredPosts.size - 1; // We reduce the slide count by 1 because we're displaying 2 on a screen and don't want an empty gap at the end + let totalSlides = filteredPosts.size; + if (screenWidth >= 768) { + totalSlides -= 1; // We reduce the slide count by 1 because we're displaying 2 on a screen and don't want an empty gap at the end + } const pinnedPostsElement = document.querySelector('.pinnedPosts'); - const sliderPosition = (index + totalSlides) % totalSlides; + let sliderPosition = 0; + if (totalSlides > 0) { + sliderPosition = (index + totalSlides) % totalSlides; + } this.setState({ currentSlide: sliderPosition }); if (pinnedPostsElement) { requestAnimationFrame(() => { @@ -191,9 +198,8 @@ class PostsList extends React.Component { top: 0, behavior: 'smooth', }); - } else { - this.updateSlide(0); } + this.updateSlide(0); } ); } @@ -203,7 +209,6 @@ class PostsList extends React.Component { const newHideResteems = !prevState.hideResteems; if (process.env.BROWSER) { localStorage.setItem('hideResteems', newHideResteems); - console.log('Storing HideResteems: ', newHideResteems); } return { hideResteems: newHideResteems }; }); @@ -231,6 +236,7 @@ class PostsList extends React.Component { post.getIn(['stats', 'is_pinned'], false) ); const pinnedPostsCount = pinnedPosts.size; + const screenWidth = process.env.BROWSER ? window.innerWidth : 0; const renderSummary = items => items.map((post, i) => { @@ -290,7 +296,10 @@ class PostsList extends React.Component { }); const renderDotLinks = totalItems => { - const adjustedTotalItems = totalItems - 1; + let adjustedTotalItems = totalItems; + if (screenWidth >= 768) { + adjustedTotalItems -= 1; + } const dots = Array.from({ length: adjustedTotalItems }, (_, i) => ( -