Skip to content

Add feature to Group starred Items per Feed #3148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f0e05b4
Add feature to Group starred Items per Feed
Juri-w Apr 20, 2025
66b6c43
Fix lint errors
Juri-w Apr 21, 2025
c302823
Remove storing route
Juri-w Apr 21, 2025
b7077e5
change to count prop
Juri-w Apr 21, 2025
38ee0b0
Add right count on top
Juri-w Apr 21, 2025
7e5aa0b
Add Changelog
Juri-w Apr 21, 2025
5a6bf75
correct to existing val
Juri-w Apr 21, 2025
b09b1b1
solve scrollbar not resetting
Juri-w Apr 21, 2025
6e640f2
correct back to lf
Juri-w Apr 27, 2025
524dd19
add items fetcher per starred feed
Juri-w Apr 27, 2025
d81b459
add backend get by feed id filter
Juri-w Apr 27, 2025
cce5858
add starredcount by feed getter
Juri-w Apr 27, 2025
ef6768e
change to filter where starred items not 0
Juri-w Apr 28, 2025
7cd8ad5
fix linters
Juri-w Apr 28, 2025
0c09771
move to unreleased
Juri-w May 3, 2025
721c9f6
chnage to feedId
Juri-w May 3, 2025
3f7f5a0
change parm order
Juri-w May 3, 2025
d032c03
remove template
Juri-w May 3, 2025
2dbb27f
remove not existing initial route
Juri-w May 3, 2025
d71569b
fix refetch
Juri-w May 5, 2025
c0fef5b
fix exact, change route
Juri-w May 5, 2025
fd2af99
fix lint
Juri-w May 5, 2025
fd83148
remove unnessesary create
Juri-w May 5, 2025
199c972
fix test
Juri-w May 5, 2025
a047aaf
remove mutations lint
Juri-w May 5, 2025
f75bdd4
update feed starred count
Juri-w May 5, 2025
4cac8d8
Merge branch 'master' into master
Juri-w May 5, 2025
1781bd4
fix update starred count linter
Juri-w May 5, 2025
1dca09b
Merge branch 'master' of https://github.com/Juri-w/news
Juri-w May 5, 2025
2d9a77b
fix findAllItems Tests
Juri-w May 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the
# Unreleased
## [26.x.x]
### Changed

- Add feature to Group starred Items per Feed

### Fixed

Expand Down
3 changes: 2 additions & 1 deletion lib/Controller/ItemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ public function index(
$limit,
$offset,
$oldestFirst,
$search_items
$search_items,
$id
);
break;
}
Expand Down
16 changes: 16 additions & 0 deletions lib/Db/Feed.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class Feed extends Entity implements IAPI, \JsonSerializable
protected $folderId;
/** @var int */
protected $unreadCount;
/** @var int */
protected $starredCount;
/** @var string|null */
protected $link = null;
/** @var bool */
Expand Down Expand Up @@ -322,6 +324,7 @@ public function jsonSerialize(): array
'added',
'folderId',
'unreadCount',
'starredCount',
'link',
'preventUpdate',
'deletedAt',
Expand Down Expand Up @@ -594,6 +597,19 @@ public function setUnreadCount(int $unreadCount): Feed
return $this;
}

/**
* @param int $starredCount
*/
public function setStarredCount(int $starredCount): Feed
{
if ($this->starredCount !== $starredCount) {
$this->starredCount = $starredCount;
$this->markFieldUpdated('starredCount');
}

return $this;
}

/**
* @param int $updateErrorCount
*/
Expand Down
17 changes: 14 additions & 3 deletions lib/Db/FeedMapperV2.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,24 @@ public function __construct(IDBConnection $db, Time $time)
public function findAllFromUser(string $userId, array $params = []): array
{
$builder = $this->db->getQueryBuilder();
$builder->select('feeds.*', $builder->func()->count('items.id', 'unreadCount'))
$builder
->select(
'feeds.*',
$builder->func()->count('items_unread.id', 'unreadCount'),
$builder->func()->count('items_starred.id', 'starredCount')
)
->from(static::TABLE_NAME, 'feeds')
->leftJoin(
'feeds',
ItemMapperV2::TABLE_NAME,
'items',
'items.feed_id = feeds.id AND items.unread = :unread'
'items_unread',
'items_unread.feed_id = feeds.id AND items_unread.unread = :unread'
)
->leftJoin(
'feeds',
ItemMapperV2::TABLE_NAME,
'items_starred',
'items_starred.feed_id = feeds.id AND items_starred.starred = 1'
)
->where('feeds.user_id = :user_id')
->andWhere('feeds.deleted_at = 0')
Expand Down
5 changes: 5 additions & 0 deletions lib/Db/ItemMapperV2.php
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ public function findAllFolder(
* @throws ServiceValidationException
*/
public function findAllItems(
int $feedId,
string $userId,
int $type,
int $limit,
Expand Down Expand Up @@ -588,6 +589,10 @@ public function findAllItems(
case ListType::STARRED:
$builder->andWhere('items.starred = :starred')
->setParameter('starred', true);
if ($feedId !== 0) {
$builder->andWhere('items.feed_id = :feedId')
->setParameter('feedId', $feedId);
}
break;
case ListType::UNREAD:
$builder->andWhere('items.unread = :unread')
Expand Down
5 changes: 3 additions & 2 deletions lib/Service/ItemServiceV2.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,9 @@ public function findAllWithFilters(
int $limit,
int $offset,
bool $oldestFirst,
array $search = []
array $search = [],
int $id = 0
): array {
return $this->mapper->findAllItems($userId, $type, $limit, $offset, $oldestFirst, $search);
return $this->mapper->findAllItems($id, $userId, $type, $limit, $offset, $oldestFirst, $search);
}
}
25 changes: 24 additions & 1 deletion src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,27 @@
<RssIcon />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem :name="t('news', 'Starred')" icon="icon-starred" :to="{ name: ROUTES.STARRED }">
<NcAppNavigationItem :name="t('news', 'Starred')"
icon="icon-starred"
:to="{ name: ROUTES.STARRED }"
:allow-collapse="true"
:exact="true"
:force-menu="true">
<NcAppNavigationItem v-for="group in GroupedStars"
:key="group.id"
:ref="'starredfeed-' + group.id"
:name="group.title"
:icon="''"
:exact="true"
:to="{ name: ROUTES.STARREDFEED, params: { feedId: group.id.toString() } }">
<template #icon>
<RssIcon v-if="!group.faviconLink" />
<span v-if="group.faviconLink" style="width: 16px; height: 16px; background-size: contain;" :style="{ 'backgroundImage': 'url(' + group.faviconLink + ')' }" />
</template>
<template #counter>
<NcCounterBubble :count="group.starredCount" />
</template>
</NcAppNavigationItem>
<template #counter>
<NcCounterBubble :count="items.starredCount" />
</template>
Expand Down Expand Up @@ -371,6 +391,9 @@ export default Vue.extend({

return navItems
},
GroupedStars(): Array<Feed> {
return this.$store.getters.feeds.filter((item: Feed) => item.starredCount !== 0)
},
loading: {
get() {
return this.$store.getters.loading
Expand Down
21 changes: 14 additions & 7 deletions src/components/routes/Starred.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<template>
<ContentTemplate :items="starred"
:fetch-key="'starred'"
:fetch-key="feedId ? 'starredfeed-'+feedId : 'starred'"
@load-more="fetchMore()">
<template #header>
{{ t('news', 'Starred') }}
<NcCounterBubble class="counter-bubble" :count="items.starredCount" />
<NcCounterBubble class="counter-bubble" :count="feedId ? starred.length : items.starredCount" />
</template>
</ContentTemplate>
</template>
Expand All @@ -18,26 +18,33 @@ import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
import ContentTemplate from '../ContentTemplate.vue'

import { FeedItem } from '../../types/FeedItem'
import { ACTIONS, MUTATIONS } from '../../store'
import { ACTIONS } from '../../store'

export default Vue.extend({
components: {
ContentTemplate,
NcCounterBubble,
},
props: {
feedId: {
type: String,
required: false,
default: undefined,
},
},
computed: {
...mapState(['items']),
starred(): FeedItem[] {
if (this.feedId) {
return this.$store.getters.starred.filter((item: FeedItem) => item.feedId === Number(this.feedId))
}
return this.$store.getters.starred
},
},
created() {
this.$store.commit(MUTATIONS.SET_SELECTED_ITEM, { id: undefined })
},
methods: {
async fetchMore() {
if (!this.$store.state.items.fetchingItems.starred) {
this.$store.dispatch(ACTIONS.FETCH_STARRED)
this.$store.dispatch(ACTIONS.FETCH_STARRED, { feedId: this.feedId === undefined ? 0 : Number(this.feedId) })
}
},
},
Expand Down
4 changes: 3 additions & 1 deletion src/dataservices/item.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ export class ItemService {
/**
* Makes backend call to retrieve starred items
*
* @param feedId (id of the feed to retrieve starred items for)
* @param start (id of last starred item loaded)
* @return {AxiosResponse} response object containing backend request response
*/
static async fetchStarred(start: number): Promise<AxiosResponse> {
static async fetchStarred(feedId: number, start: number): Promise<AxiosResponse> {
return await axios.get(API_ROUTES.ITEMS, {
params: {
limit: 40,
Expand All @@ -57,6 +58,7 @@ export class ItemService {
showAll: store.state.showAll,
type: ITEM_TYPES.STARRED,
offset: start,
...(feedId !== 0 ? { id: feedId } : {}),
},
})
}
Expand Down
23 changes: 22 additions & 1 deletion src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import store from './../store/app'
export const ROUTES = {
EXPLORE: 'explore',
STARRED: 'starred',
STARREDFEED: 'starredfeed',
UNREAD: 'unread',
FEED: 'feed',
FOLDER: 'folder',
Expand All @@ -35,7 +36,16 @@ const getInitialRoute = function() {
params,
}
case '2':
return { name: ROUTES.STARRED }
if (store.state.lastViewedFeedId) {
params.feedId = store.state.lastViewedFeedId
return {
name: ROUTES.STARREDFEED,
params,
}
}
return {
name: ROUTES.STARRED,
}
case '3':
return { name: ROUTES.ALL }
case '5':
Expand Down Expand Up @@ -63,7 +73,18 @@ const routes = [
name: ROUTES.STARRED,
path: '/starred',
component: StarredPanel,
redirect: { name: 'starredItems' },
props: true,
children: [
{
name: 'starredItems',
path: '',
},
{
name: ROUTES.STARREDFEED,
path: ':feedId',
},
],
},
{
name: ROUTES.UNREAD,
Expand Down
12 changes: 12 additions & 0 deletions src/store/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ export const mutations = {
}
},

[FEED_MUTATION_TYPES.MODIFY_STARRED_COUNT](
state: FeedState,
{ feedId, add }: { feedId: number, add: boolean },
) {
const feed = state.feeds.find((feed: Feed) => {
return feed.id === feedId
})
if (feed) {
_.assign(feed, { starredCount: feed.starredCount + (add ? 1 : -1) })
}
},

[FEED_MUTATION_TYPES.FEED_DELETE](
state: FeedState,
feedId: number,
Expand Down
24 changes: 17 additions & 7 deletions src/store/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,20 @@ export const actions = {
* @param param0 ActionParams
* @param param0.commit Commit param
* @param param1 ActionArgs
* @param param1.feedId ID of the feed
* @param param1.start Start data
*/
async [FEED_ITEM_ACTION_TYPES.FETCH_STARRED](
{ commit }: ActionParams<ItemState>,
{ start }: { start: number } = { start: 0 },
{ feedId, start }: { feedId: number, start: number } = { feedId: 0, start: 0 },
) {
commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: true })
const response = await ItemService.debounceFetchStarred(start || state.lastItemLoaded.starred)
commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: feedId ? 'starredfeed-' + feedId : 'starred', fetching: true })

const lastLoadedStarred = feedId
? state.lastItemLoaded['starredfeed-' + feedId]
: state.lastItemLoaded.starred
const offset = start || lastLoadedStarred
const response = await ItemService.debounceFetchStarred(feedId, offset)
if (response?.data.newestItemId && response?.data.newestItemId !== state.newestItemId) {
state.syncNeeded = true
}
Expand All @@ -162,15 +168,15 @@ export const actions = {
}

if (response?.data.items.length < 40) {
commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'starred', loaded: true })
commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: feedId ? 'starredfeed-' + feedId : 'starred', loaded: true })
} else {
commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: 'starred', loaded: false })
commit(FEED_ITEM_MUTATION_TYPES.SET_ALL_LOADED, { key: feedId ? 'starredfeed-' + feedId : 'starred', loaded: false })
}
if (response?.data.items.length > 0) {
const lastItem = response?.data.items[response?.data.items.length - 1].id
commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: 'starred', lastItem })
commit(FEED_ITEM_MUTATION_TYPES.SET_LAST_ITEM_LOADED, { key: feedId ? 'starredfeed-' + feedId : 'starred', lastItem })
}
commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: 'starred', fetching: false })
commit(FEED_ITEM_MUTATION_TYPES.SET_FETCHING, { key: feedId ? 'starredfeed-' + feedId : 'starred', fetching: false })
},

/**
Expand Down Expand Up @@ -300,8 +306,10 @@ export const actions = {
ItemService.markStarred(item, true)

item.starred = true
const feedId = item.feedId
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount + 1)
commit(FEED_MUTATION_TYPES.MODIFY_STARRED_COUNT, { feedId, add: true })
},

/**
Expand All @@ -319,8 +327,10 @@ export const actions = {
ItemService.markStarred(item, false)

item.starred = false
const feedId = item.feedId
commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item })
commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount - 1)
commit(FEED_MUTATION_TYPES.MODIFY_STARRED_COUNT, { feedId, add: false })
},

/**
Expand Down
1 change: 1 addition & 0 deletions src/types/Feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FEED_ORDER, FEED_UPDATE_MODE } from '../dataservices/feed.service'
export type Feed = {
folderId?: number;
unreadCount: number;
starredCount: number;
url: string;
title?: string;
autoDiscover?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/types/MutationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const FEED_MUTATION_TYPES = {
SET_NEWEST_ITEM_ID: 'SET_NEWEST_ITEM_ID',
SET_FEED_ALL_READ: 'SET_FEED_ALL_READ',
MODIFY_FEED_UNREAD_COUNT: 'MODIFY_FEED_UNREAD_COUNT',
MODIFY_STARRED_COUNT: 'MODIFY_STARRED_COUNT',

FEED_DELETE: 'FEED_DELETE',
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Db/ItemMapperPaginatedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public function testFindAllItemsFullInverted()
false
);

$result = $this->class->findAllItems('jack', 3, 10, 10, true, []);
$result = $this->class->findAllItems(0, 'jack', 3, 10, 10, true, []);
$this->assertEquals([Item::fromRow(['id' => 4])], $result);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/javascript/unit/services/item.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('item.service.ts', () => {
it('should call GET with offset set to start param and STARRED item type', async () => {
(axios as any).get.mockResolvedValue({ data: { feeds: [] } })

await ItemService.fetchStarred(0)
await ItemService.fetchStarred(0, 0)

expect(axios.get).toBeCalled()
const queryParams = (axios.get as any).mock.calls[0][1].params
Expand Down