feeds-fun 1.17.1 → 1.18.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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/components/EntriesList.vue +11 -0
  3. package/src/components/EntryForList.vue +17 -6
  4. package/src/components/RuleForList.vue +2 -2
  5. package/src/components/RulesList.vue +18 -9
  6. package/src/components/TokensCost.vue +0 -2
  7. package/src/components/collections/DetailedItem.vue +9 -0
  8. package/src/components/collections/PublicIntro.vue +63 -0
  9. package/src/components/collections/PublicSelector.vue +43 -0
  10. package/src/components/main/Block.vue +1 -1
  11. package/src/components/main/Item.vue +1 -1
  12. package/src/components/notifications/LoadedOldNews.vue +47 -0
  13. package/src/components/page_header/ExternalLinks.vue +11 -6
  14. package/src/components/side_pannel/CollapseButton.vue +56 -0
  15. package/src/components/tags/EntryTag.vue +4 -3
  16. package/src/components/tags/FilterTag.vue +6 -3
  17. package/src/components/tags/RuleTag.vue +4 -3
  18. package/src/components/tags/TagsFilter.vue +17 -5
  19. package/src/css/panels.css +5 -5
  20. package/src/css/side_panel_layout.css +4 -0
  21. package/src/layouts/SidePanelLayout.vue +51 -26
  22. package/src/layouts/WideLayout.vue +0 -4
  23. package/src/logic/api.ts +23 -0
  24. package/src/logic/enums.ts +28 -12
  25. package/src/logic/events.ts +56 -8
  26. package/src/logic/tagsFilterState.ts +61 -1
  27. package/src/logic/types.ts +15 -2
  28. package/src/logic/utils.ts +104 -1
  29. package/src/main.ts +10 -0
  30. package/src/router/index.ts +16 -3
  31. package/src/stores/collections.ts +17 -1
  32. package/src/stores/entries.ts +154 -19
  33. package/src/stores/globalSettings.ts +11 -8
  34. package/src/views/AuthView.vue +3 -1
  35. package/src/views/CollectionsView.vue +13 -2
  36. package/src/views/DiscoveryView.vue +3 -1
  37. package/src/views/FeedsView.vue +3 -1
  38. package/src/views/MainView.vue +18 -1
  39. package/src/views/NewsView.vue +36 -97
  40. package/src/views/PublicCollectionView.vue +237 -0
  41. package/src/views/RulesView.vue +31 -29
  42. package/src/views/SettingsView.vue +3 -1
@@ -9,6 +9,9 @@
9
9
 
10
10
  <template #side-menu-item-2>
11
11
  Sort by
12
+ <!-- Remember that we should use entriesStore.activeOrderProperties everywhere in the News view-->
13
+ <!-- Here, It should be in sync with this selector (and globalSettings.entriesOrder) on the NewsView, -->
14
+ <!-- so, it should work fine -->
12
15
  <config-selector
13
16
  :values="e.EntriesOrderProperties"
14
17
  v-model:property="globalSettings.entriesOrder" />
@@ -35,8 +38,7 @@
35
38
  <template #side-footer>
36
39
  <tags-filter
37
40
  :tags="tagsCount"
38
- :show-create-rule="true"
39
- change-source="news_tags_filter" />
41
+ :show-create-rule="true" />
40
42
  </template>
41
43
 
42
44
  <template #main-header>
@@ -53,10 +55,16 @@
53
55
  :collections-notification_="!hasEntries"
54
56
  :collections-warning_="false" />
55
57
 
58
+ <notifications-loaded-old-news
59
+ :entries="entriesStore.loadedEntriesReport || []"
60
+ :period="e.LastEntriesPeriodProperties.get(globalSettings.lastEntriesPeriod)" />
61
+
56
62
  <entries-list
63
+ :loading="entriesStore.loading"
57
64
  :entriesIds="entriesReport"
58
- :time-field="timeField"
65
+ :time-field="entriesStore.activeOrderProperties.timeField"
59
66
  :tags-count="tagsCount"
67
+ :show-score="true"
60
68
  :showFromStart="25"
61
69
  :showPerPage="25" />
62
70
  </side-panel-layout>
@@ -64,10 +72,12 @@
64
72
 
65
73
  <script lang="ts" setup>
66
74
  import {computed, ref, onUnmounted, watch, provide} from "vue";
75
+ import {useRoute, useRouter} from "vue-router";
67
76
  import {computedAsync} from "@vueuse/core";
68
77
  import * as api from "@/logic/api";
69
78
  import * as tagsFilterState from "@/logic/tagsFilterState";
70
79
  import * as e from "@/logic/enums";
80
+ import * as utils from "@/logic/utils";
71
81
  import type * as t from "@/logic/types";
72
82
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
73
83
  import {useEntriesStore} from "@/stores/entries";
@@ -76,107 +86,42 @@
76
86
  const globalSettings = useGlobalSettingsStore();
77
87
  const entriesStore = useEntriesStore();
78
88
 
79
- const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
80
-
81
- provide("tagsStates", tagsStates);
82
-
83
- globalSettings.mainPanelMode = e.MainPanelMode.Entries;
84
-
85
- globalSettings.updateDataVersion();
86
-
87
- const entriesWithOpenedBody = ref<{[key: t.EntryId]: boolean}>({});
88
-
89
- const _sortedEntries = computed(() => {
90
- if (entriesStore.loadedEntriesReport === null) {
91
- return [];
92
- }
93
-
94
- const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
95
-
96
- if (orderProperties === undefined) {
97
- throw new Error(`Unknown order ${globalSettings.entriesOrder}`);
98
- }
99
-
100
- const field = orderProperties.orderField;
101
- const direction = orderProperties.direction;
102
-
103
- // let report = entriesStore.loadedEntriesReport.slice();
104
-
105
- // Pre-map to avoid repeated lookups in the comparator
106
- const mapped = entriesStore.loadedEntriesReport.map((entryId) => {
107
- // @ts-ignore
108
- return {entryId, value: entriesStore.entries[entryId][field]};
109
- });
89
+ const route = useRoute();
90
+ const router = useRouter();
110
91
 
111
- mapped.sort((a: {entryId: t.EntryId; value: any}, b: {entryId: t.EntryId; value: any}) => {
112
- if (a.value === null && b.value === null) {
113
- return 0;
114
- }
115
-
116
- if (a.value === null) {
117
- return 1;
118
- }
92
+ entriesStore.setNewsMode();
119
93
 
120
- if (b.value === null) {
121
- return -1;
122
- }
123
-
124
- if (a.value < b.value) {
125
- return direction;
126
- }
127
-
128
- if (a.value > b.value) {
129
- return -direction;
130
- }
94
+ const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
131
95
 
132
- return 0;
133
- });
96
+ provide("tagsStates", tagsStates);
97
+ provide("eventsViewName", "news");
134
98
 
135
- const report = mapped.map((x) => x.entryId);
99
+ tagsFilterState.setSyncingTagsWithRoute({
100
+ tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
101
+ route,
102
+ router
103
+ });
136
104
 
137
- return report;
105
+ tagsFilterState.setSyncingTagsSidebarPoint({
106
+ tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
107
+ globalSettings
138
108
  });
139
109
 
140
- const _visibleEntries = computed(() => {
141
- let report = _sortedEntries.value.slice();
142
-
143
- if (!globalSettings.showRead) {
144
- report = report.filter((entryId) => {
145
- if (entriesStore.displayedEntryId == entryId) {
146
- // always show read entries with open body
147
- // otherwise, they will hide right after opening it
148
- return true;
149
- }
150
- return !entriesStore.entries[entryId].hasMarker(e.Marker.Read);
151
- });
152
- }
110
+ globalSettings.mainPanelMode = e.MainPanelMode.Entries;
153
111
 
154
- return report;
155
- });
112
+ globalSettings.updateDataVersion();
156
113
 
157
114
  const entriesReport = computed(() => {
158
- let report = _visibleEntries.value.slice();
115
+ let report = entriesStore.visibleEntries.slice();
159
116
 
160
117
  report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
161
118
  return report;
162
119
  });
163
120
 
164
121
  const tagsCount = computed(() => {
165
- const tagsCount: {[key: string]: number} = {};
166
-
167
- for (const entryId of entriesReport.value) {
168
- const entry = entriesStore.entries[entryId];
122
+ const entriesToProcess = entriesReport.value.map((entryId) => entriesStore.entries[entryId]);
169
123
 
170
- for (const tag of entry.tags) {
171
- if (tag in tagsCount) {
172
- tagsCount[tag] += 1;
173
- } else {
174
- tagsCount[tag] = 1;
175
- }
176
- }
177
- }
178
-
179
- return tagsCount;
124
+ return utils.countTags(entriesToProcess);
180
125
  });
181
126
 
182
127
  const entriesNumber = computed(() => {
@@ -188,6 +133,10 @@
188
133
  });
189
134
 
190
135
  const hasRules = computed(() => {
136
+ if (entriesStore.loading) {
137
+ return false;
138
+ }
139
+
191
140
  if (entriesStore.loadedEntriesReport === null) {
192
141
  return false;
193
142
  }
@@ -201,16 +150,6 @@
201
150
  }
202
151
  return false;
203
152
  });
204
-
205
- const timeField = computed(() => {
206
- const orderProperties = e.EntriesOrderProperties.get(globalSettings.entriesOrder);
207
-
208
- if (orderProperties === undefined) {
209
- throw new Error(`Unknown entries order: ${globalSettings.entriesOrder}`);
210
- }
211
-
212
- return orderProperties.timeField;
213
- });
214
153
  </script>
215
154
 
216
155
  <style></style>
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <side-panel-layout
3
+ :reloadButton="false"
4
+ :login-required="false"
5
+ :home-button="true">
6
+ <template #side-menu-item-1>
7
+ <collections-public-selector
8
+ class="min-w-full"
9
+ v-if="collection"
10
+ :collection-id="collection.id" />
11
+
12
+ <p
13
+ v-if="collection"
14
+ class="ffun-info-common my-2"
15
+ >{{ collection.description }}</p
16
+ >
17
+ </template>
18
+ <template #side-menu-item-2>
19
+ For
20
+ <config-selector
21
+ :values="e.LastEntriesPeriodProperties"
22
+ v-model:property="globalSettings.lastEntriesPeriod" />
23
+ </template>
24
+
25
+ <template #side-menu-item-3>
26
+ Show read
27
+
28
+ <config-flag
29
+ style="min-width: 2.5rem"
30
+ v-model:flag="globalSettings.showRead"
31
+ on-text="no"
32
+ off-text="yes" />
33
+
34
+ <button
35
+ class="ffun-form-button py-0 ml-1"
36
+ title='Undo last "mark read" operation'
37
+ :disabled="!entriesStore.canUndoMarkRead"
38
+ @click="entriesStore.undoMarkRead()">
39
+
40
+ </button>
41
+ </template>
42
+
43
+ <template #side-footer>
44
+ <tags-filter
45
+ :tags="tagsCount"
46
+ :show-create-rule="false"
47
+ :show-registration-invitation="true" />
48
+ </template>
49
+
50
+ <template #main-header>
51
+ News
52
+ <span v-if="entriesNumber > 0">[{{ entriesNumber }}]</span>
53
+ </template>
54
+
55
+ <template #main-footer> </template>
56
+
57
+ <!-- currently we have a "nuance" with tags user experience in this block -->
58
+ <!-- The tags work as expected till the user selects their own tags from other places -->
59
+ <!-- after that we can get a situation, when, after clicking on a tag in the block, there will be no news displayed -->
60
+ <!-- because the user previously selected tags that have no common news with the tags in the block -->
61
+ <!-- That effect is possible only if the user has already interacted with the ags filter => should not be a problem -->
62
+ <collections-public-intro
63
+ v-if="collection && !globalState.isLoggedIn"
64
+ :collectionId="collection.id"
65
+ :tag1Uid="medianTag1"
66
+ :tag1Count="tagsCount[medianTag1] || 0"
67
+ :tag2Uid="medianTag2"
68
+ :tag2Count="tagsCount[medianTag2] || 0" />
69
+
70
+ <div
71
+ v-if="collection && globalState.isLoggedIn"
72
+ class="ffun-info-good">
73
+ <p
74
+ >Welcome to curated <strong>{{ collection.name }}</strong> news collection!</p
75
+ >
76
+
77
+ <p>Collection news is always shown in the order of publication.</p>
78
+ </div>
79
+
80
+ <notifications-loaded-old-news
81
+ :entries="entriesStore.loadedEntriesReport || []"
82
+ :period="e.LastEntriesPeriodProperties.get(globalSettings.lastEntriesPeriod)" />
83
+
84
+ <entries-list
85
+ :loading="entriesStore.loading"
86
+ :entriesIds="entriesReport"
87
+ :time-field="entriesStore.activeOrderProperties.timeField"
88
+ :tags-count="tagsCount"
89
+ :show-score="false"
90
+ :showFromStart="25"
91
+ :showPerPage="25" />
92
+ </side-panel-layout>
93
+ </template>
94
+
95
+ <script lang="ts" setup>
96
+ import {computed, ref, onUnmounted, watch, provide} from "vue";
97
+ import type {ComputedRef} from "vue";
98
+ import {useRoute, useRouter} from "vue-router";
99
+ import {computedAsync} from "@vueuse/core";
100
+ import * as api from "@/logic/api";
101
+ import * as tagsFilterState from "@/logic/tagsFilterState";
102
+ import * as e from "@/logic/enums";
103
+ import * as utils from "@/logic/utils";
104
+ import type * as t from "@/logic/types";
105
+ import {useGlobalSettingsStore} from "@/stores/globalSettings";
106
+ import {useEntriesStore} from "@/stores/entries";
107
+ import {useCollectionsStore} from "@/stores/collections";
108
+ import {useGlobalState} from "@/stores/globalState";
109
+ import _ from "lodash";
110
+ import * as asserts from "@/logic/asserts";
111
+
112
+ const route = useRoute();
113
+ const router = useRouter();
114
+
115
+ const globalState = useGlobalState();
116
+ const globalSettings = useGlobalSettingsStore();
117
+ const entriesStore = useEntriesStore();
118
+ const collections = useCollectionsStore();
119
+
120
+ const collectionSlug = computed(() => route.params.collectionSlug as t.CollectionSlug);
121
+
122
+ const collection = computed(() => {
123
+ if (!collectionSlug.value) {
124
+ return null;
125
+ }
126
+
127
+ const result = collections.getCollectionBySlug({slug: collectionSlug.value});
128
+
129
+ if (Object.keys(collections.collections).length > 0 && !result) {
130
+ // TODO: implement better behaviour for broken slugs
131
+ router.push({name: "main"});
132
+ }
133
+
134
+ return result;
135
+ });
136
+
137
+ const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
138
+
139
+ globalSettings.mainPanelMode = e.MainPanelMode.PublicCollection;
140
+
141
+ // Required to separate real collection change (and reset tags filter) from the collection initialization
142
+ const lastDefinedCollectionId = ref<t.CollectionId | null>(null);
143
+
144
+ watch(
145
+ collection,
146
+ () => {
147
+ if (!collection.value) {
148
+ return;
149
+ }
150
+
151
+ entriesStore.setPublicCollectionMode(collection.value.slug);
152
+
153
+ if (lastDefinedCollectionId.value !== null && lastDefinedCollectionId.value !== collection.value.id) {
154
+ tagsStates.value.clear();
155
+ }
156
+
157
+ if (lastDefinedCollectionId.value !== collection.value.id) {
158
+ lastDefinedCollectionId.value = collection.value.id;
159
+ }
160
+ },
161
+ {immediate: true}
162
+ );
163
+
164
+ provide("tagsStates", tagsStates);
165
+ provide("eventsViewName", "public_collections");
166
+
167
+ tagsFilterState.setSyncingTagsWithRoute({
168
+ tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
169
+ route,
170
+ router
171
+ });
172
+
173
+ globalSettings.updateDataVersion();
174
+
175
+ const entriesReport = computed(() => {
176
+ let report = entriesStore.visibleEntries.slice();
177
+
178
+ report = tagsStates.value.filterByTags(report, (entryId) => entriesStore.entries[entryId].tags);
179
+ return report;
180
+ });
181
+
182
+ const tagsCount = computed(() => {
183
+ const entriesToProcess = entriesReport.value.map((entryId) => entriesStore.entries[entryId]);
184
+
185
+ return utils.countTags(entriesToProcess);
186
+ });
187
+
188
+ const entriesNumber = computed(() => {
189
+ return entriesReport.value.length;
190
+ });
191
+
192
+ const medianTag1: ComputedRef<string> = computed(() => {
193
+ // do not change tag when the filter changed
194
+ if (tagsStates.value.hasSelectedTags) {
195
+ return medianTag1.value;
196
+ }
197
+
198
+ const entriesNumber = entriesReport.value.length;
199
+
200
+ const result = utils.chooseTagByUsage({tagsCount: tagsCount.value, border: 0.5 * entriesNumber, exclude: []});
201
+
202
+ if (result === null) {
203
+ return "";
204
+ }
205
+
206
+ return result;
207
+ });
208
+
209
+ const medianTag2: ComputedRef<string> = computed(() => {
210
+ // do not change tag when the filter changed
211
+ if (tagsStates.value.hasSelectedTags) {
212
+ return medianTag2.value;
213
+ }
214
+
215
+ const entriesToProcess = entriesReport.value
216
+ .map((entryId) => entriesStore.entries[entryId])
217
+ .filter((entry) => entry.tags.includes(medianTag1.value));
218
+
219
+ const entriesNumber = entriesToProcess.length;
220
+
221
+ const counts = utils.countTags(entriesToProcess);
222
+
223
+ const result = utils.chooseTagByUsage({
224
+ tagsCount: counts,
225
+ border: 0.5 * entriesNumber,
226
+ exclude: [medianTag1.value]
227
+ });
228
+
229
+ if (result === null) {
230
+ return "";
231
+ }
232
+
233
+ return result;
234
+ });
235
+ </script>
236
+
237
+ <style></style>
@@ -2,7 +2,7 @@
2
2
  <side-panel-layout :reload-button="true">
3
3
  <template #main-header>
4
4
  Rules
5
- <span v-if="rules">[{{ rules.length }}]</span>
5
+ <span v-if="rules">[{{ rulesNumber }}]</span>
6
6
  </template>
7
7
 
8
8
  <template #side-menu-item-2>
@@ -15,8 +15,7 @@
15
15
  <template #side-footer>
16
16
  <tags-filter
17
17
  :tags="tagsCount"
18
- :show-create-rule="false"
19
- change-source="rules_tags_filter" />
18
+ :show-create-rule="false" />
20
19
  </template>
21
20
 
22
21
  <div class="ffun-info-common mb-2">
@@ -32,14 +31,14 @@
32
31
  </div>
33
32
 
34
33
  <rules-list
35
- v-if="rules"
34
+ :loading="loading"
36
35
  :rules="sortedRules" />
37
36
  </side-panel-layout>
38
37
  </template>
39
38
 
40
39
  <script lang="ts" setup>
41
40
  import {computed, ref, onUnmounted, watch, provide} from "vue";
42
- import {useRouter} from "vue-router";
41
+ import {useRoute, useRouter} from "vue-router";
43
42
  import {computedAsync} from "@vueuse/core";
44
43
  import {useGlobalSettingsStore} from "@/stores/globalSettings";
45
44
  import _ from "lodash";
@@ -49,46 +48,41 @@
49
48
  import * as e from "@/logic/enums";
50
49
  import * as tagsFilterState from "@/logic/tagsFilterState";
51
50
 
51
+ const route = useRoute();
52
52
  const router = useRouter();
53
53
 
54
54
  const tagsStates = ref<tagsFilterState.Storage>(new tagsFilterState.Storage());
55
55
 
56
56
  provide("tagsStates", tagsStates);
57
+ provide("eventsViewName", "rules");
57
58
 
58
59
  const globalSettings = useGlobalSettingsStore();
59
60
 
61
+ tagsFilterState.setSyncingTagsWithRoute({
62
+ tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
63
+ route,
64
+ router
65
+ });
66
+
67
+ tagsFilterState.setSyncingTagsSidebarPoint({
68
+ tagsStates: tagsStates.value as unknown as tagsFilterState.Storage,
69
+ globalSettings
70
+ });
71
+
60
72
  globalSettings.mainPanelMode = e.MainPanelMode.Rules;
61
73
 
62
74
  function goToNews() {
63
75
  router.push({name: e.MainPanelMode.Entries, params: {}});
64
76
  }
65
77
 
78
+ const loading = computed(() => rules.value === null);
79
+
66
80
  const rules = computedAsync(async () => {
67
81
  // force refresh
68
82
  globalSettings.dataVersion;
69
83
  return await api.getRules();
70
84
  }, null);
71
85
 
72
- const tagsCount = computed(() => {
73
- if (!rules.value) {
74
- return {};
75
- }
76
-
77
- const tags: {[key: string]: number} = {};
78
-
79
- for (const rule of rules.value) {
80
- for (const tag of rule.requiredTags) {
81
- tags[tag] = (tags[tag] || 0) + 1;
82
- }
83
-
84
- for (const tag of rule.excludedTags) {
85
- tags[tag] = (tags[tag] || 0) + 1;
86
- }
87
- }
88
-
89
- return tags;
90
- });
91
-
92
86
  const sortedRules = computed(() => {
93
87
  if (!rules.value) {
94
88
  return null;
@@ -96,7 +90,7 @@
96
90
 
97
91
  let sorted = rules.value.slice();
98
92
 
99
- sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.allTags);
93
+ sorted = tagsStates.value.filterByTags(sorted, (rule) => rule.tags);
100
94
 
101
95
  const orderProperties = e.RulesOrderProperties.get(globalSettings.rulesOrder);
102
96
 
@@ -113,14 +107,14 @@
113
107
 
114
108
  sorted = sorted.sort((a: t.Rule, b: t.Rule) => {
115
109
  if (globalSettings.rulesOrder === e.RulesOrder.Tags) {
116
- return utils.compareLexicographically(a.allTags, b.allTags);
110
+ return utils.compareLexicographically(a.tags, b.tags);
117
111
  }
118
112
 
119
113
  const valueA = _.get(a, orderField, null);
120
114
  const valueB = _.get(b, orderField, null);
121
115
 
122
116
  if (valueA === null && valueB === null) {
123
- return utils.compareLexicographically(a.allTags, b.allTags);
117
+ return utils.compareLexicographically(a.tags, b.tags);
124
118
  }
125
119
 
126
120
  if (valueA === null) {
@@ -139,9 +133,17 @@
139
133
  return -1 * direction;
140
134
  }
141
135
 
142
- return utils.compareLexicographically(a.allTags, b.allTags);
136
+ return utils.compareLexicographically(a.tags, b.tags);
143
137
  });
144
138
 
145
139
  return sorted;
146
140
  });
141
+
142
+ const rulesNumber = computed(() => {
143
+ return sortedRules.value ? sortedRules.value.length : 0;
144
+ });
145
+
146
+ const tagsCount = computed(() => {
147
+ return utils.countTags(sortedRules.value);
148
+ });
147
149
  </script>
@@ -120,7 +120,7 @@
120
120
  </template>
121
121
 
122
122
  <script lang="ts" setup>
123
- import {computed, ref, onUnmounted, watch} from "vue";
123
+ import {computed, ref, onUnmounted, watch, provide} from "vue";
124
124
  import {computedAsync} from "@vueuse/core";
125
125
  import * as api from "@/logic/api";
126
126
  import * as t from "@/logic/types";
@@ -130,6 +130,8 @@
130
130
 
131
131
  const globalSettings = useGlobalSettingsStore();
132
132
 
133
+ provide("eventsViewName", "settings");
134
+
133
135
  globalSettings.mainPanelMode = e.MainPanelMode.Settings;
134
136
 
135
137
  const tokensCostData = computedAsync(async () => {