summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDevlin Junker <devlin.junker@gmail.com>2023-08-25 07:42:12 -0700
committerBenjamin Brahmer <info@b-brahmer.de>2023-08-28 07:03:07 +0200
commitbb154d0a49d30932bb8b2aedfbcbf34f3aeee46c (patch)
tree07d38f0efe5dab33726c449bcbfcc7c6285285f6
parentd4d20e91e5cdd801f837cbd296e1e798cbcfbf12 (diff)
add Feed component and filtering in FeedItemDisplayList
Signed-off-by: Devlin Junker <devlin.junker@gmail.com>
-rw-r--r--src/components/Feed.vue85
-rw-r--r--src/components/FeedItemDisplay.vue9
-rw-r--r--src/components/FeedItemDisplayList.vue145
-rw-r--r--src/components/FeedItemRow.vue12
-rw-r--r--src/components/Sidebar.vue28
-rw-r--r--src/components/Starred.vue40
-rw-r--r--src/components/Unread.vue30
-rw-r--r--src/dataservices/item.service.ts23
-rw-r--r--src/routes/index.ts8
-rw-r--r--src/store/item.ts11
-rw-r--r--src/types/FeedItem.ts1
11 files changed, 321 insertions, 71 deletions
diff --git a/src/components/Feed.vue b/src/components/Feed.vue
new file mode 100644
index 000000000..33c5ed6dd
--- /dev/null
+++ b/src/components/Feed.vue
@@ -0,0 +1,85 @@
+<template>
+ <div class="route-container">
+ <div class="header">
+ {{ feed ? feed.title : '' }}
+ <NcCounterBubble v-if="feed" class="counter-bubble">
+ {{ feed.unreadCount }}
+ </NcCounterBubble>
+ </div>
+
+ <FeedItemDisplayList :items="items" :fetch-key="'feed-'+feedId" @load-more="fetchMore()" />
+ </div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue'
+import { mapState } from 'vuex'
+
+import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
+
+import FeedItemDisplayList from './FeedItemDisplayList.vue'
+
+import { FeedItem } from '../types/FeedItem'
+import { ACTIONS, MUTATIONS } from '../store'
+import { Feed } from '../types/Feed'
+
+export default Vue.extend({
+ components: {
+ NcCounterBubble,
+ FeedItemDisplayList,
+ },
+ props: {
+ feedId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['items', 'feeds']),
+ feed(): Feed {
+ return this.$store.getters.feeds.find((feed: Feed) => feed.id === this.id)
+ },
+ items(): FeedItem[] {
+ return this.$store.state.items.allItems.filter((item: FeedItem) => {
+ return item.feedId === this.id
+ }) || []
+ },
+ id(): number {
+ return Number(this.feedId)
+ },
+ },
+ created() {
+ this.$store.commit(MUTATIONS.SET_SELECTED_ITEM, { id: undefined })
+ this.fetchMore()
+ this.$watch(() => this.$route.params, this.fetchMore)
+
+ },
+ methods: {
+ async fetchMore() {
+ if (!this.$store.state.items.fetchingItems['feed-' + this.feedId]) {
+ this.$store.dispatch(ACTIONS.FETCH_FEED_ITEMS, { feedId: this.id, start: this.items && this.items.length > 0 ? this.items[this.items.length - 1].id : 0 })
+ }
+ },
+ },
+})
+</script>
+
+<style scoped>
+.route-container {
+ height: 100%;
+}
+
+.header {
+ padding-left: 50px;
+ position: absolute;
+ top: 1em;
+ font-weight: 700;
+}
+
+.counter-bubble {
+ display: inline-block;
+ vertical-align: sub;
+ margin-left: 10px;
+}
+
+</style>
diff --git a/src/components/FeedItemDisplay.vue b/src/components/FeedItemDisplay.vue
index 5818fad51..1d7d5a843 100644
--- a/src/components/FeedItemDisplay.vue
+++ b/src/components/FeedItemDisplay.vue
@@ -183,7 +183,7 @@ export default Vue.extend({
</script>
-<style>
+<style scoped>
.feed-item-display {
max-height: 100%;
overflow-y: hidden;
@@ -228,11 +228,10 @@ export default Vue.extend({
.article h1 {
font-weight: bold;
font-size: 17px;
- margin-top: 25px;
}
.action-bar {
- padding: 20px;
+ padding: 0px 20px 0px 20px;
display: flex;
justify-content: right
@@ -242,4 +241,8 @@ export default Vue.extend({
cursor: pointer;
margin: 5px;
}
+
+ .action-bar .material-design-icon:hover {
+ color: var(--color-text-light);
+ }
</style>
diff --git a/src/components/FeedItemDisplayList.vue b/src/components/FeedItemDisplayList.vue
index e86aa93c1..11be6266a 100644
--- a/src/components/FeedItemDisplayList.vue
+++ b/src/components/FeedItemDisplayList.vue
@@ -1,17 +1,44 @@
<template>
- <div class="feed-item-display-container">
- <VirtualScroll :reached-end="reachedEnd"
- :fetch-key="fetchKey"
- @load-more="fetchMore()">
- <template v-if="items && items.length > 0">
- <template v-for="item in items">
- <FeedItemRow :key="item.id" :item="item" />
+ <div>
+ <div style="justify-content: right; display: flex">
+ <NcActions class="filter-container" :force-menu="true">
+ <template #icon>
+ <FilterIcon />
</template>
- </template>
- </VirtualScroll>
+ <NcActionButton v-if="config.unreadFilter" @click="toggleFilter(unreadFilter)">
+ <template #default>
+ {{ t("news", "Unread") }}
+ </template>
+ <template #icon>
+ <EyeIcon v-if="filter !== unreadFilter" />
+ <EyeCheckIcon v-if="filter === unreadFilter" />
+ </template>
+ </NcActionButton>
+ <NcActionButton v-if="config.starFilter" @click="toggleFilter(starFilter)">
+ <template #default>
+ {{ t("news", "Starred") }}
+ </template>
+ <template #icon>
+ <StarIcon v-if="filter !== starFilter" />
+ <StarCheckIcon v-if="filter === starFilter" />
+ </template>
+ </NcActionButton>
+ </NcActions>
+ </div>
+ <div class="feed-item-display-container">
+ <VirtualScroll :reached-end="reachedEnd"
+ :fetch-key="fetchKey"
+ @load-more="fetchMore()">
+ <template v-if="items && items.length > 0">
+ <template v-for="item in filterSortedItems()">
+ <FeedItemRow :key="item.id" :item="item" />
+ </template>
+ </template>
+ </VirtualScroll>
- <div v-if="selected !== undefined" class="feed-item-container">
- <FeedItemDisplay :item="selected" />
+ <div v-if="selected !== undefined" class="feed-item-container">
+ <FeedItemDisplay :item="selected" />
+ </div>
</div>
</div>
</template>
@@ -19,6 +46,15 @@
<script lang="ts">
import Vue from 'vue'
+import FilterIcon from 'vue-material-design-icons/Filter.vue'
+import StarIcon from 'vue-material-design-icons/Star.vue'
+import StarCheckIcon from 'vue-material-design-icons/StarCheck.vue'
+import EyeIcon from 'vue-material-design-icons/Eye.vue'
+import EyeCheckIcon from 'vue-material-design-icons/EyeCheck.vue'
+
+import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
+import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
+
import VirtualScroll from './VirtualScroll.vue'
import FeedItemRow from './FeedItemRow.vue'
import FeedItemDisplay from './FeedItemDisplay.vue'
@@ -30,20 +66,49 @@ export default Vue.extend({
VirtualScroll,
FeedItemRow,
FeedItemDisplay,
+ FilterIcon,
+ StarIcon,
+ StarCheckIcon,
+ EyeIcon,
+ EyeCheckIcon,
+ NcActions,
+ NcActionButton,
},
props: {
items: {
- type: Array,
+ type: Array<FeedItem>,
required: true,
},
fetchKey: {
type: String,
required: true,
},
+ config: {
+ type: Object,
+ default: () => {
+ return {
+ unreadFilter: true,
+ starFilter: true,
+ }
+ },
+ },
},
data() {
return {
mounted: false,
+
+ // no filter to start
+ filter: () => { return true as boolean },
+
+ // Always want to sort by date (most recent first)
+ sort: (a: FeedItem, b: FeedItem) => {
+ if (a.pubDate > b.pubDate) {
+ return -1
+ } else {
+ return 1
+ }
+ },
+ cache: [] as FeedItem[] | undefined,
}
},
computed: {
@@ -61,22 +126,70 @@ export default Vue.extend({
fetchMore() {
this.$emit('load-more')
},
+ noFilter(): boolean {
+ return true
+ },
+ starFilter(item: FeedItem): boolean {
+ return item.starred
+ },
+ unreadFilter(item: FeedItem): boolean {
+ return item.unread
+ },
+ toggleFilter(filter: () => boolean) {
+ if (this.filter === filter) {
+ this.filter = this.noFilter
+ if (filter === this.unreadFilter) {
+ this.cache = undefined
+ }
+ } else {
+ this.filter = filter as () => boolean
+ }
+ },
+ filterSortedItems(): FeedItem[] {
+ let response = [...this.items] as FeedItem[]
+
+ // if we're filtering on unread, we want to cache the unread items when the user presses the filter button
+ // that way when the user opens an item, it won't be removed from the displayed list of items (once it's no longer unread)
+ if (this.filter === this.unreadFilter) {
+ if (!this.cache) {
+ if (this.items.length > 0) {
+ this.cache = this.items.filter(this.unreadFilter)
+ }
+ } else if (this.items.length > (this.cache?.length)) {
+ for (const item of this.items) {
+ if (item.unread && this.cache.find((unread: FeedItem) => unread.id === item.id) === undefined) {
+ this.cache.push(item)
+ }
+ }
+ }
+ response = [...this.cache as FeedItem[]]
+ } else {
+ response = response.filter(this.filter)
+ }
+
+ return response.sort(this.sort)
+ },
},
})
</script>
<style scoped>
+ .virtual-scroll {
+ border-top: 1px solid var(--color-border);
+ width: 100%;
+ }
+
.feed-item-display-container {
display: flex;
height: 100%;
}
- .virtual-scroll {
- width: 100%;
- }
-
.feed-item-container {
max-width: 50%;
overflow-y: hidden;
}
+
+ .filter-container {
+ padding: 5px;
+ }
</style>
diff --git a/src/components/FeedItemRow.vue b/src/components/FeedItemRow.vue
index c7fcfd6ec..6d436ba01 100644
--- a/src/components/FeedItemRow.vue
+++ b/src/components/FeedItemRow.vue
@@ -210,23 +210,23 @@ export default Vue.extend({
height: 30px;
}
- .material-design-icon {
+ .feed-item-row .button-container .material-design-icon {
color: var(--color-text-lighter)
}
- .material-design-icon:hover {
+ .feed-item-row .button-container .material-design-icon:hover {
color: var(--color-text-light);
}
- .material-design-icon.rss-icon:hover {
+ .feed-item-row .button-container .material-design-icon.rss-icon:hover {
color: #555555;
}
.material-design-icon.starred {
- color: rgb(255, 204, 0);
+ color: rgb(255, 204, 0) !important;
}
- .material-design-icon.keep-unread {
+ .feed-item-row .button-container .material-design-icon.keep-unread {
color: var(--color-main-text);
}
@@ -234,7 +234,7 @@ export default Vue.extend({
color: #555555;
}
- .eye-check-icon {
+ .feed-item-row .button-container .eye-check-icon {
color: var(--color-primary-light);
}
</style>
diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue
index c5499c585..9a3f97bfb 100644
--- a/src/components/Sidebar.vue
+++ b/src/components/Sidebar.vue
@@ -36,17 +36,17 @@
<NcAppNavigationItem v-for="topLevelItem in topLevelNav"
:key="topLevelItem.name || topLevelItem.title"
:title="topLevelItem.name || topLevelItem.title"
- :icon="topLevelItem.name !== undefined ? 'icon-folder': ''"
+ :icon="isFolder(topLevelItem) ? 'icon-folder': ''"
+ :to="isFolder(topLevelItem) ? {} : { name: ROUTES.FEED, params: { feedId: topLevelItem.id.toString() } }"
:allow-collapse="true">
<template #default>
<NcAppNavigationItem v-for="feed in topLevelItem.feeds"
:key="feed.name"
- :title="feed.title">
+ :title="feed.title"
+ :to="{ name: ROUTES.FEED, props: { feedId: feed.id } }">
<template #icon>
- <img v-if="feed.faviconLink"
- :src="feed.faviconLink"
- alt="feedIcon">
- <div v-if="!feed.faviconLink" class="icon-rss" />
+ <RssIcon v-if="!feed.faviconLink" />
+ <span v-if="feed.faviconLink" style="width: 24px; background-size: contain;" :style="{ 'backgroundImage': 'url(' + feed.faviconLink + ')' }" />
</template>
<template #actions>
<NcActionButton icon="icon-checkmark"
@@ -100,8 +100,17 @@
</template>
</NcAppNavigationItem>
</template>
- <template v-if="topLevelItem.feedCount > 0" #counter>
- <NcCounterBubble>{{ topLevelItem.feedCount }}</NcCounterBubble>
+ <template #icon>
+ <RssIcon v-if="topLevelItem.feedCount === undefined && !topLevelItem.faviconLink" />
+ <span v-if="topLevelItem.feedCount === undefined && topLevelItem.faviconLink" style="height: 16px; width: 16px; background-size: contain;" :style="{ 'backgroundImage': 'url(' + topLevelItem.faviconLink + ')' }" />
+ </template>
+ <template #counter>
+ <NcCounterBubble v-if="topLevelItem.feedCount > 0">
+ {{ topLevelItem.feedCount }}
+ </NcCounterBubble>
+ <NcCounterBubble v-if="topLevelItem.unreadCount > 0">
+ {{ topLevelItem.unreadCount }}
+ </NcCounterBubble>
</template>
<template #actions>
<NcActionButton icon="icon-checkmark" @click="alert('TODO: Mark read')">
@@ -199,6 +208,9 @@ export default Vue.extend({
alert(msg: string) {
window.alert(msg)
},
+ isFolder(item: Feed | Folder) {
+ return (item as Folder).name !== undefined
+ },
},
})
diff --git a/src/components/Starred.vue b/src/components/Starred.vue
index 29e7d3bf3..3d5845484 100644
--- a/src/components/Starred.vue
+++ b/src/components/Starred.vue
@@ -7,7 +7,10 @@
</NcCounterBubble>
</div>
- <FeedItemDisplayList :items="starred" :fetch-key="'starred'" @load-more="fetchMore()" />
+ <FeedItemDisplayList :items="starred"
+ :fetch-key="'starred'"
+ :config="{ unreadFilter: true }"
+ @load-more="fetchMore()" />
</div>
</template>
@@ -47,26 +50,21 @@ export default Vue.extend({
})
</script>
-<style>
-.route-container {
- height: 100%;
-}
+<style scoped>
+ .route-container {
+ height: 100%;
+ }
-.header {
- padding-left: 50px;
- position: absolute;
- top: 1em;
- font-weight: 700;
-}
+ .header {
+ padding-left: 50px;
+ position: absolute;
+ top: 1em;
+ font-weight: 700;
+ }
-.counter-bubble {
- display: inline-block;
- vertical-align: sub;
- margin-left: 10px;
-}
-
-.virtual-scroll {
- margin-top: 50px;
- border-top: 1px solid var(--color-border);
-}
+ .counter-bubble {
+ display: inline-block;
+ vertical-align: sub;
+ margin-left: 10px;
+ }
</style>
diff --git a/src/components/Unread.vue b/src/components/Unread.vue
index 9a59f529b..d012ca43b 100644
--- a/src/components/Unread.vue
+++ b/src/components/Unread.vue
@@ -10,6 +10,7 @@
<FeedItemDisplayList v-if="unread()"
:items="unread()"
:fetch-key="'unread'"
+ :config="{ starFilter: true }"
@load-more="fetchMore()" />
</div>
</template>
@@ -74,22 +75,17 @@ export default Vue.extend({
})
</script>
-<style>
-.header {
- padding-left: 50px;
- position: absolute;
- top: 1em;
- font-weight: 700;
-}
-
-.counter-bubble {
- display: inline-block;
- vertical-align: sub;
- margin-left: 10px;
-}
+<style scoped>
+ .header {
+ padding-left: 50px;
+ position: absolute;
+ top: 1em;
+ font-weight: 700;
+ }
-.virtual-scroll {
- margin-top: 50px;
- border-top: 1px solid var(--color-border);
-}
+ .counter-bubble {
+ display: inline-block;
+ vertical-align: sub;
+ margin-left: 10px;
+ }
</style>
diff --git a/src/dataservices/item.service.ts b/src/dataservices/item.service.ts
index cddd1e978..6d4aabdb4 100644
--- a/src/dataservices/item.service.ts
+++ b/src/dataservices/item.service.ts
@@ -6,6 +6,7 @@ import { API_ROUTES } from '../types/ApiRoutes'
import { FeedItem } from '../types/FeedItem'
export const ITEM_TYPES = {
+ ALL: 0,
STARRED: 2,
UNREAD: 6,
}
@@ -14,6 +15,7 @@ export class ItemService {
static debounceFetchStarred = _.debounce(ItemService.fetchStarred, 400, { leading: true })
static debounceFetchUnread = _.debounce(ItemService.fetchUnread, 400, { leading: true })
+ static debounceFetchFeedItems = _.debounce(ItemService.fetchFeedItems, 400, { leading: true })
/**
* Makes backend call to retrieve starred items
@@ -54,6 +56,27 @@ export class ItemService {
}
/**
+ * Makes backend call to retrieve items from a specific feed
+ *
+ * @param feedId id number of feed to retrieve items for
+ * @param start (id of last unread item loaded)
+ * @return {AxiosResponse} response object containing backend request response
+ */
+ static async fetchFeedItems(feedId: number, start: number): Promise<AxiosResponse> {
+ return await axios.get(API_ROUTES.ITEMS, {
+ params: {
+ limit: 40,
+ oldestFirst: false,
+ search: '',
+ showAll: false,
+ type: ITEM_TYPES.ALL,
+ offset: start,
+ id: feedId,
+ },
+ })
+ }
+
+ /**
* Makes backend call to mark item as read/unread in DB
*
* @param {FeedItem} item FeedItem (containing id) that wil be marked as read/unread
diff --git a/src/routes/index.ts b/src/routes/index.ts
index b572742c8..0a67042b7 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -3,11 +3,13 @@ import VueRouter from 'vue-router'
import ExplorePanel from '../components/Explore.vue'
import StarredPanel from '../components/Starred.vue'
import UnreadPanel from '../components/Unread.vue'
+import FeedPanel from '../components/Feed.vue'
export const ROUTES = {
EXPLORE: 'explore',
STARRED: 'starred',
UNREAD: 'unread',
+ FEED: 'feeed',
}
const getInitialRoute = function() {
@@ -41,6 +43,12 @@ const routes = [
component: UnreadPanel,
props: true,
},
+ {
+ name: ROUTES.FEED,
+ path: '/feed/:feedId',
+ component: FeedPanel,
+ props: true,
+ },
]
export default new VueRouter({
diff --git a/src/store/item.ts b/src/store/item.ts
index 982e75979..f409305f1 100644
--- a/src/store/item.ts
+++ b/src/store/item.ts
@@ -11,6 +11,7 @@ export const FEED_ITEM_ACTION_TYPES = {
MARK_UNREAD: 'MARK_UNREAD',
STAR_ITEM: 'STAR_ITEM',
UNSTAR_ITEM: 'UNSTAR_ITEM',
+ FETCH_FEED_ITEMS: 'FETCH_FEED_ITEMS',
}
export type ItemState = {
@@ -77,6 +78,16 @@ export const actions = {
}
commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: false })
},
+ async [FEED_ITEM_ACTION_TYPES.FETCH_FEED_ITEMS]({ commit }: ActionParams, { feedId, start }: { feedId: number; start: number }) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: true })
+ const response = await ItemService.debounceFetchFeedItems(feedId, start)
+
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ITEMS, response?.data.items)
+ if (response?.data.items.length < 40) {
+ commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'feed-' + feedId, loaded: true })
+ }
+ commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'feed-' + feedId, fetching: false })
+ },
[FEED_ITEM_ACTION_TYPES.MARK_READ]({ commit }: ActionParams, { item }: { item: FeedItem}) {
ItemService.markRead(item, true)
diff --git a/src/types/FeedItem.ts b/src/types/FeedItem.ts
index dcd8c255a..879a663f4 100644
--- a/src/types/FeedItem.ts
+++ b/src/types/FeedItem.ts
@@ -5,4 +5,5 @@ export type FeedItem = {
starred: boolean;
feedId: number;
guidHash: string;
+ pubDate: number;
};