feeds-fun
Version:
Frontend for the Feeds Fun — web-based news reader
202 lines (155 loc) • 3.73 kB
text/typescript
import _ from "lodash";
import type * as t from "@/logic/types";
import DOMPurify from "dompurify";
export function timeSince(date: Date) {
const now = new Date();
const secondsPast = Math.floor((now.getTime() - date.getTime()) / 1000);
if (secondsPast < 60) {
return "<min";
}
const minutesPast = Math.floor(secondsPast / 60);
if (minutesPast < 60) {
return `${minutesPast}min`;
}
const hoursPast = Math.floor(minutesPast / 60);
if (hoursPast < 24) {
return `${hoursPast}h`;
}
const daysPast = Math.floor(hoursPast / 24);
if (daysPast < 30) {
return `${daysPast}d`;
}
const monthsPast = Math.floor(daysPast / 30);
if (monthsPast < 12) {
return `${monthsPast}m`;
}
const yearsPast = Math.floor(monthsPast / 12);
return `${yearsPast}y`;
}
export function compareLexicographically(a: string[], b: string[]) {
for (let i = 0; i < Math.min(a.length, b.length); i++) {
const comparison = a[i].localeCompare(b[i]);
if (comparison !== 0) {
return comparison;
}
}
if (a.length > b.length) {
return 1;
}
if (a.length < b.length) {
return -1;
}
return 0;
}
export function faviconForUrl(url: string): string | null {
try {
const parsedUrl = new URL(url);
return `${parsedUrl.protocol}//${parsedUrl.host}/favicon.ico`;
} catch (error) {
return null;
}
}
export function purifyTitle({raw, default_}: {raw: string | null; default_: string}) {
if (raw === null) {
return default_;
}
let title = DOMPurify.sanitize(raw, {ALLOWED_TAGS: []}).trim();
if (title.length === 0) {
return default_;
}
return title;
}
export function purifyBody({raw, default_}: {raw: string | null; default_: string}) {
if (raw === null) {
return default_;
}
let body = DOMPurify.sanitize(raw).trim();
if (body.length === 0) {
return default_;
}
return body;
}
export function chooseTagByUsage({
tagsCount,
border,
exclude
}: {
tagsCount: {[key: string]: number};
border: number;
exclude: string[];
}) {
if (Object.keys(tagsCount).length === 0) {
return null;
}
if (!exclude) {
exclude = [];
}
const tags = _.toPairs(tagsCount).sort((a, b) => {
if (a[1] === b[1]) {
return a[0].localeCompare(b[0]);
}
return b[1] - a[1];
});
for (let i = 0; i < tags.length; i++) {
if (exclude.includes(tags[i][0])) {
continue;
}
if (tags[i][1] < border) {
return tags[i][0];
}
}
return tags[tags.length - 1][0];
}
export function countTags(entries: t.Entry[] | t.Rule[] | null) {
if (!entries) {
return {};
}
const tagsCount: {[key: string]: number} = {};
for (const entry of entries) {
for (const tag of entry.tags) {
if (tag in tagsCount) {
tagsCount[tag] += 1;
} else {
tagsCount[tag] = 1;
}
}
}
return tagsCount;
}
export function sortIdsList<ID extends string = string>({
ids,
storage,
field,
direction
}: {
ids: ID[];
storage: {[key: string]: any};
field: string;
direction: number;
}) {
// Pre-map to avoid repeated lookups in the comparator
// required for the cases when storage is reactive
const mapped = ids.map((id) => {
// @ts-ignore
return {id, value: storage[id][field]};
});
mapped.sort((a: {id: ID; value: any}, b: {id: ID; value: any}) => {
if (a.value === null && b.value === null) {
return 0;
}
if (a.value === null) {
return 1;
}
if (b.value === null) {
return -1;
}
if (a.value < b.value) {
return direction;
}
if (a.value > b.value) {
return -direction;
}
return 0;
});
return mapped.map((x) => x.id);
}