Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/components/status.css
Original file line number Diff line number Diff line change
Expand Up @@ -1658,29 +1658,31 @@ body:has(#modal-container .carousel) .status .media img:hover {
}
}

.status:not(.large) .hashtag-stuffing {
opacity: 0.75;
.status .entity-stuffing {
opacity: 0.50;
transition: opacity 0.2s ease-in-out;
}
.status:not(.large) .hashtag-stuffing:is(:hover, :focus, :focus-within) {
.status .entity-stuffing:is(:hover, :focus, :focus-within) {
opacity: 1;
}
.status:not(.large) .hashtag-stuffing {
.status:not(.large) .entity-stuffing {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;

font-size: smaller;

/* Convert breaks to spaces */
br {
display: none;

+ * {
margin-inline-start: 1ex;
margin-inline-start: 0.5ex;
}
}
}
.status:not(.large) .hashtag-stuffing:first-child {
.status:not(.large) .entity-stuffing:first-child {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
Expand All @@ -1690,13 +1692,13 @@ body:has(#modal-container .carousel) .status .media img:hover {
/* If >= 9 hashtags, collapse */
/* TODO: lower the threshold one day */
.status:not(.large, .contextual .status)
p:not(.hashtag-stuffing):has(.hashtag:nth-of-type(1)):has(
.hashtag:nth-of-type(2)
):has(.hashtag:nth-of-type(3)):has(.hashtag:nth-of-type(4)):has(
.hashtag:nth-of-type(5)
):has(.hashtag:nth-of-type(6)):has(.hashtag:nth-of-type(7)):has(
.hashtag:nth-of-type(8)
):has(.hashtag:nth-of-type(9)) {
p:not(.entity-stuffing):has(.mention:nth-of-type(1)):has(
.mention:nth-of-type(2)
):has(.mention:nth-of-type(3)):has(.mention:nth-of-type(4)):has(
.mention:nth-of-type(5)
):has(.mention:nth-of-type(6)):has(.mention:nth-of-type(7)):has(
.mention:nth-of-type(8)
):has(.mention:nth-of-type(9)) {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
Expand Down Expand Up @@ -2561,7 +2563,7 @@ a.card:is(:hover, :focus):visited {
display: revert;
}

.hashtag-stuffing {
.entity-stuffing {
white-space: normal;
opacity: 1;
}
Expand Down
86 changes: 57 additions & 29 deletions src/utils/enhance-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,45 @@ function createDOM(html, isDocumentFragment) {
return isDocumentFragment ? tpl.content : tpl;
}

function _countEntities(p) {
let count = 0;

for (const node of p.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
// Check if there's text between the entities
const text = node.textContent.trim();
if (text !== '') {
// End if there's text
throw false;
}
} else if (node.tagName === 'BR') {
// Ignore <br />
} else if (node.tagName === 'A') {
// Check if the link has text
const linkText = node.textContent.trim();

if (!linkText) {
// End if there's a link without text
throw false;
} else if (!(linkText.startsWith('#') || linkText.startsWith('@'))) {
// End if there's a link that's not a mention or an hashtag
throw false;
} else {
// This is an entity
count++;
}
} else if (node.tagName === 'SPAN') {
// If this is a span, we might need to go deeper
count += _countEntities(node);
} else {
// There's something else here we should not touch
throw false;
}
}

return count;
}

function _enhanceContent(content, opts = {}) {
const { emojis, returnDOM, postEnhanceDOM = () => {} } = opts;
let enhancedContent = content;
Expand Down Expand Up @@ -222,51 +261,40 @@ function _enhanceContent(content, opts = {}) {
}
}

// HASHTAG STUFFING
// ENTITY STUFFING
// ================
// Get the <p> that contains a lot of hashtags, add a class to it
if (enhancedContent.includes('#')) {
// Get the <p> that contains a lot of hashtags or mentions, add a class to it
if (enhancedContent.includes('#') || enhancedContent.includes('@')) {
let prevIndex = null;
const hashtagStuffedParagraphs = [...dom.querySelectorAll('p')].filter(
const stuffedParagraphs = [...dom.querySelectorAll('p')].filter(
(p, index) => {
let hashtagCount = 0;
for (let i = 0; i < p.childNodes.length; i++) {
const node = p.childNodes[i];
let entitiesCount = 0;

if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text !== '') {
return false;
}
} else if (node.tagName === 'BR') {
// Ignore <br />
} else if (node.tagName === 'A') {
const linkText = node.textContent.trim();
if (!linkText || !linkText.startsWith('#')) {
return false;
} else {
hashtagCount++;
}
} else {
try {
entitiesCount = _countEntities(p);
} catch (e) {
if (e === false) {
return false;
}
throw e;
}

// Only consider "stuffing" if:
// - there are more than 3 hashtags
// - there are more than 1 hashtag in adjacent paragraphs
if (hashtagCount > 3) {
// - there are more than 3 entities
// - there are more than 1 entity in adjacent paragraphs
if (entitiesCount > 3) {
prevIndex = index;
return true;
}
if (hashtagCount > 1 && prevIndex && index === prevIndex + 1) {
if (entitiesCount > 1 && prevIndex && index === prevIndex + 1) {
prevIndex = index;
return true;
}
},
);
if (hashtagStuffedParagraphs?.length) {
for (const p of hashtagStuffedParagraphs) {
p.classList.add('hashtag-stuffing');
if (stuffedParagraphs?.length) {
for (const p of stuffedParagraphs) {
p.classList.add('entity-stuffing');
p.title = p.innerText;
}
}
Expand Down