feeds-fun 1.25.2 → 1.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,6 +84,18 @@ export class Feed {
84
84
  }
85
85
  }
86
86
 
87
+ export type RawFeed = {
88
+ id: string;
89
+ title: string | null;
90
+ description: string | null;
91
+ url: string;
92
+ state: string;
93
+ lastError?: string | null;
94
+ loadedAt?: string | null;
95
+ linkedAt: string;
96
+ collectionIds: string[];
97
+ };
98
+
87
99
  export function feedFromJSON({
88
100
  id,
89
101
  title,
@@ -94,31 +106,109 @@ export function feedFromJSON({
94
106
  loadedAt,
95
107
  linkedAt,
96
108
  collectionIds
97
- }: {
98
- id: string;
99
- title: string;
100
- description: string;
101
- url: string;
102
- state: string;
103
- lastError: string | null;
104
- loadedAt: string;
105
- linkedAt: string;
106
- collectionIds: string[];
107
- }): Feed {
109
+ }: RawFeed): Feed {
108
110
  return {
109
111
  id: toFeedId(id),
110
112
  title: title !== null ? title : null,
111
113
  description: description !== null ? description : null,
112
114
  url: toURL(url),
113
115
  state: state,
114
- lastError: lastError,
115
- loadedAt: loadedAt !== null ? new Date(loadedAt) : null,
116
+ lastError: lastError !== undefined ? lastError : null,
117
+ loadedAt: loadedAt !== undefined && loadedAt !== null ? new Date(loadedAt) : null,
116
118
  linkedAt: new Date(linkedAt),
117
119
  isOk: state === "loaded",
118
120
  collectionIds: collectionIds.map(toCollectionId)
119
121
  };
120
122
  }
121
123
 
124
+ export type ReferenceExtraValue = number | string | null;
125
+
126
+ export type RawReference = {
127
+ kind: e.ReferenceKind;
128
+ url: string;
129
+ title?: string | null;
130
+ mime_type?: string | null;
131
+ width?: number | null;
132
+ height?: number | null;
133
+ duration?: string | null;
134
+ size?: number | null;
135
+ extra?: {[key: string]: ReferenceExtraValue} | null;
136
+ };
137
+
138
+ export class Reference {
139
+ readonly kind: e.ReferenceKind;
140
+ readonly url: URL;
141
+ readonly title: string | null;
142
+ readonly mimeType: string | null;
143
+ readonly width: number | null;
144
+ readonly height: number | null;
145
+ readonly duration: string | null;
146
+ readonly size: number | null;
147
+ readonly extra: {[key: string]: ReferenceExtraValue} | null;
148
+
149
+ constructor({
150
+ kind,
151
+ url,
152
+ title,
153
+ mimeType,
154
+ width,
155
+ height,
156
+ duration,
157
+ size,
158
+ extra
159
+ }: {
160
+ kind: e.ReferenceKind;
161
+ url: URL;
162
+ title: string | null;
163
+ mimeType: string | null;
164
+ width: number | null;
165
+ height: number | null;
166
+ duration: string | null;
167
+ size: number | null;
168
+ extra: {[key: string]: ReferenceExtraValue} | null;
169
+ }) {
170
+ this.kind = kind;
171
+ this.url = url;
172
+ this.title = title;
173
+ this.mimeType = mimeType;
174
+ this.width = width;
175
+ this.height = height;
176
+ this.duration = duration;
177
+ this.size = size;
178
+ this.extra = extra;
179
+ }
180
+
181
+ youtubeId(): string | null {
182
+ const youtubeId = this.extra?.youtube_id;
183
+
184
+ return typeof youtubeId === "string" && youtubeId.length > 0 ? youtubeId : null;
185
+ }
186
+ }
187
+
188
+ export function referenceFromJSON({
189
+ kind,
190
+ url,
191
+ title,
192
+ mime_type,
193
+ width,
194
+ height,
195
+ duration,
196
+ size,
197
+ extra
198
+ }: RawReference): Reference {
199
+ return new Reference({
200
+ kind: kind,
201
+ url: toURL(url),
202
+ title: title !== undefined ? title : null,
203
+ mimeType: mime_type !== undefined ? mime_type : null,
204
+ width: width !== undefined ? width : null,
205
+ height: height !== undefined ? height : null,
206
+ duration: duration !== undefined ? duration : null,
207
+ size: size !== undefined ? size : null,
208
+ extra: extra !== undefined ? extra : null
209
+ });
210
+ }
211
+
122
212
  export class Entry {
123
213
  readonly id: EntryId;
124
214
  readonly feedId: FeedId;
@@ -131,6 +221,7 @@ export class Entry {
131
221
  readonly scoreToZero: number;
132
222
  readonly publishedAt: Date;
133
223
  body: string | null;
224
+ references: Reference[] | null;
134
225
 
135
226
  constructor({
136
227
  id,
@@ -142,7 +233,8 @@ export class Entry {
142
233
  score,
143
234
  scoreContributions,
144
235
  publishedAt,
145
- body
236
+ body,
237
+ references
146
238
  }: {
147
239
  id: EntryId;
148
240
  feedId: FeedId;
@@ -154,6 +246,7 @@ export class Entry {
154
246
  scoreContributions: {[key: string]: number};
155
247
  publishedAt: Date;
156
248
  body: string | null;
249
+ references: Reference[] | null;
157
250
  }) {
158
251
  this.id = id;
159
252
  this.feedId = feedId;
@@ -165,6 +258,7 @@ export class Entry {
165
258
  this.scoreContributions = scoreContributions;
166
259
  this.publishedAt = publishedAt;
167
260
  this.body = body;
261
+ this.references = references;
168
262
 
169
263
  this.scoreToZero = -Math.abs(score);
170
264
  }
@@ -190,21 +284,21 @@ export class Entry {
190
284
  }
191
285
  }
192
286
 
193
- export function entryFromJSON(
194
- rawEntry: {
195
- id: string;
196
- feedId: string;
197
- title: string;
198
- url: string;
199
- tags: number[];
200
- markers: string[];
201
- score: number;
202
- scoreContributions: {[key: number]: number};
203
- publishedAt: string;
204
- body: string | null;
205
- },
206
- tagsMapping: {[key: number]: string}
207
- ): Entry {
287
+ export type RawEntry = {
288
+ id: string;
289
+ feedId: string;
290
+ title: string;
291
+ url: string;
292
+ tags: number[];
293
+ markers: string[];
294
+ score: number;
295
+ scoreContributions: {[key: number]: number};
296
+ publishedAt: string;
297
+ body?: string | null;
298
+ references?: RawReference[] | null;
299
+ };
300
+
301
+ export function entryFromJSON(rawEntry: RawEntry, tagsMapping: {[key: number]: string}): Entry {
208
302
  const contributions: {[key: string]: number} = {};
209
303
 
210
304
  for (const key in rawEntry.scoreContributions) {
@@ -228,8 +322,11 @@ export function entryFromJSON(
228
322
  // map keys from int to string
229
323
  scoreContributions: contributions,
230
324
  publishedAt: new Date(rawEntry.publishedAt),
231
-
232
- body: rawEntry.body
325
+ body: rawEntry.body !== undefined ? rawEntry.body : null,
326
+ references:
327
+ rawEntry.references !== undefined && rawEntry.references !== null
328
+ ? rawEntry.references.map(referenceFromJSON)
329
+ : null
233
330
  });
234
331
  }
235
332
 
@@ -279,17 +376,14 @@ export type EntryInfo = {
279
376
  readonly publishedAt: Date;
280
377
  };
281
378
 
282
- export function entryInfoFromJSON({
283
- title,
284
- body,
285
- url,
286
- publishedAt
287
- }: {
379
+ export type RawEntryInfo = {
288
380
  title: string;
289
381
  body: string;
290
382
  url: string;
291
383
  publishedAt: string;
292
- }): EntryInfo {
384
+ };
385
+
386
+ export function entryInfoFromJSON({title, body, url, publishedAt}: RawEntryInfo): EntryInfo {
293
387
  return {title, body, url: toURL(url), publishedAt: new Date(publishedAt)};
294
388
  }
295
389
 
@@ -301,19 +395,15 @@ export type FeedInfo = {
301
395
  readonly isLinked: boolean;
302
396
  };
303
397
 
304
- export function feedInfoFromJSON({
305
- url,
306
- title,
307
- description,
308
- entries,
309
- isLinked
310
- }: {
398
+ export type RawFeedInfo = {
311
399
  url: string;
312
400
  title: string;
313
401
  description: string;
314
- entries: any[];
402
+ entries: RawEntryInfo[];
315
403
  isLinked: boolean;
316
- }): FeedInfo {
404
+ };
405
+
406
+ export function feedInfoFromJSON({url, title, description, entries, isLinked}: RawFeedInfo): FeedInfo {
317
407
  return {
318
408
  url: toURL(url),
319
409
  title,
@@ -510,6 +600,32 @@ export function collectionFeedInfoFromJSON({
510
600
  });
511
601
  }
512
602
 
603
+ export class IntegrationInfo {
604
+ readonly name: string;
605
+ readonly discovery: boolean;
606
+ readonly postprocessing: boolean;
607
+
608
+ constructor({name, discovery, postprocessing}: {name: string; discovery: boolean; postprocessing: boolean}) {
609
+ this.name = name;
610
+ this.discovery = discovery;
611
+ this.postprocessing = postprocessing;
612
+ }
613
+ }
614
+
615
+ export type RawIntegrationInfo = {
616
+ name: string;
617
+ discovery: boolean;
618
+ postprocessing: boolean;
619
+ };
620
+
621
+ export function integrationInfoFromJSON({name, discovery, postprocessing}: RawIntegrationInfo): IntegrationInfo {
622
+ return new IntegrationInfo({
623
+ name: name,
624
+ discovery: discovery,
625
+ postprocessing: postprocessing
626
+ });
627
+ }
628
+
513
629
  export class ApiMessage {
514
630
  readonly type: string;
515
631
  readonly code: string;
@@ -1,6 +1,7 @@
1
1
  import _ from "lodash";
2
2
  import type * as t from "@/logic/types";
3
3
  import DOMPurify from "dompurify";
4
+ import * as iframeSanitizer from "@/logic/iframeSanitizer";
4
5
 
5
6
  const REQUIRED_LINK_ATTRIBUTES = {
6
7
  target: "_blank",
@@ -8,6 +9,33 @@ const REQUIRED_LINK_ATTRIBUTES = {
8
9
  referrerpolicy: "strict-origin-when-cross-origin"
9
10
  } as const;
10
11
 
12
+ const INTERFERING_BODY_ATTRIBUTES = new Set(["class", "id", "style"]);
13
+
14
+ const INTERFERING_BODY_ATTRIBUTE_PREFIXES = ["data-"];
15
+
16
+ function isInterferingBodyAttribute(attributeName: string) {
17
+ if (INTERFERING_BODY_ATTRIBUTES.has(attributeName)) {
18
+ return true;
19
+ }
20
+
21
+ return INTERFERING_BODY_ATTRIBUTE_PREFIXES.some((prefix) => attributeName.startsWith(prefix));
22
+ }
23
+
24
+ function removeInterferingAttributes(html: string) {
25
+ const parsed = new DOMParser().parseFromString(html, "text/html");
26
+ const elements = parsed.body.querySelectorAll("*");
27
+
28
+ for (const element of elements) {
29
+ for (const attribute of Array.from(element.attributes)) {
30
+ if (isInterferingBodyAttribute(attribute.name)) {
31
+ element.removeAttribute(attribute.name);
32
+ }
33
+ }
34
+ }
35
+
36
+ return parsed.body.innerHTML;
37
+ }
38
+
11
39
  function hardenLinksSecurityAttributes(html: string) {
12
40
  const parsed = new DOMParser().parseFromString(html, "text/html");
13
41
  const links = parsed.body.querySelectorAll("[href]");
@@ -109,12 +137,14 @@ export function purifyBody({raw, default_}: {raw: string | null; default_: strin
109
137
  return default_;
110
138
  }
111
139
 
112
- let body = DOMPurify.sanitize(raw).trim();
140
+ let body = DOMPurify.sanitize(raw, iframeSanitizer.DOM_PURIFY_IFRAME_OPTIONS).trim();
113
141
 
114
142
  if (body.length === 0) {
115
143
  return default_;
116
144
  }
117
145
 
146
+ body = removeInterferingAttributes(body);
147
+ body = iframeSanitizer.sanitizeIframes(body);
118
148
  body = hardenLinksSecurityAttributes(body);
119
149
 
120
150
  return body;
package/src/main.ts CHANGED
@@ -65,12 +65,18 @@ import SocialLink from "./values/SocialLink.vue";
65
65
  import BodyListReverseTimeColumn from "./components/body_list/ReverseTimeColumn.vue";
66
66
  import BodyListFaviconColumn from "./components/body_list/FaviconColumn.vue";
67
67
  import BodyListEntryBody from "./components/body_list/EntryBody.vue";
68
+ import BodyListEntryCover from "./components/body_list/EntryCover.vue";
69
+ import BodyListReferences from "./components/body_list/References.vue";
70
+ import BodyListReference from "./components/body_list/Reference.vue";
71
+ import IntegrationsYouTube from "./integrations/YouTube.vue";
68
72
 
69
73
  import MainDescription from "./components/main/Description.vue";
70
74
  import MainItem from "./components/main/Item.vue";
71
75
  import MainNewsTitle from "./components/main/NewsTitle.vue";
72
76
  import MainHeaderLine from "./components/main/HeaderLine.vue";
73
77
  import MainBlock from "./components/main/Block.vue";
78
+ import MainIntegrationsTable from "./components/main/IntegrationsTable.vue";
79
+ import MainShowMoreButton from "./components/main/ShowMoreButton.vue";
74
80
 
75
81
  import SidePanelCollapseButton from "./components/side_pannel/CollapseButton.vue";
76
82
 
@@ -138,12 +144,18 @@ app.component("SocialLink", SocialLink);
138
144
  app.component("BodyListReverseTimeColumn", BodyListReverseTimeColumn);
139
145
  app.component("BodyListFaviconColumn", BodyListFaviconColumn);
140
146
  app.component("BodyListEntryBody", BodyListEntryBody);
147
+ app.component("BodyListEntryCover", BodyListEntryCover);
148
+ app.component("BodyListReferences", BodyListReferences);
149
+ app.component("BodyListReference", BodyListReference);
150
+ app.component("IntegrationsYouTube", IntegrationsYouTube);
141
151
 
142
152
  app.component("MainDescription", MainDescription);
143
153
  app.component("MainItem", MainItem);
144
154
  app.component("MainNewsTitle", MainNewsTitle);
145
155
  app.component("MainHeaderLine", MainHeaderLine);
146
156
  app.component("MainBlock", MainBlock);
157
+ app.component("MainIntegrationsTable", MainIntegrationsTable);
158
+ app.component("MainShowMoreButton", MainShowMoreButton);
147
159
 
148
160
  app.component("SidePanelCollapseButton", SidePanelCollapseButton);
149
161
 
@@ -96,6 +96,10 @@ export const useEntriesStore = defineStore("entriesStore", () => {
96
96
  entry.body = existingEntry.body;
97
97
  }
98
98
 
99
+ if (entry.references === null && existingEntry.references !== null) {
100
+ entry.references = existingEntry.references;
101
+ }
102
+
99
103
  if (!updateTags) {
100
104
  entry.tags = _.cloneDeep(existingEntry.tags);
101
105
  }
@@ -202,7 +206,11 @@ export const useEntriesStore = defineStore("entriesStore", () => {
202
206
  });
203
207
 
204
208
  function requestFullEntry({entryId}: {entryId: t.EntryId}) {
205
- if (entryId in entries.value && entries.value[entryId].body !== null) {
209
+ if (
210
+ entryId in entries.value &&
211
+ entries.value[entryId].body !== null &&
212
+ entries.value[entryId].references !== null
213
+ ) {
206
214
  return;
207
215
  }
208
216
 
@@ -0,0 +1,14 @@
1
+ import {defineStore} from "pinia";
2
+ import {computedAsync} from "@vueuse/core";
3
+
4
+ import * as api from "@/logic/api";
5
+
6
+ export const useIntegrationsStore = defineStore("integrationsStore", () => {
7
+ const integrations = computedAsync(async () => {
8
+ return await api.getIntegrations();
9
+ }, []);
10
+
11
+ return {
12
+ integrations
13
+ };
14
+ });
@@ -20,6 +20,15 @@
20
20
  IconChevronsRight,
21
21
  IconLayoutSidebarLeftCollapse,
22
22
  IconLayoutSidebarLeftExpand,
23
+ IconUser,
24
+ IconMessageCircle,
25
+ IconWorld,
26
+ IconPlayerPlay,
27
+ IconVolume,
28
+ IconPhoto,
29
+ IconFileText,
30
+ IconInfoSquareFilled,
31
+ IconCheck,
23
32
  IconX,
24
33
  IconMoodSmile,
25
34
  IconMoodSad
@@ -38,6 +47,15 @@
38
47
  "chevrons-left": IconChevronsLeft,
39
48
  "sidebar-left-collapse": IconLayoutSidebarLeftCollapse,
40
49
  "sidebar-left-expand": IconLayoutSidebarLeftExpand,
50
+ user: IconUser,
51
+ comments: IconMessageCircle,
52
+ world: IconWorld,
53
+ "player-play": IconPlayerPlay,
54
+ volume: IconVolume,
55
+ photo: IconPhoto,
56
+ "file-text": IconFileText,
57
+ "info-square-filled": IconInfoSquareFilled,
58
+ check: IconCheck,
41
59
  x: IconX,
42
60
  "face-smile": IconMoodSmile,
43
61
  "face-sad": IconMoodSad
@@ -60,12 +60,14 @@
60
60
  import {computed, ref, onUnmounted, watch, provide} from "vue";
61
61
  import {computedAsync} from "@vueuse/core";
62
62
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
63
+ import {useGlobalState} from "@/stores/globalState";
63
64
  import {useFeedsStore} from "@/stores/feeds";
64
65
  import * as api from "@/logic/api";
65
66
  import type * as t from "@/logic/types";
66
67
  import * as e from "@/logic/enums";
67
68
 
68
69
  const globalSettings = useGlobalSettingsStore();
70
+ const globalState = useGlobalState();
69
71
 
70
72
  const feedsStore = useFeedsStore();
71
73
 
@@ -73,7 +75,15 @@
73
75
 
74
76
  globalSettings.mainPanelMode = e.MainPanelMode.Feeds;
75
77
 
78
+ const readyToUseSettings = computed(() => {
79
+ return globalSettings.userSettingsPresent || !globalState.loginConfirmed;
80
+ });
81
+
76
82
  const sortedFeeds = computed(() => {
83
+ if (!readyToUseSettings.value) {
84
+ return null;
85
+ }
86
+
77
87
  let sorted = Object.values(feedsStore.feeds);
78
88
 
79
89
  if (sorted.length === 0) {
@@ -10,7 +10,7 @@
10
10
 
11
11
  <main-block>
12
12
  <h1 class="m-0 text-5xl">Feeds Fun</h1>
13
- <p class="mt-2 text-2xl">Transparent & Personalized News</p>
13
+ <p class="mt-2 text-2xl">Personalized News You Control</p>
14
14
 
15
15
  <div class="h-12 grid grid-flow-col auto-cols-fr gap-3 w-max mx-auto">
16
16
  <a
@@ -224,14 +224,16 @@
224
224
  <div
225
225
  v-if="showMoreCollectionsButtonRequired"
226
226
  class="mt-4 text-center">
227
- <button
228
- class="ffun-main-button short"
229
- @click="showAllCollections = !showAllCollections">
230
- {{ showAllCollections ? "Show less" : "Show more" }}
231
- </button>
227
+ <main-show-more-button v-model:expanded="showAllCollections" />
232
228
  </div>
233
229
  </main-block>
234
230
 
231
+ <main-header-line v-if="settings.hasIntegrations"> Advanced support for popular sources </main-header-line>
232
+
233
+ <main-block v-if="settings.hasIntegrations">
234
+ <main-integrations-table />
235
+ </main-block>
236
+
235
237
  <main-header-line> Here, take a peek </main-header-line>
236
238
  <div class="text-center p-5">
237
239
  <img
@@ -41,6 +41,7 @@
41
41
  import {useRoute, useRouter} from "vue-router";
42
42
  import {computedAsync} from "@vueuse/core";
43
43
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
44
+ import {useGlobalState} from "@/stores/globalState";
44
45
  import _ from "lodash";
45
46
  import * as utils from "@/logic/utils";
46
47
  import * as api from "@/logic/api";
@@ -57,6 +58,7 @@
57
58
  provide("eventsViewName", "rules");
58
59
 
59
60
  const globalSettings = useGlobalSettingsStore();
61
+ const globalState = useGlobalState();
60
62
 
61
63
  tagsFilterState.setSyncingTagsWithRoute({
62
64
  tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
@@ -75,7 +77,11 @@
75
77
  router.push({name: e.MainPanelMode.Entries, params: {}});
76
78
  }
77
79
 
78
- const loading = computed(() => rules.value === null);
80
+ const readyToUseSettings = computed(() => {
81
+ return globalSettings.userSettingsPresent || !globalState.loginConfirmed;
82
+ });
83
+
84
+ const loading = computed(() => rules.value === null || !readyToUseSettings.value);
79
85
 
80
86
  const rules = computedAsync(async () => {
81
87
  // force refresh
@@ -84,7 +90,7 @@
84
90
  }, null);
85
91
 
86
92
  const sortedRules = computed(() => {
87
- if (!rules.value) {
93
+ if (!rules.value || !readyToUseSettings.value) {
88
94
  return null;
89
95
  }
90
96