New experiment: followed tag indicator
This commit is contained in:
parent
b34ef09411
commit
aa8cbe046c
10 changed files with 248 additions and 31 deletions
|
@ -13,6 +13,7 @@ import multiColumnUrl from '../assets/multi-column.svg';
|
||||||
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
|
import tabMenuBarUrl from '../assets/tab-menu-bar.svg';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { fetchFollowedTags } from '../utils/followed-tags';
|
||||||
import pmem from '../utils/pmem';
|
import pmem from '../utils/pmem';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
@ -500,13 +501,7 @@ function ShortcutForm({
|
||||||
(async () => {
|
(async () => {
|
||||||
if (currentType !== 'hashtag') return;
|
if (currentType !== 'hashtag') return;
|
||||||
try {
|
try {
|
||||||
const iterator = masto.v1.followedTags.list();
|
const tags = await fetchFollowedTags();
|
||||||
const tags = [];
|
|
||||||
do {
|
|
||||||
const { value, done } = await iterator.next();
|
|
||||||
if (done || value?.length === 0) break;
|
|
||||||
tags.push(...value);
|
|
||||||
} while (true);
|
|
||||||
setFollowedHashtags(tags);
|
setFollowedHashtags(tags);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
@ -14,6 +14,13 @@
|
||||||
transparent min(160px, 50%)
|
transparent min(160px, 50%)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
.status-followed-tags {
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
var(--hashtag-faded-color),
|
||||||
|
transparent min(160px, 50%)
|
||||||
|
);
|
||||||
|
}
|
||||||
.status-reply-to {
|
.status-reply-to {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
160deg,
|
160deg,
|
||||||
|
@ -21,7 +28,7 @@
|
||||||
transparent min(160px, 50%)
|
transparent min(160px, 50%)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
:is(.status-reblog, .status-group) .status-reply-to {
|
:is(.status-reblog, .status-group, .status-followed-tags) .status-reply-to {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
-20deg,
|
-20deg,
|
||||||
var(--reply-to-faded-color),
|
var(--reply-to-faded-color),
|
||||||
|
@ -63,6 +70,49 @@
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
.status-followed-tags {
|
||||||
|
.status-pre-meta {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--hashtag-color);
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--hashtag-text-color);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration-color: var(--hashtag-faded-color);
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px;
|
||||||
|
vertical-align: top;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-shadow: 0 1px var(--bg-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration-color: var(--hashtag-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-followed-tag-item {
|
||||||
|
color: var(--hashtag-text-color);
|
||||||
|
padding: 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-inline-end: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* STATUS */
|
/* STATUS */
|
||||||
|
|
||||||
|
|
|
@ -92,11 +92,12 @@ function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
instance: propInstance,
|
instance: propInstance,
|
||||||
withinContext,
|
|
||||||
size = 'm',
|
size = 'm',
|
||||||
skeleton,
|
|
||||||
readOnly,
|
|
||||||
contentTextWeight,
|
contentTextWeight,
|
||||||
|
readOnly,
|
||||||
|
enableCommentHint,
|
||||||
|
withinContext,
|
||||||
|
skeleton,
|
||||||
enableTranslate,
|
enableTranslate,
|
||||||
forceTranslate: _forceTranslate,
|
forceTranslate: _forceTranslate,
|
||||||
previewMode,
|
previewMode,
|
||||||
|
@ -104,7 +105,7 @@ function Status({
|
||||||
onMediaClick,
|
onMediaClick,
|
||||||
quoted,
|
quoted,
|
||||||
onStatusLinkClick = () => {},
|
onStatusLinkClick = () => {},
|
||||||
enableCommentHint,
|
showFollowedTags,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -174,6 +175,7 @@ function Status({
|
||||||
uri,
|
uri,
|
||||||
url,
|
url,
|
||||||
emojis,
|
emojis,
|
||||||
|
tags,
|
||||||
// Non-API props
|
// Non-API props
|
||||||
_deleted,
|
_deleted,
|
||||||
_pinned,
|
_pinned,
|
||||||
|
@ -214,6 +216,7 @@ function Status({
|
||||||
containerProps={{
|
containerProps={{
|
||||||
onMouseEnter: debugHover,
|
onMouseEnter: debugHover,
|
||||||
}}
|
}}
|
||||||
|
showFollowedTags
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -302,6 +305,39 @@ function Status({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check followedTags
|
||||||
|
if (showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-state-post-id={sKey}
|
||||||
|
class="status-followed-tags"
|
||||||
|
onMouseEnter={debugHover}
|
||||||
|
>
|
||||||
|
<div class="status-pre-meta">
|
||||||
|
<Icon icon="hashtag" size="l" />{' '}
|
||||||
|
{snapStates.statusFollowedTags[sKey].slice(0, 3).map((tag) => (
|
||||||
|
<Link
|
||||||
|
key={tag}
|
||||||
|
to={instance ? `/${instance}/t/${tag}` : `/t/${tag}`}
|
||||||
|
class="status-followed-tag-item"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Status
|
||||||
|
status={statusID ? null : status}
|
||||||
|
statusID={statusID ? status.id : null}
|
||||||
|
instance={instance}
|
||||||
|
size={size}
|
||||||
|
contentTextWeight={contentTextWeight}
|
||||||
|
readOnly={readOnly}
|
||||||
|
enableCommentHint
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isSizeLarge = size === 'l';
|
const isSizeLarge = size === 'l';
|
||||||
|
|
||||||
const [forceTranslate, setForceTranslate] = useState(_forceTranslate);
|
const [forceTranslate, setForceTranslate] = useState(_forceTranslate);
|
||||||
|
@ -2372,7 +2408,14 @@ function nicePostURL(url) {
|
||||||
|
|
||||||
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
||||||
|
|
||||||
function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
function FilteredStatus({
|
||||||
|
status,
|
||||||
|
filterInfo,
|
||||||
|
instance,
|
||||||
|
containerProps = {},
|
||||||
|
showFollowedTags,
|
||||||
|
}) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
const {
|
const {
|
||||||
id: statusID,
|
id: statusID,
|
||||||
account: { avatar, avatarStatic, bot, group },
|
account: { avatar, avatarStatic, bot, group },
|
||||||
|
@ -2399,7 +2442,8 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusPeekRef = useTruncated();
|
const statusPeekRef = useTruncated();
|
||||||
const sKey =
|
const sKey = statusKey(status.id, instance);
|
||||||
|
const ssKey =
|
||||||
statusKey(status.id, instance) +
|
statusKey(status.id, instance) +
|
||||||
' ' +
|
' ' +
|
||||||
(statusKey(reblog?.id, instance) || '');
|
(statusKey(reblog?.id, instance) || '');
|
||||||
|
@ -2408,10 +2452,20 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
const url = instance
|
const url = instance
|
||||||
? `/${instance}/s/${actualStatusID}`
|
? `/${instance}/s/${actualStatusID}`
|
||||||
: `/s/${actualStatusID}`;
|
: `/s/${actualStatusID}`;
|
||||||
|
const isFollowedTags =
|
||||||
|
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={isReblog ? (group ? 'status-group' : 'status-reblog') : ''}
|
class={
|
||||||
|
isReblog
|
||||||
|
? group
|
||||||
|
? 'status-group'
|
||||||
|
: 'status-reblog'
|
||||||
|
: isFollowedTags
|
||||||
|
? 'status-followed-tags'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
{...containerProps}
|
{...containerProps}
|
||||||
title={statusPeekText}
|
title={statusPeekText}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
|
@ -2420,7 +2474,7 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
}}
|
}}
|
||||||
{...bindLongPressPeek()}
|
{...bindLongPressPeek()}
|
||||||
>
|
>
|
||||||
<article data-state-post-id={sKey} class="status filtered" tabindex="-1">
|
<article data-state-post-id={ssKey} class="status filtered" tabindex="-1">
|
||||||
<b
|
<b
|
||||||
class="status-filtered-badge clickable badge-meta"
|
class="status-filtered-badge clickable badge-meta"
|
||||||
title={filterTitleStr}
|
title={filterTitleStr}
|
||||||
|
@ -2443,6 +2497,14 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
/>{' '}
|
/>{' '}
|
||||||
{isReblog ? (
|
{isReblog ? (
|
||||||
'boosted'
|
'boosted'
|
||||||
|
) : isFollowedTags ? (
|
||||||
|
<span>
|
||||||
|
{snapStates.statusFollowedTags[sKey].slice(0, 3).map((tag) => (
|
||||||
|
<span key={tag} class="status-followed-tag-item">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<RelativeTime datetime={createdAtDate} format="micro" />
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -44,6 +44,7 @@ function Timeline({
|
||||||
refresh,
|
refresh,
|
||||||
view,
|
view,
|
||||||
filterContext,
|
filterContext,
|
||||||
|
showFollowedTags,
|
||||||
}) {
|
}) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
@ -391,6 +392,7 @@ function Timeline({
|
||||||
filterContext={filterContext}
|
filterContext={filterContext}
|
||||||
key={status.id + status?._pinned + view}
|
key={status.id + status?._pinned + view}
|
||||||
view={view}
|
view={view}
|
||||||
|
showFollowedTags={showFollowedTags}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showMore &&
|
{showMore &&
|
||||||
|
@ -478,6 +480,7 @@ function TimelineItem({
|
||||||
// allowFilters,
|
// allowFilters,
|
||||||
filterContext,
|
filterContext,
|
||||||
view,
|
view,
|
||||||
|
showFollowedTags,
|
||||||
}) {
|
}) {
|
||||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||||
if (_pinned) useItemID = false;
|
if (_pinned) useItemID = false;
|
||||||
|
@ -567,12 +570,13 @@ function TimelineItem({
|
||||||
!_differentAuthor &&
|
!_differentAuthor &&
|
||||||
!items[i - 1]._differentAuthor &&
|
!items[i - 1]._differentAuthor &&
|
||||||
!items[i + 1]._differentAuthor)));
|
!items[i + 1]._differentAuthor)));
|
||||||
|
const isStart = i === 0;
|
||||||
const isEnd = i === items.length - 1;
|
const isEnd = i === items.length - 1;
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`timeline-${statusID}`}
|
key={`timeline-${statusID}`}
|
||||||
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
class={`timeline-item-container timeline-item-container-type-${type} timeline-item-container-${
|
||||||
i === 0 ? 'start' : isEnd ? 'end' : 'middle'
|
isStart ? 'start' : isEnd ? 'end' : 'middle'
|
||||||
} ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
|
} ${_differentAuthor ? 'timeline-item-diff-author' : ''}`}
|
||||||
>
|
>
|
||||||
<Link class="status-link timeline-item" to={url}>
|
<Link class="status-link timeline-item" to={url}>
|
||||||
|
@ -583,6 +587,7 @@ function TimelineItem({
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
enableCommentHint={isEnd}
|
enableCommentHint={isEnd}
|
||||||
|
showFollowedTags={showFollowedTags}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -590,6 +595,7 @@ function TimelineItem({
|
||||||
status={item}
|
status={item}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
enableCommentHint={isEnd}
|
enableCommentHint={isEnd}
|
||||||
|
showFollowedTags={showFollowedTags}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -631,6 +637,7 @@ function TimelineItem({
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
enableCommentHint
|
enableCommentHint
|
||||||
|
showFollowedTags={showFollowedTags}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -638,6 +645,7 @@ function TimelineItem({
|
||||||
status={status}
|
status={status}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
enableCommentHint
|
enableCommentHint
|
||||||
|
showFollowedTags={showFollowedTags}
|
||||||
// allowFilters={allowFilters}
|
// allowFilters={allowFilters}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -54,6 +54,17 @@
|
||||||
--reply-to-text-color: #b36200;
|
--reply-to-text-color: #b36200;
|
||||||
--favourite-color: var(--red-color);
|
--favourite-color: var(--red-color);
|
||||||
--reply-to-faded-color: #ffa60020;
|
--reply-to-faded-color: #ffa60020;
|
||||||
|
--hashtag-color: LightSeaGreen;
|
||||||
|
--hashtag-faded-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--hashtag-color) 15%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
--hashtag-text-color: color-mix(
|
||||||
|
in lch,
|
||||||
|
var(--hashtag-color) 40%,
|
||||||
|
var(--text-color) 60%
|
||||||
|
);
|
||||||
--outline-color: rgba(128, 128, 128, 0.2);
|
--outline-color: rgba(128, 128, 128, 0.2);
|
||||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||||
--divider-color: rgba(0, 0, 0, 0.1);
|
--divider-color: rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
@ -5,10 +5,9 @@ import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import NavMenu from '../components/nav-menu';
|
import NavMenu from '../components/nav-menu';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import { fetchFollowedTags } from '../utils/followed-tags';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 200;
|
|
||||||
|
|
||||||
function FollowedHashtags() {
|
function FollowedHashtags() {
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
useTitle(`Followed Hashtags`, `/ft`);
|
useTitle(`Followed Hashtags`, `/ft`);
|
||||||
|
@ -19,17 +18,7 @@ function FollowedHashtags() {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const iterator = masto.v1.followedTags.list({
|
const tags = await fetchFollowedTags();
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
const tags = [];
|
|
||||||
do {
|
|
||||||
const { value, done } = await iterator.next();
|
|
||||||
if (done || value?.length === 0) break;
|
|
||||||
tags.push(...value);
|
|
||||||
} while (true);
|
|
||||||
tags.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
console.log(tags);
|
|
||||||
setFollowedHashtags(tags);
|
setFollowedHashtags(tags);
|
||||||
setUIState('default');
|
setUIState('default');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -6,7 +6,11 @@ import { api } from '../utils/api';
|
||||||
import { filteredItems } from '../utils/filters';
|
import { filteredItems } from '../utils/filters';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import { getStatus, saveStatus } from '../utils/states';
|
import { getStatus, saveStatus } from '../utils/states';
|
||||||
import { dedupeBoosts } from '../utils/timeline-utils';
|
import {
|
||||||
|
assignFollowedTags,
|
||||||
|
clearFollowedTagsState,
|
||||||
|
dedupeBoosts,
|
||||||
|
} from '../utils/timeline-utils';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
@ -37,6 +41,8 @@ function Following({ title, path, id, ...props }) {
|
||||||
saveStatus(item, instance);
|
saveStatus(item, instance);
|
||||||
});
|
});
|
||||||
value = dedupeBoosts(value, instance);
|
value = dedupeBoosts(value, instance);
|
||||||
|
if (firstLoad) clearFollowedTagsState();
|
||||||
|
assignFollowedTags(value, instance);
|
||||||
|
|
||||||
// ENFORCE sort by datetime (Latest first)
|
// ENFORCE sort by datetime (Latest first)
|
||||||
value.sort((a, b) => {
|
value.sort((a, b) => {
|
||||||
|
@ -118,6 +124,7 @@ function Following({ title, path, id, ...props }) {
|
||||||
{...props}
|
{...props}
|
||||||
// allowFilters
|
// allowFilters
|
||||||
filterContext="home"
|
filterContext="home"
|
||||||
|
showFollowedTags
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
62
src/utils/followed-tags.js
Normal file
62
src/utils/followed-tags.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
import store from '../utils/store';
|
||||||
|
|
||||||
|
const LIMIT = 200;
|
||||||
|
const MAX_FETCH = 10;
|
||||||
|
|
||||||
|
export async function fetchFollowedTags() {
|
||||||
|
const { masto } = api();
|
||||||
|
const iterator = masto.v1.followedTags.list({
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
const tags = [];
|
||||||
|
let fetchCount = 0;
|
||||||
|
do {
|
||||||
|
const { value, done } = await iterator.next();
|
||||||
|
if (done || value?.length === 0) break;
|
||||||
|
tags.push(...value);
|
||||||
|
fetchCount++;
|
||||||
|
} while (fetchCount < MAX_FETCH);
|
||||||
|
tags.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
console.log(tags);
|
||||||
|
|
||||||
|
if (tags.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Save to local storage, with saved timestamp
|
||||||
|
store.account.set('followedTags', {
|
||||||
|
tags,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_AGE = 24 * 60 * 60 * 1000; // 1 day
|
||||||
|
export async function getFollowedTags() {
|
||||||
|
try {
|
||||||
|
const { tags, updatedAt } = store.account.get('followedTags') || {};
|
||||||
|
if (!tags?.length) return await fetchFollowedTags();
|
||||||
|
if (Date.now() - updatedAt > MAX_AGE) {
|
||||||
|
// Stale-while-revalidate
|
||||||
|
fetchFollowedTags();
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fauxDiv = document.createElement('div');
|
||||||
|
export const extractTagsFromStatus = (content) => {
|
||||||
|
if (!content) return [];
|
||||||
|
if (content.indexOf('#') === -1) return [];
|
||||||
|
fauxDiv.innerHTML = content;
|
||||||
|
const hashtagLinks = fauxDiv.querySelectorAll('a.hashtag');
|
||||||
|
if (!hashtagLinks.length) return [];
|
||||||
|
return Array.from(hashtagLinks).map((a) =>
|
||||||
|
a.innerText.trim().replace(/^[^#]*#+/, ''),
|
||||||
|
);
|
||||||
|
};
|
|
@ -31,6 +31,7 @@ const states = proxy({
|
||||||
scrollPositions: {},
|
scrollPositions: {},
|
||||||
unfurledLinks: {},
|
unfurledLinks: {},
|
||||||
statusQuotes: {},
|
statusQuotes: {},
|
||||||
|
statusFollowedTags: {},
|
||||||
accounts: {},
|
accounts: {},
|
||||||
routeNotification: null,
|
routeNotification: null,
|
||||||
// Modals
|
// Modals
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { extractTagsFromStatus, getFollowedTags } from './followed-tags';
|
||||||
|
import states, { statusKey } from './states';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
export function groupBoosts(values) {
|
export function groupBoosts(values) {
|
||||||
|
@ -175,3 +177,33 @@ export function groupContext(items) {
|
||||||
|
|
||||||
return newItems;
|
return newItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function assignFollowedTags(items, instance) {
|
||||||
|
const followedTags = await getFollowedTags(); // [{name: 'tag'}, {...}]
|
||||||
|
if (!followedTags.length) return;
|
||||||
|
const { statusFollowedTags } = states;
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item.reblog) return;
|
||||||
|
const { id, content, tags = [] } = item;
|
||||||
|
const sKey = statusKey(id, instance);
|
||||||
|
if (statusFollowedTags[sKey]?.length) return;
|
||||||
|
const extractedTags = extractTagsFromStatus(content);
|
||||||
|
if (!extractedTags.length && !tags.length) return;
|
||||||
|
const itemFollowedTags = followedTags.reduce((acc, tag) => {
|
||||||
|
if (
|
||||||
|
extractedTags.some((t) => t.toLowerCase() === tag.name.toLowerCase()) ||
|
||||||
|
tags.some((t) => t.name.toLowerCase() === tag.name.toLowerCase())
|
||||||
|
) {
|
||||||
|
acc.push(tag.name);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
if (itemFollowedTags.length) {
|
||||||
|
statusFollowedTags[sKey] = itemFollowedTags;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearFollowedTagsState() {
|
||||||
|
states.statusFollowedTags = {};
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue