UNPKG

feeds-fun

Version:

Frontend for the Feeds Fun — web-based news reader

530 lines (477 loc) 11.2 kB
import * as e from "@/logic/enums"; export type FeedId = string & {readonly __brand: unique symbol}; export function toFeedId(id: string): FeedId { return id as FeedId; } export type EntryId = string & {readonly __brand: unique symbol}; export function toEntryId(id: string): EntryId { return id as EntryId; } export type RuleId = string & {readonly __brand: unique symbol}; export function toRuleId(id: string): RuleId { return id as RuleId; } export type CollectionId = string & {readonly __brand: unique symbol}; export function toCollectionId(id: string): CollectionId { return id as CollectionId; } export type CollectionSlug = string & {readonly __brand: unique symbol}; export function toCollectionSlug(slug: string): CollectionSlug { return slug as CollectionSlug; } export type URL = string & {readonly __brand: unique symbol}; export function toURL(url: string): URL { return url as URL; } export class Feed { readonly id: FeedId; readonly title: string | null; readonly description: string | null; readonly url: URL; readonly state: string; readonly lastError: string | null; readonly loadedAt: Date | null; readonly linkedAt: Date; readonly isOk: boolean; readonly collectionIds: CollectionId[]; constructor({ id, title, description, url, state, lastError, loadedAt, linkedAt, isOk, collectionIds }: { id: FeedId; title: string | null; description: string | null; url: URL; state: string; lastError: string | null; loadedAt: Date | null; linkedAt: Date; isOk: boolean; collectionIds: CollectionId[]; }) { this.id = id; this.title = title; this.description = description; this.url = url; this.state = state; this.lastError = lastError; this.loadedAt = loadedAt; this.linkedAt = linkedAt; this.isOk = isOk; this.collectionIds = collectionIds; } } export function feedFromJSON({ id, title, description, url, state, lastError, loadedAt, linkedAt, collectionIds }: { id: string; title: string; description: string; url: string; state: string; lastError: string | null; loadedAt: string; linkedAt: string; collectionIds: string[]; }): Feed { return { id: toFeedId(id), title: title !== null ? title : null, description: description !== null ? description : null, url: toURL(url), state: state, lastError: lastError, loadedAt: loadedAt !== null ? new Date(loadedAt) : null, linkedAt: new Date(linkedAt), isOk: state === "loaded", collectionIds: collectionIds.map(toCollectionId) }; } export class Entry { readonly id: EntryId; readonly feedId: FeedId; readonly title: string; readonly url: URL; readonly tags: string[]; readonly markers: e.Marker[]; readonly score: number; readonly scoreContributions: {[key: string]: number}; readonly scoreToZero: number; readonly publishedAt: Date; readonly catalogedAt: Date; body: string | null; constructor({ id, feedId, title, url, tags, markers, score, scoreContributions, publishedAt, catalogedAt, body }: { id: EntryId; feedId: FeedId; title: string; url: URL; tags: string[]; markers: e.Marker[]; score: number; scoreContributions: {[key: string]: number}; publishedAt: Date; catalogedAt: Date; body: string | null; }) { this.id = id; this.feedId = feedId; this.title = title; this.url = url; this.tags = tags; this.markers = markers; this.score = score; this.scoreContributions = scoreContributions; this.publishedAt = publishedAt; this.catalogedAt = catalogedAt; this.body = body; this.scoreToZero = -Math.abs(score); } setMarker(marker: e.Marker): void { if (!this.hasMarker(marker)) { this.markers.push(marker); } } removeMarker(marker: e.Marker): void { if (this.hasMarker(marker)) { this.markers.splice(this.markers.indexOf(marker), 1); } } hasMarker(marker: e.Marker): boolean { return this.markers.includes(marker); } } export function entryFromJSON( rawEntry: { id: string; feedId: string; title: string; url: string; tags: number[]; markers: string[]; score: number; scoreContributions: {[key: number]: number}; publishedAt: string; catalogedAt: string; body: string | null; }, tagsMapping: {[key: number]: string} ): Entry { const contributions: {[key: string]: number} = {}; for (const key in rawEntry.scoreContributions) { contributions[tagsMapping[key]] = rawEntry.scoreContributions[key]; } return new Entry({ id: toEntryId(rawEntry.id), feedId: toFeedId(rawEntry.feedId), title: rawEntry.title, url: toURL(rawEntry.url), tags: rawEntry.tags.map((t: number) => tagsMapping[t]), markers: rawEntry.markers.map((m: string) => { if (m in e.reverseMarker) { return e.reverseMarker[m]; } throw new Error(`Unknown marker: ${m}`); }), score: rawEntry.score, // map keys from int to string scoreContributions: contributions, publishedAt: new Date(rawEntry.publishedAt), catalogedAt: new Date(rawEntry.catalogedAt), body: rawEntry.body }); } export type Rule = { readonly id: RuleId; readonly requiredTags: string[]; readonly excludedTags: string[]; readonly tags: string[]; readonly score: number; readonly createdAt: Date; readonly updatedAt: Date; }; export function ruleFromJSON({ id, requiredTags, excludedTags, score, createdAt, updatedAt }: { id: string; requiredTags: string[]; excludedTags: string[]; score: number; createdAt: string; updatedAt: string; }): Rule { requiredTags = requiredTags.sort(); excludedTags = excludedTags.sort(); return { id: toRuleId(id), requiredTags: requiredTags, excludedTags: excludedTags, tags: requiredTags.concat(excludedTags).sort(), score: score, createdAt: new Date(createdAt), updatedAt: new Date(updatedAt) }; } export type EntryInfo = { readonly title: string; readonly body: string; readonly url: URL; readonly publishedAt: Date; }; export function entryInfoFromJSON({ title, body, url, publishedAt }: { title: string; body: string; url: string; publishedAt: string; }): EntryInfo { return {title, body, url: toURL(url), publishedAt: new Date(publishedAt)}; } export type FeedInfo = { readonly url: URL; readonly title: string; readonly description: string; readonly entries: EntryInfo[]; readonly isLinked: boolean; }; export function feedInfoFromJSON({ url, title, description, entries, isLinked }: { url: string; title: string; description: string; entries: any[]; isLinked: boolean; }): FeedInfo { return { url: toURL(url), title, description, entries: entries.map(entryInfoFromJSON), isLinked }; } export type TagInfo = { readonly uid: string; readonly name: string | null; readonly link: string | null; readonly categories: string[]; }; export function tagInfoFromJSON({ uid, name, link, categories }: { uid: string; name: string | null; link: string | null; categories: string[]; }): TagInfo { return {uid, name: name, link: link, categories}; } export function noInfoTag(uid: string): TagInfo { return {uid, name: uid, link: null, categories: []}; } export function fakeTag({uid, name, link, categories}: TagInfo): TagInfo { return {uid, name, link, categories}; } export type UserSetting = { readonly kind: string; readonly type: string; value: string | number | boolean; readonly name: string; }; export function userSettingFromJSON({ kind, type, value, name, description }: { kind: string; type: string; value: string | number | boolean; name: string; description: string; }): UserSetting { return { kind, type, value: type === "decimal" ? parseFloat(value as string) : value, name }; } export class ResourceHistoryRecord { readonly intervalStartedAt: Date; readonly used: number; readonly reserved: number; constructor({intervalStartedAt, used, reserved}: {intervalStartedAt: Date; used: number; reserved: number}) { this.intervalStartedAt = intervalStartedAt; this.used = used; this.reserved = reserved; } total(): number { return this.used + this.reserved; } } export function resourceHistoryRecordFromJSON({ intervalStartedAt, used, reserved }: { intervalStartedAt: string; used: number | string; reserved: number | string; }): ResourceHistoryRecord { return new ResourceHistoryRecord({ intervalStartedAt: new Date(intervalStartedAt), // TODO: refactor to use kind of Decimals and to respect input types used: parseFloat(used as string), reserved: parseFloat(reserved as string) }); } export class Collection { readonly id: CollectionId; readonly slug: CollectionSlug; readonly guiOrder: number; readonly name: string; readonly description: string; readonly feedsNumber: number; readonly showOnMain: boolean; constructor({ id, slug, guiOrder, name, description, feedsNumber, showOnMain }: { id: CollectionId; slug: CollectionSlug; guiOrder: number; name: string; description: string; feedsNumber: number; showOnMain: boolean; }) { this.id = id; this.slug = slug; this.guiOrder = guiOrder; this.name = name; this.description = description; this.feedsNumber = feedsNumber; this.showOnMain = showOnMain; } } export function collectionFromJSON({ id, slug, guiOrder, name, description, feedsNumber, showOnMain }: { id: string; slug: string; guiOrder: number; name: string; description: string; feedsNumber: number; showOnMain: boolean; }): Collection { return { id: toCollectionId(id), slug: toCollectionSlug(slug), guiOrder: guiOrder, name: name, description: description, feedsNumber: feedsNumber, showOnMain: showOnMain }; } export class CollectionFeedInfo { readonly url: URL; readonly title: string; readonly description: string; readonly id: FeedId; constructor({url, title, description, id}: {url: URL; title: string; description: string; id: FeedId}) { this.url = url; this.title = title; this.description = description; this.id = id; } } export function collectionFeedInfoFromJSON({ url, title, description, id }: { url: string; title: string; description: string; id: string; }): CollectionFeedInfo { return new CollectionFeedInfo({ url: toURL(url), title: title, description: description, id: toFeedId(id) }); } export class ApiMessage { readonly type: string; readonly code: string; readonly message: string; constructor({type, code, message}: {type: string; code: string; message: string}) { this.type = type; this.code = code; this.message = message; } } export function apiMessageFromJSON({type, code, message}: {type: string; code: string; message: string}): ApiMessage { return new ApiMessage({type, code, message}); }