phanpy/src/utils/timeline-utils.jsx

266 wiersze
7.9 KiB
JavaScript

import { api } from './api';
import { extractTagsFromStatus, getFollowedTags } from './followed-tags';
import pmem from './pmem';
import { fetchRelationships } from './relationships';
import states, { saveStatus, statusKey } from './states';
import store from './store';
export function groupBoosts(values) {
let newValues = [];
let boostStash = [];
let serialBoosts = 0;
for (let i = 0; i < values.length; i++) {
const item = values[i];
if (item.reblog && !item.account?.group) {
boostStash.push(item);
serialBoosts++;
} else {
newValues.push(item);
if (serialBoosts < 3) {
serialBoosts = 0;
}
}
}
// if boostStash is more than quarter of values
// or if there are 3 or more boosts in a row
if (
values.length > 10 &&
(boostStash.length > values.length / 4 || serialBoosts >= 3)
) {
// if boostStash is more than 3 quarter of values
const boostStashID = boostStash.map((status) => status.id);
if (boostStash.length > (values.length * 3) / 4) {
// insert boost array at the end of specialHome list
newValues = [
...newValues,
{ id: boostStashID, items: boostStash, type: 'boosts' },
];
} else {
// insert boosts array in the middle of specialHome list
const half = Math.floor(newValues.length / 2);
newValues = [
...newValues.slice(0, half),
{
id: boostStashID,
items: boostStash,
type: 'boosts',
},
...newValues.slice(half),
];
}
return newValues;
} else {
return values;
}
}
export function dedupeBoosts(items, instance) {
const boostedStatusIDs = store.account.get('boostedStatusIDs') || {};
const filteredItems = items.filter((item) => {
if (!item.reblog) return true;
const statusKey = `${instance}-${item.reblog.id}`;
const boosterID = boostedStatusIDs[statusKey];
if (boosterID && boosterID !== item.id) {
console.warn(
`🚫 Duplicate boost by ${item.account.displayName}`,
item,
item.reblog,
);
return false;
} else {
boostedStatusIDs[statusKey] = item.id;
}
return true;
});
// Limit to 50
const keys = Object.keys(boostedStatusIDs);
if (keys.length > 50) {
keys.slice(0, keys.length - 50).forEach((key) => {
delete boostedStatusIDs[key];
});
}
store.account.set('boostedStatusIDs', boostedStatusIDs);
return filteredItems;
}
export function groupContext(items, instance) {
const contexts = [];
let contextIndex = 0;
items.forEach((item) => {
for (let i = 0; i < contexts.length; i++) {
if (contexts[i].find((t) => t.id === item.id)) return;
if (
contexts[i].find((t) => t.id === item.inReplyToId) ||
contexts[i].find((t) => t.inReplyToId === item.id)
) {
contexts[i].push(item);
return;
}
}
const repliedItem = items.find((i) => i.id === item.inReplyToId);
if (repliedItem) {
contexts[contextIndex++] = [item, repliedItem];
}
});
// Check for cross-item contexts
// Merge contexts into one if they have a common item (same id)
for (let i = 0; i < contexts.length; i++) {
for (let j = i + 1; j < contexts.length; j++) {
const commonItem = contexts[i].find((t) => contexts[j].includes(t));
if (commonItem) {
contexts[i] = [...contexts[i], ...contexts[j]];
// Remove duplicate items
contexts[i] = contexts[i].filter(
(item, index, self) =>
self.findIndex((t) => t.id === item.id) === index,
);
contexts.splice(j, 1);
j--;
}
}
}
// Sort items by checking inReplyToId
contexts.forEach((context) => {
context.sort((a, b) => {
if (!a.inReplyToId && !b.inReplyToId) {
return new Date(a.createdAt) - new Date(b.createdAt);
}
if (a.inReplyToId === b.id) return 1;
if (b.inReplyToId === a.id) return -1;
if (!a.inReplyToId) return -1;
if (!b.inReplyToId) return 1;
return new Date(a.createdAt) - new Date(b.createdAt);
});
});
// Tag items that has different author than first post's author
contexts.forEach((context) => {
const firstItemAccountID = context[0].account.id;
context.forEach((item) => {
if (item.account.id !== firstItemAccountID) {
item._differentAuthor = true;
}
});
});
if (contexts.length) console.log('🧵 Contexts', contexts);
const newItems = [];
const appliedContextIndices = [];
items.forEach((item) => {
if (item.reblog) {
newItems.push(item);
return;
}
for (let i = 0; i < contexts.length; i++) {
if (contexts[i].find((t) => t.id === item.id)) {
if (appliedContextIndices.includes(i)) return;
const contextItems = contexts[i];
contextItems.sort((a, b) => {
const aDate = new Date(a.createdAt);
const bDate = new Date(b.createdAt);
return aDate - bDate;
});
const firstItemAccountID = contextItems[0].account.id;
newItems.push({
id: contextItems.map((i) => i.id),
items: contextItems,
type: contextItems.every((it) => it.account.id === firstItemAccountID)
? 'thread'
: 'conversation',
});
appliedContextIndices.push(i);
return;
}
}
if (item.inReplyToId && item.inReplyToAccountId !== item.account.id) {
const sKey = statusKey(item.id, instance);
if (!states.statusReply[sKey]) {
// If it's a reply and not a thread
queueMicrotask(async () => {
try {
const { masto } = api({ instance });
// const replyToStatus = await masto.v1.statuses
// .$select(item.inReplyToId)
// .fetch();
const replyToStatus = await fetchStatus(item.inReplyToId, masto);
saveStatus(replyToStatus, instance, {
skipThreading: true,
skipUnfurling: true,
});
states.statusReply[sKey] = {
id: replyToStatus.id,
instance,
};
} catch (e) {
// Silently fail
console.error(e);
}
});
}
}
newItems.push(item);
});
return newItems;
}
const fetchStatus = pmem((statusID, masto) => {
return masto.v1.statuses.$select(statusID).fetch();
});
export async function assignFollowedTags(items, instance) {
const followedTags = await getFollowedTags(); // [{name: 'tag'}, {...}]
if (!followedTags.length) return;
const { statusFollowedTags } = states;
console.log('statusFollowedTags', statusFollowedTags);
const statusWithFollowedTags = [];
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;
statusWithFollowedTags.push({
item,
sKey,
followedTags: itemFollowedTags,
});
}
});
if (statusWithFollowedTags.length) {
const accounts = statusWithFollowedTags.map((s) => s.item.account);
const relationships = await fetchRelationships(accounts);
if (!relationships) return;
statusWithFollowedTags.forEach((s) => {
const { item, sKey, followedTags } = s;
const r = relationships[item.account.id];
if (r && !r.following) {
statusFollowedTags[sKey] = followedTags;
}
});
}
}
export function clearFollowedTagsState() {
states.statusFollowedTags = {};
}