From 8097b4e1a8404b2aee68f260bf5bbef78f3dfb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Mar 2021 13:18:17 +0100 Subject: [PATCH] MOBILE-3627 blog: Add blog functionality --- src/addons/addons.module.ts | 2 + src/addons/blog/blog-lazy.module.ts | 52 ++++ src/addons/blog/blog.module.ts | 65 ++++ src/addons/blog/lang.json | 12 + src/addons/blog/pages/entries/entries.html | 82 +++++ src/addons/blog/pages/entries/entries.ts | 288 ++++++++++++++++++ src/addons/blog/services/blog.ts | 204 +++++++++++++ .../blog/services/handlers/course-option.ts | 109 +++++++ .../blog/services/handlers/index-link.ts | 61 ++++ src/addons/blog/services/handlers/mainmenu.ts | 51 ++++ src/addons/blog/services/handlers/tag-area.ts | 53 ++++ src/addons/blog/services/handlers/user.ts | 64 ++++ .../calendar/services/handlers/mainmenu.ts | 1 - .../mod/forum/components/index/index.html | 2 +- .../infinite-loading/infinite-loading.ts | 4 + .../course/classes/main-resource-component.ts | 11 +- 16 files changed, 1056 insertions(+), 5 deletions(-) create mode 100644 src/addons/blog/blog-lazy.module.ts create mode 100644 src/addons/blog/blog.module.ts create mode 100644 src/addons/blog/lang.json create mode 100644 src/addons/blog/pages/entries/entries.html create mode 100644 src/addons/blog/pages/entries/entries.ts create mode 100644 src/addons/blog/services/blog.ts create mode 100644 src/addons/blog/services/handlers/course-option.ts create mode 100644 src/addons/blog/services/handlers/index-link.ts create mode 100644 src/addons/blog/services/handlers/mainmenu.ts create mode 100644 src/addons/blog/services/handlers/tag-area.ts create mode 100644 src/addons/blog/services/handlers/user.ts diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index 408a1d3a6..6bf6d8aa5 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -26,11 +26,13 @@ import { AddonMessagesModule } from './messages/messages.module'; import { AddonModModule } from './mod/mod.module'; import { AddonQbehaviourModule } from './qbehaviour/qbehaviour.module'; import { AddonQtypeModule } from './qtype/qtype.module'; +import { AddonBlogModule } from './blog/blog.module'; @NgModule({ imports: [ AddonBlockModule, AddonBadgesModule, + AddonBlogModule, AddonCalendarModule, AddonMessagesModule, AddonPrivateFilesModule, diff --git a/src/addons/blog/blog-lazy.module.ts b/src/addons/blog/blog-lazy.module.ts new file mode 100644 index 000000000..ff61fe333 --- /dev/null +++ b/src/addons/blog/blog-lazy.module.ts @@ -0,0 +1,52 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injector, NgModule } from '@angular/core'; +import { RouterModule, ROUTES, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonBlogEntriesPage } from './pages/entries/entries'; +import { CoreCommentsComponentsModule } from '@features/comments/components/components.module'; + +import { CoreTagComponentsModule } from '@features/tag/components/components.module'; +import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; + +function buildRoutes(injector: Injector): Routes { + return [ + ...buildTabMainRoutes(injector, { + component: AddonBlogEntriesPage, + }), + ]; +} + +@NgModule({ + imports: [ + CoreSharedModule, + CoreCommentsComponentsModule, + CoreTagComponentsModule, + ], + exports: [RouterModule], + providers: [ + { + provide: ROUTES, + multi: true, + deps: [Injector], + useFactory: buildRoutes, + }, + ], + declarations: [ + AddonBlogEntriesPage, + ], +}) +export class AddonBlogLazyModule {} diff --git a/src/addons/blog/blog.module.ts b/src/addons/blog/blog.module.ts new file mode 100644 index 000000000..c0fc7aa94 --- /dev/null +++ b/src/addons/blog/blog.module.ts @@ -0,0 +1,65 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseIndexRoutingModule } from '@features/course/pages/index/index-routing.module'; +import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; +import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; +import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate'; +import { CoreUserDelegate } from '@features/user/services/user-delegate'; +import { AddonBlogProvider } from './services/blog'; +import { AddonBlogCourseOptionHandler } from './services/handlers/course-option'; +import { AddonBlogIndexLinkHandler } from './services/handlers/index-link'; +import { AddonBlogMainMenuHandler, AddonBlogMainMenuHandlerService } from './services/handlers/mainmenu'; +import { AddonBlogTagAreaHandler } from './services/handlers/tag-area'; +import { AddonBlogUserHandler } from './services/handlers/user'; + +export const ADDON_BLOG_SERVICES: Type[] = [ + AddonBlogProvider, +]; + +const routes: Routes = [ + { + path: AddonBlogMainMenuHandlerService.PAGE_NAME, + loadChildren: () => import('@addons/blog/blog-lazy.module').then(m => m.AddonBlogLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + CoreMainMenuRoutingModule.forChild({ children: routes }), + CoreCourseIndexRoutingModule.forChild({ children: routes }), + ], + exports: [CoreMainMenuRoutingModule], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => async () => { + CoreContentLinksDelegate.registerHandler(AddonBlogIndexLinkHandler.instance); + CoreMainMenuDelegate.registerHandler(AddonBlogMainMenuHandler.instance); + CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance); + CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance); + CoreCourseOptionsDelegate.registerHandler(AddonBlogCourseOptionHandler.instance); + }, + }, + ], +}) +export class AddonBlogModule {} diff --git a/src/addons/blog/lang.json b/src/addons/blog/lang.json new file mode 100644 index 000000000..6e183232f --- /dev/null +++ b/src/addons/blog/lang.json @@ -0,0 +1,12 @@ +{ + "blog": "Blog", + "blogentries": "Blog entries", + "errorloadentries": "Error loading blog entries.", + "linktooriginalentry": "Link to original blog entry", + "noentriesyet": "No visible entries here", + "publishtonoone": "Yourself (draft)", + "publishtosite": "Anyone on this site", + "publishtoworld": "Anyone in the world", + "showonlyyourentries": "Show only your entries", + "siteblogheading": "Site blog" +} \ No newline at end of file diff --git a/src/addons/blog/pages/entries/entries.html b/src/addons/blog/pages/entries/entries.html new file mode 100644 index 000000000..5d6c68e37 --- /dev/null +++ b/src/addons/blog/pages/entries/entries.html @@ -0,0 +1,82 @@ + + + + + + {{ title | translate }} + + + + + + + + + + {{ 'addon.blog.showonlyyourentries' | translate }} + + + + + + + + + +

+ + + + {{ 'addon.blog.' + entry.publishTranslated! | translate}} + +

+

+ + {{entry.created | coreDateDayOrTime}} + + {{entry.user && entry.user!.fullname}} +

+
+
+ + + + + + + + + +
{{ 'core.tag.tags' | translate }}:
+ +
+
+ + + + + + + + + + {{ 'addon.blog.linktooriginalentry' | translate }} + +
+ + + + {{entry.lastmodified | coreTimeAgo}} + + + +
+
+ +
+
diff --git a/src/addons/blog/pages/entries/entries.ts b/src/addons/blog/pages/entries/entries.ts new file mode 100644 index 000000000..39a63ae49 --- /dev/null +++ b/src/addons/blog/pages/entries/entries.ts @@ -0,0 +1,288 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ContextLevel } from '@/core/constants'; +import { AddonBlog, AddonBlogFilter, AddonBlogPost, AddonBlogProvider } from '@addons/blog/services/blog'; +import { Component, OnInit } from '@angular/core'; +import { CoreComments } from '@features/comments/services/comments'; +import { CoreTag } from '@features/tag/services/tag'; +import { CoreUser, CoreUserProfile } from '@features/user/services/user'; +import { IonRefresher } from '@ionic/angular'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; + +/** + * Page that displays the list of blog entries. + */ +@Component({ + selector: 'page-addon-blog-entries', + templateUrl: 'entries.html', +}) +export class AddonBlogEntriesPage implements OnInit { + + title = ''; + + protected filter: AddonBlogFilter = {}; + protected pageLoaded = 0; + protected userPageLoaded = 0; + protected canLoadMoreEntries = false; + protected canLoadMoreUserEntries = true; + protected siteHomeId: number; + + loaded = false; + canLoadMore = false; + loadMoreError = false; + entries: AddonBlogPostFormatted[] = []; + currentUserId: number; + showMyEntriesToggle = false; + onlyMyEntries = false; + component = AddonBlogProvider.COMPONENT; + commentsEnabled = false; + tagsEnabled = false; + contextLevel: ContextLevel = ContextLevel.SYSTEM; + contextInstanceId = 0; + + constructor() { + this.currentUserId = CoreSites.getCurrentSiteUserId(); + this.siteHomeId = CoreSites.getCurrentSiteHomeId(); + } + + /** + * View loaded. + */ + async ngOnInit(): Promise { + const userId = CoreNavigator.getRouteNumberParam('userId'); + const courseId = CoreNavigator.getRouteNumberParam('courseId'); + const cmId = CoreNavigator.getRouteNumberParam('cmId'); + const entryId = CoreNavigator.getRouteNumberParam('entryId'); + const groupId = CoreNavigator.getRouteNumberParam('groupId'); + const tagId = CoreNavigator.getRouteNumberParam('tagId'); + + if (!userId && !courseId && !cmId && !entryId && !groupId && !tagId) { + this.title = 'addon.blog.siteblogheading'; + } else { + this.title = 'addon.blog.blogentries'; + } + + if (userId) { + this.filter.userid = userId; + } + this.showMyEntriesToggle = !userId; + + if (courseId) { + this.filter.courseid = courseId; + } + + if (cmId) { + this.filter.cmid = cmId; + } + + if (entryId) { + this.filter.entryid = entryId; + } + + if (groupId) { + this.filter.groupid = groupId; + } + + if (tagId) { + this.filter.tagid = tagId; + } + + // Calculate the context level. + if (userId && !courseId && !cmId) { + this.contextLevel = ContextLevel.USER; + this.contextInstanceId = userId; + } else if (courseId && courseId != this.siteHomeId) { + this.contextLevel = ContextLevel.COURSE; + this.contextInstanceId = courseId; + } else { + this.contextLevel = ContextLevel.SYSTEM; + this.contextInstanceId = 0; + } + + this.commentsEnabled = !CoreComments.areCommentsDisabledInSite(); + this.tagsEnabled = CoreTag.areTagsAvailableInSite(); + + await this.fetchEntries(); + + CoreUtils.ignoreErrors(AddonBlog.logView(this.filter)); + } + + /** + * Fetch blog entries. + * + * @param refresh Empty events array first. + * @return Promise with the entries. + */ + protected async fetchEntries(refresh: boolean = false): Promise { + this.loadMoreError = false; + + if (refresh) { + this.pageLoaded = 0; + this.userPageLoaded = 0; + } + + const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded; + + try { + const result = await AddonBlog.getEntries(this.filter, loadPage); + + const promises = result.entries.map(async (entry: AddonBlogPostFormatted) => { + switch (entry.publishstate) { + case 'draft': + entry.publishTranslated = 'publishtonoone'; + break; + case 'site': + entry.publishTranslated = 'publishtosite'; + break; + case 'public': + entry.publishTranslated = 'publishtoworld'; + break; + default: + entry.publishTranslated = 'privacy:unknown'; + break; + } + + // Calculate the context. This code was inspired by calendar events, Moodle doesn't do this for blogs. + if (entry.moduleid || entry.coursemoduleid) { + entry.contextLevel = ContextLevel.MODULE; + entry.contextInstanceId = entry.moduleid || entry.coursemoduleid; + } else if (entry.courseid) { + entry.contextLevel = ContextLevel.COURSE; + entry.contextInstanceId = entry.courseid; + } else { + entry.contextLevel = ContextLevel.USER; + entry.contextInstanceId = entry.userid; + } + + entry.summary = CoreTextUtils.instance.replacePluginfileUrls(entry.summary, entry.summaryfiles || []); + + return CoreUser.getProfile(entry.userid, entry.courseid, true).then((user) => { + entry.user = user; + + return; + }).catch(() => { + // Ignore errors. + }); + }); + + if (refresh) { + this.entries = result.entries; + } else { + this.entries = CoreUtils.uniqueArray(this.entries + .concat(result.entries), 'id') + .sort((a, b) => b.created - a.created); + } + + if (this.onlyMyEntries) { + const count = this.entries.filter((entry) => entry.userid == this.currentUserId).length; + this.canLoadMoreUserEntries = result.totalentries > count; + this.canLoadMore = this.canLoadMoreUserEntries; + this.userPageLoaded++; + } else { + this.canLoadMoreEntries = result.totalentries > this.entries.length; + this.canLoadMore = this.canLoadMoreEntries; + this.pageLoaded++; + } + + await Promise.all(promises); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true); + this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. + } finally { + this.loaded = true; + } + } + + /** + * Toggle between showing only my entries or not. + * + * @param enabled If true, filter my entries. False otherwise. + */ + onlyMyEntriesToggleChanged(enabled: boolean): void { + this.canLoadMore = enabled ? this.canLoadMoreUserEntries : this.canLoadMoreEntries; + + if (!enabled) { + delete this.filter.userid; + + return; + } + + const count = this.entries.filter((entry) => entry.userid == this.currentUserId).length; + this.userPageLoaded = Math.floor(count / AddonBlogProvider.ENTRIES_PER_PAGE); + this.filter.userid = this.currentUserId; + + if (count == 0 && this.canLoadMoreUserEntries) { + // First time but no entry loaded. Try to load some. + this.loadMore(); + } + } + + /** + * Function to load more entries. + * + * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. + * @return Resolved when done. + */ + loadMore(infiniteComplete?: () => void): Promise { + return this.fetchEntries().finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + + /** + * Refresh blog entries on PTR. + * + * @param refresher Refresher instance. + */ + refresh(refresher?: CustomEvent): void { + const promises = this.entries.map((entry) => + CoreComments.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog')); + + promises.push(AddonBlog.invalidateEntries(this.filter)); + + if (this.showMyEntriesToggle) { + this.filter['userid'] = this.currentUserId; + promises.push(AddonBlog.invalidateEntries(this.filter)); + + if (!this.onlyMyEntries) { + delete this.filter['userid']; + } + + } + + CoreUtils.allPromises(promises).finally(() => { + this.fetchEntries(true).finally(() => { + if (refresher) { + refresher?.detail.complete(); + } + }); + }); + } + +} + +/** + * Blog post with some calculated data. + */ +type AddonBlogPostFormatted = AddonBlogPost & { + publishTranslated?: string; // Calculated in the app. Key of the string to translate the publish state of the post. + user?: CoreUserProfile; // Calculated in the app. Data of the user that wrote the post. + contextLevel?: string; // Calculated in the app. The context level of the entry. + contextInstanceId?: number; // Calculated in the app. The context instance id. +}; diff --git a/src/addons/blog/services/blog.ts b/src/addons/blog/services/blog.ts new file mode 100644 index 000000000..0b9e77ed8 --- /dev/null +++ b/src/addons/blog/services/blog.ts @@ -0,0 +1,204 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; +import { CoreTagItem } from '@features/tag/services/tag'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'addonBlog:'; + +/** + * Service to handle blog entries. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogProvider { + + static readonly ENTRIES_PER_PAGE = 10; + static readonly COMPONENT = 'blog'; + + /** + * Returns whether or not the blog plugin is enabled for a certain site. + * + * This method is called quite often and thus should only perform a quick + * check, we should not be calling WS from here. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if enabled, resolved with false or rejected otherwise. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('core_blog_get_entries') &&site.canUseAdvancedFeature('enableblogs'); + } + + /** + * Get the cache key for the blog entries. + * + * @param filter Filter to apply on search. + * @return Cache key. + */ + getEntriesCacheKey(filter: AddonBlogFilter = {}): string { + return ROOT_CACHE_KEY + CoreUtils.sortAndStringify(filter); + } + + /** + * Get blog entries. + * + * @param filter Filter to apply on search. + * @param page Page of the blog entries to fetch. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when the entries are retrieved. + */ + async getEntries(filter: AddonBlogFilter = {}, page: number = 0, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const data: CoreBlogGetEntriesWSParams = { + filters: CoreUtils.objectToArrayOfObjects(filter, 'name', 'value'), + page: page, + perpage: AddonBlogProvider.ENTRIES_PER_PAGE, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getEntriesCacheKey(filter), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + return site.read('core_blog_get_entries', data, preSets); + } + + /** + * Invalidate blog entries WS call. + * + * @param filter Filter to apply on search + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when data is invalidated. + */ + async invalidateEntries(filter: AddonBlogFilter = {}, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter)); + } + + /** + * Trigger the blog_entries_viewed event. + * + * @param filter Filter to apply on search. + * @param siteId Site ID. If not defined, current site. + * @return Promise to be resolved when done. + */ + async logView(filter: AddonBlogFilter = {}, siteId?: string): Promise { + CorePushNotifications.logViewListEvent('blog', 'core_blog_view_entries', filter, siteId); + + const site = await CoreSites.getSite(siteId); + + const data: AddonBlogViewEntriesWSParams = { + filters: CoreUtils.objectToArrayOfObjects(filter, 'name', 'value'), + }; + + return site.write('core_blog_view_entries', data); + } + +} +export const AddonBlog = makeSingleton(AddonBlogProvider); + +/** + * Params of core_blog_get_entries WS. + */ +type CoreBlogGetEntriesWSParams = { + filters?: { // Parameters to filter blog listings. + name: string; // The expected keys (value format) are: + // tag PARAM_NOTAGS blog tag + // tagid PARAM_INT blog tag id + // userid PARAM_INT blog author (userid) + // cmid PARAM_INT course module id + // entryid PARAM_INT entry id + // groupid PARAM_INT group id + // courseid PARAM_INT course id + // search PARAM_RAW search term. + value: string; // The value of the filter. + }[]; + page?: number; // The blog page to return. + perpage?: number; // The number of posts to return per page. +}; + +/** + * Data returned by core_blog_get_entries WS. + */ +export type CoreBlogGetEntriesWSResponse = { + entries: AddonBlogPost[]; + totalentries: number; // The total number of entries found. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by blog's post_exporter. + */ +export type AddonBlogPost = { + id: number; // Post/entry id. + module: string; // Where it was published the post (blog, blog_external...). + userid: number; // Post author. + courseid: number; // Course where the post was created. + groupid: number; // Group post was created for. + moduleid: number; // Module id where the post was created (not used anymore). + coursemoduleid: number; // Course module id where the post was created. + subject: string; // Post subject. + summary: string; // Post summary. + summaryformat?: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + content: string; // Post content. + uniquehash: string; // Post unique hash. + rating: number; // Post rating. + format: number; // Post content format. + attachment: string; // Post atachment. + publishstate: string; // Post publish state. + lastmodified: number; // When it was last modified. + created: number; // When it was created. + usermodified: number; // User that updated the post. + summaryfiles: CoreWSExternalFile[]; // Summaryfiles. + attachmentfiles?: CoreWSExternalFile[]; // Attachmentfiles. + tags?: CoreTagItem[]; // @since 3.7. Tags. +}; + +/** + * Params of core_blog_view_entries WS. + */ +type AddonBlogViewEntriesWSParams = { + filters?: { // Parameters used in the filter of view_entries. + name: string; // The expected keys (value format) are: + // tag PARAM_NOTAGS blog tag + // tagid PARAM_INT blog tag id + // userid PARAM_INT blog author (userid) + // cmid PARAM_INT course module id + // entryid PARAM_INT entry id + // groupid PARAM_INT group id + // courseid PARAM_INT course id + // search PARAM_RAW search term. + value: string; // The value of the filter. + }[]; +}; + +export type AddonBlogFilter = { + tag?: string; // Blog tag + tagid?: number; // Blog tag id + userid?: number; // Blog author (userid) + cmid?: number; // Course module id + entryid?: number; // Entry id + groupid?: number; // Group id + courseid?: number; // Course id + search?: string; // Search term. +}; diff --git a/src/addons/blog/services/handlers/course-option.ts b/src/addons/blog/services/handlers/course-option.ts new file mode 100644 index 000000000..7b6f241e4 --- /dev/null +++ b/src/addons/blog/services/handlers/course-option.ts @@ -0,0 +1,109 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { + CoreCourseAccess, + CoreCourseOptionsHandler, + CoreCourseOptionsHandlerData, +} from '@features/course/services/course-options-delegate'; +import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites } from '@services/sites'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonBlog } from '../blog'; +import { AddonBlogMainMenuHandlerService } from './mainmenu'; + +/** + * Course nav handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogCourseOptionHandlerService implements CoreCourseOptionsHandler { + + name = 'AddonBlog'; + priority = 100; + + /** + * @inheritdoc + */ + invalidateEnabledForCourse(courseId: number): Promise { + return CoreCourse.invalidateCourseBlocks(courseId); + } + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonBlog.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + async isEnabledForCourse( + courseId: number, + accessData: CoreCourseAccess, + navOptions?: CoreCourseUserAdminOrNavOptionIndexed, + ): Promise { + const enabled = await CoreCourseHelper.hasABlockNamed(courseId, 'blog_menu'); + + if (enabled && navOptions && typeof navOptions.blogs != 'undefined') { + return navOptions.blogs; + } + + return enabled; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreCourseOptionsHandlerData | Promise { + return { + title: 'addon.blog.blog', + class: 'addon-blog-handler', + page: AddonBlogMainMenuHandlerService.PAGE_NAME, + }; + } + + /** + * @inheritdoc + */ + async prefetch(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise { + const siteId = CoreSites.getCurrentSiteId(); + + const result = await AddonBlog.getEntries({ courseid: course.id }); + + await Promise.all(result.entries.map(async (entry) => { + let files: CoreWSExternalFile[] = []; + + if (entry.attachmentfiles && entry.attachmentfiles.length) { + files = entry.attachmentfiles; + } + + if (entry.summaryfiles && entry.summaryfiles.length) { + files = files.concat(entry.summaryfiles); + } + + if (files.length > 0) { + await CoreFilepool.addFilesToQueue(siteId, files, entry.module, entry.id); + } + })); + } + +} +export const AddonBlogCourseOptionHandler = makeSingleton(AddonBlogCourseOptionHandlerService); diff --git a/src/addons/blog/services/handlers/index-link.ts b/src/addons/blog/services/handlers/index-link.ts new file mode 100644 index 000000000..add0dcafb --- /dev/null +++ b/src/addons/blog/services/handlers/index-link.ts @@ -0,0 +1,61 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Params } from '@angular/router'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonBlog } from '../blog'; + +/** + * Handler to treat links to blog page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogIndexLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonBlogIndexLinkHandler'; + featureName = 'CoreUserDelegate_AddonBlog:blogs'; + pattern = /\/blog\/index\.php/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + const pageParams: Params = {}; + + params.userid ? pageParams['userId'] = parseInt(params.userid, 10) : null; + params.modid ? pageParams['cmId'] = parseInt(params.modid, 10) : null; + params.courseid ? pageParams['courseId'] = parseInt(params.courseid, 10) : null; + params.entryid ? pageParams['entryId'] = parseInt(params.entryid, 10) : null; + params.groupid ? pageParams['groupId'] = parseInt(params.groupid, 10) : null; + params.tagid ? pageParams['tagId'] = parseInt(params.tagid, 10) : null; + + return [{ + action: (siteId: string): void => { + CoreNavigator.navigateToSitePath('/blog', { params: pageParams, siteId }); + }, + }]; + } + + /** + * @inheritdoc + */ + isEnabled(siteId: string): Promise { + return AddonBlog.isPluginEnabled(siteId); + } + +} +export const AddonBlogIndexLinkHandler = makeSingleton(AddonBlogIndexLinkHandlerService); diff --git a/src/addons/blog/services/handlers/mainmenu.ts b/src/addons/blog/services/handlers/mainmenu.ts new file mode 100644 index 000000000..6b49a07ec --- /dev/null +++ b/src/addons/blog/services/handlers/mainmenu.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonBlog } from '../blog'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogMainMenuHandlerService implements CoreMainMenuHandler { + + static readonly PAGE_NAME = 'blog'; + + name = 'AddonBlog'; + priority = 450; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return AddonBlog.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'far-newspaper', + title: 'addon.blog.siteblogheading', + page: AddonBlogMainMenuHandlerService.PAGE_NAME, + class: 'addon-blog-handler', + }; + } + +} +export const AddonBlogMainMenuHandler = makeSingleton(AddonBlogMainMenuHandlerService); diff --git a/src/addons/blog/services/handlers/tag-area.ts b/src/addons/blog/services/handlers/tag-area.ts new file mode 100644 index 000000000..3a01dd0ac --- /dev/null +++ b/src/addons/blog/services/handlers/tag-area.ts @@ -0,0 +1,53 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Type } from '@angular/core'; +import { CoreTagFeedComponent } from '@features/tag/components/feed/feed'; +import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; +import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper'; +import { makeSingleton } from '@singletons'; +import { AddonBlog } from '../blog'; + +/** + * Handler to support tags. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogTagAreaHandlerService implements CoreTagAreaHandler { + + name = 'AddonBlogTagAreaHandler'; + type = 'core/post'; + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonBlog.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + async parseContent(content: string): Promise { + return CoreTagHelper.parseFeedContent(content); + } + + /** + * @inheritdoc + */ + getComponent(): Type | Promise> { + return CoreTagFeedComponent; + } + +} +export const AddonBlogTagAreaHandler = makeSingleton(AddonBlogTagAreaHandlerService); diff --git a/src/addons/blog/services/handlers/user.ts b/src/addons/blog/services/handlers/user.ts new file mode 100644 index 000000000..f2e625f58 --- /dev/null +++ b/src/addons/blog/services/handlers/user.ts @@ -0,0 +1,64 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreUserProfileHandler, CoreUserProfileHandlerData, CoreUserDelegateService } from '@features/user/services/user-delegate'; +import { CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonBlog } from '../blog'; + +/** + * Profile item handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonBlogUserHandlerService implements CoreUserProfileHandler { + + name = 'AddonBlog:blogs'; + priority = 300; + type = CoreUserDelegateService.TYPE_NEW_PAGE; + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonBlog.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + async isEnabledForUser(): Promise { + return true; + } + + /** + * @inheritdoc + */ + getDisplayData(): CoreUserProfileHandlerData { + return { + icon: 'far-newspaper', + title: 'addon.blog.blogentries', + class: 'addon-blog-handler', + action: (event, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + CoreNavigator.navigateToSitePath('/blog', { + params: { courseId, userId: user.id }, + }); + }, + }; + } + +} +export const AddonBlogUserHandler = makeSingleton(AddonBlogUserHandlerService); diff --git a/src/addons/calendar/services/handlers/mainmenu.ts b/src/addons/calendar/services/handlers/mainmenu.ts index 2f2e2395e..5540b1149 100644 --- a/src/addons/calendar/services/handlers/mainmenu.ts +++ b/src/addons/calendar/services/handlers/mainmenu.ts @@ -25,7 +25,6 @@ export class AddonCalendarMainMenuHandlerService implements CoreMainMenuHandler static readonly PAGE_NAME = 'calendar'; - name = 'AddonCalendar'; priority = 900; diff --git a/src/addons/mod/forum/components/index/index.html b/src/addons/mod/forum/components/index/index.html index cf63909d5..5a76edcc0 100644 --- a/src/addons/mod/forum/components/index/index.html +++ b/src/addons/mod/forum/components/index/index.html @@ -8,7 +8,7 @@ [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"> + [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'far-newspaper'" (action)="gotoBlog()"> { - // const params: Params = { cmId: this.module?.id }; - // @todo return CoreNavigator.navigateToSitePath('AddonBlogEntriesPage', { params }); + const params: Params = { cmId: this.module?.id }; + + CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params }); } /**