diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4ba5e1f6..86cdb0bcf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the ### Changed ### Fixed +- First features for user settings after vue migration (#2795) # Releases ## [25.0.0-alpha10] - 2024-10-14 diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index df52cdeb5b..6c307a7820 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -25,6 +25,7 @@ use OCA\News\Search\FolderSearchProvider; use OCA\News\Search\ItemSearchProvider; use OCA\News\Listeners\AddMissingIndicesListener; +use OCA\News\Listeners\UserSettingsListener; use OCA\News\Utility\Cache; use OCP\AppFramework\Bootstrap\IBootContext; @@ -36,6 +37,8 @@ use OCA\News\Fetcher\FeedFetcher; use OCA\News\Fetcher\Fetcher; use OCP\User\Events\BeforeUserDeletedEvent; +use OCP\Config\BeforePreferenceDeletedEvent; +use OCP\Config\BeforePreferenceSetEvent; use OCP\DB\Events\AddMissingIndicesEvent; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -91,6 +94,8 @@ public function register(IRegistrationContext $context): void $context->registerEventListener(BeforeUserDeletedEvent::class, UserDeleteHook::class); $context->registerEventListener(AddMissingIndicesEvent::class, AddMissingIndicesListener::class); + $context->registerEventListener(BeforePreferenceDeletedEvent::class, UserSettingsListener::class); + $context->registerEventListener(BeforePreferenceSetEvent::class, UserSettingsListener::class); // parameters $context->registerParameter('exploreDir', __DIR__ . '/../Explore/feeds'); diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 8af00664c9..9ec6065652 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -24,6 +24,7 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\AppFramework\Services\IInitialState; use OCA\News\Service\StatusService; use OCA\News\Explore\RecommendedSites; @@ -42,7 +43,8 @@ public function __construct( private IURLGenerator $urlGenerator, private IL10N $l10n, private RecommendedSites $recommendedSites, - private StatusService $statusService + private StatusService $statusService, + private IInitialState $initialState ) { parent::__construct($request, $userSession); } @@ -73,6 +75,23 @@ public function index(): TemplateResponse ] ); + $usersettings = [ + 'compact', + 'compactExpand', + 'preventReadOnScroll', + 'oldestFirst', + 'showAll' + ]; + + foreach ($usersettings as $setting) { + $this->initialState->provideInitialState($setting, $this->config->getUserValue( + $this->getUserId(), + $this->appName, + $setting, + '0' + )); + } + $csp = new ContentSecurityPolicy(); $csp->addAllowedImageDomain('*') ->addAllowedMediaDomain('*') diff --git a/lib/Listeners/UserSettingsListener.php b/lib/Listeners/UserSettingsListener.php new file mode 100644 index 0000000000..92d57871ab --- /dev/null +++ b/lib/Listeners/UserSettingsListener.php @@ -0,0 +1,31 @@ + */ +class UserSettingsListener implements IEventListener +{ + + public function handle(Event $event): void + { + if (!($event instanceof BeforePreferenceSetEvent || $event instanceof BeforePreferenceDeletedEvent)) { + return; + } + + if ($event->getAppId() !== 'news') { + return; + } + + $event->setValid(true); + } +} diff --git a/package.json b/package.json index da31b701f0..2c68245bc0 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@nextcloud/auth": "^2.4.0", "@nextcloud/axios": "^2.5.0", "@nextcloud/dialogs": "^4.2.7", + "@nextcloud/event-bus": "^3.3.1", "@nextcloud/initial-state": "^2.2.0", "@nextcloud/l10n": "^3.1.0", "@nextcloud/moment": "^1.3.1", diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index d425717f9a..b6fd72522a 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -34,7 +34,10 @@ - + @@ -111,6 +114,43 @@ + @@ -118,13 +158,19 @@ import { mapState } from 'vuex' import Vue from 'vue' +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' +import { showError, showSuccess } from '@nextcloud/dialogs' +import { subscribe } from '@nextcloud/event-bus' import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js' import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js' import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js' import NcAppNavigationNewItem from '@nextcloud/vue/dist/Components/NcAppNavigationNewItem.js' +import NcAppNavigationSettings from '@nextcloud/vue/dist/Components/NcAppNavigationSettings.js' import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import RssIcon from 'vue-material-design-icons/Rss.vue' import FolderIcon from 'vue-material-design-icons/Folder.vue' @@ -139,6 +185,7 @@ import { ACTIONS, AppState } from '../store' import AddFeed from './AddFeed.vue' import SidebarFeedLinkActions from './SidebarFeedLinkActions.vue' +import HelpModal from './modals/HelpModal.vue' import { Folder } from '../types/Folder' import { Feed } from '../types/Feed' @@ -168,8 +215,10 @@ export default Vue.extend({ NcAppNavigationNew, NcAppNavigationItem, NcAppNavigationNewItem, + NcAppNavigationSettings, NcCounterBubble, NcActionButton, + NcButton, AddFeed, RssIcon, FolderIcon, @@ -178,23 +227,106 @@ export default Vue.extend({ FolderPlusIcon, PlusIcon, SidebarFeedLinkActions, + HelpModal, }, data: () => { return { showAddFeed: false, ROUTES, + showHelp: false, } }, computed: { ...mapState(['feeds', 'folders', 'items']), ...mapState(SideBarState), + compact: { + get() { + return this.$store.getters.compact + }, + set(newValue) { + this.saveSetting('compact', newValue) + }, + }, + compactExpand: { + get() { + return this.$store.getters.compactExpand + }, + set(newValue) { + this.saveSetting('compactExpand', newValue) + }, + }, + oldestFirst: { + get() { + return this.$store.getters.oldestFirst + + }, + set(newValue) { + this.saveSetting('oldestFirst', newValue) + this.$store.dispatch(ACTIONS.RESET_ITEMS) + }, + }, + preventReadOnScroll: { + get() { + return this.$store.getters.preventReadOnScroll + + }, + set(newValue) { + this.saveSetting('preventReadOnScroll', newValue) + }, + }, + showAll: { + get() { + return this.$store.getters.showAll + + }, + set(newValue) { + this.saveSetting('showAll', newValue) + }, + }, }, created() { if (this.$route.query.subscribe_to) { this.showAddFeed = true } }, + mounted() { + subscribe('news:global:toggle-help-dialog', () => { + this.showHelp = !this.showHelp + }) + }, methods: { + async saveSetting(key, value) { + this.$store.commit(key, { value }) + const url = generateOcsUrl( + '/apps/provisioning_api/api/v1/config/users/{appId}/{key}', + { + appId: 'news', + key, + }, + ) + value = value ? '1' : '0' + try { + const { data } = await axios.post(url, { + configValue: value, + }) + this.handleResponse({ + status: data.ocs?.meta?.status, + }) + } catch (e) { + this.handleResponse({ + errorMessage: t('news', 'Unable to update news config'), + error: e, + }) + } + }, + handleResponse({ status, errorMessage, error }) { + if (status !== 'ok') { + showError(errorMessage) + console.error(errorMessage, error) + } else { + showSuccess(t('news', 'Successfully updated news configuration')) + } + }, newFolder(value: string) { const folderName = value.trim() const folder = { name: folderName } @@ -241,6 +373,7 @@ export default Vue.extend({ isFolder(item: Feed | Folder) { return (item as Folder).name !== undefined }, + }, }) diff --git a/src/components/feed-display/FeedItemDisplayList.vue b/src/components/feed-display/FeedItemDisplayList.vue index d2fd722f5e..26ff5a97f7 100644 --- a/src/components/feed-display/FeedItemDisplayList.vue +++ b/src/components/feed-display/FeedItemDisplayList.vue @@ -28,12 +28,24 @@ + + + +
{ return this.unreadFilter }, - // Always want to sort by date (most recent first) + // Always want to sort by id sort: (a: FeedItem, b: FeedItem) => { - if (a.pubDate > b.pubDate) { - return -1 + if (this.$store.getters.oldestFirst) { + return a.id < b.id ? -1 : 1 } else { - return 1 + return a.id > b.id ? -1 : 1 } }, cache: [] as FeedItem[] | undefined, @@ -189,8 +201,8 @@ export default Vue.extend({ fetchMore() { this.$emit('load-more') }, - noFilter(): boolean { - return true + noFilter(item: FeedItem): boolean { + return this.$store.getters.showAll ? true : item.unread }, starFilter(item: FeedItem): boolean { return item.starred diff --git a/src/components/modals/HelpModal.vue b/src/components/modals/HelpModal.vue new file mode 100644 index 0000000000..10d1aea1a3 --- /dev/null +++ b/src/components/modals/HelpModal.vue @@ -0,0 +1,230 @@ + + + + + + diff --git a/src/dataservices/item.service.ts b/src/dataservices/item.service.ts index 348cb936f8..866b34d516 100644 --- a/src/dataservices/item.service.ts +++ b/src/dataservices/item.service.ts @@ -1,6 +1,7 @@ import _ from 'lodash' import { AxiosResponse } from 'axios' import axios from '@nextcloud/axios' +import store from './../store/app' import { API_ROUTES } from '../types/ApiRoutes' import { FeedItem } from '../types/FeedItem' @@ -31,9 +32,9 @@ export class ItemService { return await axios.get(API_ROUTES.ITEMS, { params: { limit: 40, - oldestFirst: false, + oldestFirst: store.state.oldestFirst, search: '', - showAll: true, + showAll: store.state.showAll, type: ITEM_TYPES.ALL, offset: start, }, @@ -50,9 +51,9 @@ export class ItemService { return await axios.get(API_ROUTES.ITEMS, { params: { limit: 40, - oldestFirst: false, + oldestFirst: store.state.oldestFirst, search: '', - showAll: true, + showAll: store.state.showAll, type: ITEM_TYPES.STARRED, offset: start, }, @@ -69,9 +70,9 @@ export class ItemService { return await axios.get(API_ROUTES.ITEMS, { params: { limit: 40, - oldestFirst: false, + oldestFirst: store.state.oldestFirst, search: '', - showAll: false, + showAll: store.state.showAll, type: ITEM_TYPES.UNREAD, offset: start, }, @@ -89,9 +90,9 @@ export class ItemService { return await axios.get(API_ROUTES.ITEMS, { params: { limit: 40, - oldestFirst: false, + oldestFirst: store.state.oldestFirst, search: '', - showAll: true, + showAll: store.state.showAll, type: ITEM_TYPES.FEED, offset: start, id: feedId, @@ -110,9 +111,9 @@ export class ItemService { return await axios.get(API_ROUTES.ITEMS, { params: { limit: 40, - oldestFirst: false, + oldestFirst: store.state.oldestFirst, search: '', - showAll: true, + showAll: store.state.showAll, type: ITEM_TYPES.FOLDER, offset: start, id: folderId, diff --git a/src/store/app.ts b/src/store/app.ts index 2b470c9469..c394758bd2 100644 --- a/src/store/app.ts +++ b/src/store/app.ts @@ -1,3 +1,4 @@ +import { loadState } from '@nextcloud/initial-state' import { APPLICATION_MUTATION_TYPES } from '../types/MutationTypes' export const APPLICATION_ACTION_TYPES = { @@ -6,16 +7,41 @@ export const APPLICATION_ACTION_TYPES = { export type AppInfoState = { error?: Error; + compact: boolean; + compactExpand: boolean; + oldestFirst: boolean; + preventReadOnScroll: boolean; + showAll: boolean; } const state: AppInfoState = { error: undefined, + compact: loadState('news', 'compact', null) === '1', + compactExpand: loadState('news', 'compactExpand', null) === '1', + oldestFirst: loadState('news', 'oldestFirst', null) === '1', + preventReadOnScroll: loadState('news', 'preventReadOnScroll', null) === '1', + showAll: loadState('news', 'showAll', null) === '1', } const getters = { error(state: AppInfoState) { return state.error }, + compact() { + return state.compact + }, + compactExpand() { + return state.compactExpand + }, + oldestFirst() { + return state.oldestFirst + }, + preventReadOnScroll() { + return state.preventReadOnScroll + }, + showAll() { + return state.showAll + }, } export const actions = { @@ -31,6 +57,36 @@ export const mutations = { ) { state.error = error }, + compact( + state: AppInfoState, + { value }: { value: boolean }, + ) { + state.compact = value + }, + compactExpand( + state: AppInfoState, + { value }: { value: boolean }, + ) { + state.compactExpand = value + }, + oldestFirst( + state: AppInfoState, + { value }: { value: boolean }, + ) { + state.oldestFirst = value + }, + preventReadOnScroll( + state: AppInfoState, + { value }: { value: boolean }, + ) { + state.preventReadOnScroll = value + }, + showAll( + state: AppInfoState, + { value }: { value: boolean }, + ) { + state.showAll = value + }, } export default { diff --git a/src/store/feed.ts b/src/store/feed.ts index 72d3aba222..8b5ed0a17d 100644 --- a/src/store/feed.ts +++ b/src/store/feed.ts @@ -115,9 +115,9 @@ export const actions = { { commit }: ActionParams, { feed }: { feed: Feed }, ) { - // want to fetch feed so that we can retrieve the "highestItemId" + // want to fetch feed so that we can retrieve the "newestItemId" const response = await ItemService.fetchFeedItems(feed.id as number) - await FeedService.markRead({ feedId: feed.id as number, highestItemId: response.data.items[0].id }) + await FeedService.markRead({ feedId: feed.id as number, highestItemId: response.data.newestItemId }) if (feed.folderId) { commit(FOLDER_MUTATION_TYPES.MODIFY_FOLDER_UNREAD_COUNT, { folderId: feed.folderId, delta: -feed.unreadCount }) diff --git a/src/store/item.ts b/src/store/item.ts index 2aa22c59af..f63aa3eaa3 100644 --- a/src/store/item.ts +++ b/src/store/item.ts @@ -16,6 +16,7 @@ export const FEED_ITEM_ACTION_TYPES = { FETCH_FEED_ITEMS: 'FETCH_FEED_ITEMS', FETCH_FOLDER_FEED_ITEMS: 'FETCH_FOLDER_FEED_ITEMS', FETCH_ITEMS: 'FETCH_ITEMS', + RESET_ITEMS: 'RESET_ITEMS', } export type ItemState = { @@ -289,6 +290,25 @@ export const actions = { commit(FEED_ITEM_MUTATION_TYPES.UPDATE_ITEM, { item }) commit(FEED_ITEM_MUTATION_TYPES.SET_STARRED_COUNT, state.starredCount - 1) }, + + /** + * Remove all loaded items from memory and reset ItemsLoaded counters + * + * @param param0 ActionParams + * @param param0.dispatch Dispatch + * @param param1 ActionArgs + * @param param1.start Start data + */ + [FEED_ITEM_ACTION_TYPES.RESET_ITEMS]( + { dispatch }: ActionParams, + { start }: { start: number } = { start: 0 }, + ) { + state.allItems.splice(start, state.allItems.length - start) + state.allItemsLoaded = {} + state.lastItemLoaded = {} + dispatch(FEED_ITEM_ACTION_TYPES.FETCH_STARRED, 0) + dispatch(FEED_ITEM_ACTION_TYPES.FETCH_ITEMS, 0) + }, } export const mutations = { diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php index 7f0b144a06..af0399fa45 100644 --- a/tests/Unit/Controller/PageControllerTest.php +++ b/tests/Unit/Controller/PageControllerTest.php @@ -26,6 +26,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\AppFramework\Services\IInitialState; use PHPUnit\Framework\TestCase; class PageControllerTest extends TestCase @@ -86,6 +87,11 @@ class PageControllerTest extends TestCase */ private $userSession; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|IInitialState + */ + private $initialState; + /** * Gets run before each test */ @@ -131,6 +137,9 @@ public function setUp(): void $this->userSession->expects($this->any()) ->method('getUser') ->will($this->returnValue($this->user)); + $this->initialState = $this->getMockBuilder(IInitialState::class) + ->disableOriginalConstructor() + ->getMock(); $this->controller = new PageController( $this->request, $this->userSession, @@ -139,7 +148,8 @@ public function setUp(): void $this->urlGenerator, $this->l10n, $this->recommended, - $this->status + $this->status, + $this->initialState ); }