+
+
+
+
+
+
+
+ {{ 'addon.blog.linktooriginalentry' | translate }}
+
+
+ entry.created">
+
+ {{entry.lastmodified | coreTimeAgo}}
+
+
+
+
+
+
+
+
diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts
new file mode 100644
index 000000000..54f84d30a
--- /dev/null
+++ b/src/addon/blog/components/entries/entries.ts
@@ -0,0 +1,176 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Content } from 'ionic-angular';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreUserProvider } from '@core/user/providers/user';
+import { AddonBlogProvider } from '../../providers/blog';
+
+/**
+ * Component that displays the blog entries.
+ */
+@Component({
+ selector: 'addon-blog-entries',
+ templateUrl: 'addon-blog-entries.html',
+})
+export class AddonBlogEntriesComponent implements OnInit {
+ @Input() userId?: number;
+ @Input() courseId?: number;
+ @Input() cmId?: number;
+ @Input() entryId?: number;
+ @Input() groupId?: number;
+ @Input() tagId?: number;
+
+ protected filter = {};
+ protected pageLoaded = 0;
+
+ @ViewChild(Content) content: Content;
+
+ loaded = false;
+ canLoadMore = false;
+ loadMoreError = false;
+ entries = [];
+ currentUserId: number;
+ showMyIssuesToggle = false;
+ onlyMyEntries = false;
+ component = AddonBlogProvider.COMPONENT;
+
+ constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
+ protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider) {
+ this.currentUserId = sitesProvider.getCurrentSiteUserId();
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ if (this.userId) {
+ this.filter['userid'] = this.userId;
+ }
+
+ if (this.courseId) {
+ this.filter['courseid'] = this.courseId;
+ }
+
+ if (this.cmId) {
+ this.filter['cmid'] = this.cmId;
+ }
+
+ if (this.entryId) {
+ this.filter['entryid'] = this.entryId;
+ }
+
+ if (this.groupId) {
+ this.filter['groupid'] = this.groupId;
+ }
+
+ if (this.tagId) {
+ this.filter['tagid'] = this.tagId;
+ }
+
+ this.fetchEntries().then(() => {
+ this.blogProvider.logView(this.filter).catch(() => {
+ // Ignore errors.
+ });
+ });
+ }
+
+ /**
+ * Fetch blog entries.
+ *
+ * @param {boolean} [refresh] Empty events array first.
+ * @return {Promise} Promise with the entries.
+ */
+ private fetchEntries(refresh: boolean = false): Promise {
+ this.loadMoreError = false;
+
+ if (refresh) {
+ this.pageLoaded = 0;
+ }
+
+ return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => {
+ const promises = result.entries.map((entry) => {
+ 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;
+ }
+
+ return this.userProvider.getProfile(entry.userid, entry.courseid, true).then((user) => {
+ entry.user = user;
+ }).catch(() => {
+ // Ignore errors.
+ });
+ });
+
+ if (refresh) {
+ this.showMyIssuesToggle = false;
+ this.entries = result.entries;
+ } else {
+ this.entries = this.entries.concat(result.entries);
+ }
+
+ this.canLoadMore = result.totalentries > this.entries.length;
+ this.pageLoaded++;
+
+ this.showMyIssuesToggle = !this.userId;
+
+ return Promise.all(promises);
+ }).catch((message) => {
+ this.domUtils.showErrorModalDefault(message, 'addon.blog.errorloadentries', true);
+ this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Function to load more entries.
+ *
+ * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading.
+ * @return {Promise} Resolved when done.
+ */
+ loadMore(infiniteComplete?: any): Promise {
+ return this.fetchEntries().finally(() => {
+ infiniteComplete && infiniteComplete();
+ });
+ }
+
+ /**
+ * Refresh blog entries on PTR.
+ *
+ * @param {any} refresher Refresher instance.
+ */
+ refresh(refresher?: any): void {
+ this.blogProvider.invalidateEntries(this.filter).finally(() => {
+ this.fetchEntries(true).finally(() => {
+ if (refresher) {
+ refresher.complete();
+ }
+ });
+ });
+ }
+
+}
diff --git a/src/addon/blog/lang/en.json b/src/addon/blog/lang/en.json
new file mode 100644
index 000000000..6e183232f
--- /dev/null
+++ b/src/addon/blog/lang/en.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/addon/blog/pages/entries/entries.html b/src/addon/blog/pages/entries/entries.html
new file mode 100644
index 000000000..e5a12aba8
--- /dev/null
+++ b/src/addon/blog/pages/entries/entries.html
@@ -0,0 +1,7 @@
+
+
+ {{ title | translate }}
+
+
+
+
diff --git a/src/addon/blog/pages/entries/entries.module.ts b/src/addon/blog/pages/entries/entries.module.ts
new file mode 100644
index 000000000..a9cb5564f
--- /dev/null
+++ b/src/addon/blog/pages/entries/entries.module.ts
@@ -0,0 +1,33 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonBlogEntriesPage } from './entries';
+import { AddonBlogComponentsModule } from '../../components/components.module';
+
+@NgModule({
+ declarations: [
+ AddonBlogEntriesPage,
+ ],
+ imports: [
+ CoreDirectivesModule,
+ AddonBlogComponentsModule,
+ IonicPageModule.forChild(AddonBlogEntriesPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonBlogEntriesPageModule {}
diff --git a/src/addon/blog/pages/entries/entries.ts b/src/addon/blog/pages/entries/entries.ts
new file mode 100644
index 000000000..220de15df
--- /dev/null
+++ b/src/addon/blog/pages/entries/entries.ts
@@ -0,0 +1,49 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { Component } from '@angular/core';
+import { IonicPage, NavParams } from 'ionic-angular';
+
+/**
+ * Page that displays the list of blog entries.
+ */
+@IonicPage({ segment: 'addon-blog-entries' })
+@Component({
+ selector: 'page-addon-blog-entries',
+ templateUrl: 'entries.html',
+})
+export class AddonBlogEntriesPage {
+ userId: number;
+ courseId: number;
+ cmId: number;
+ entryId: number;
+ groupId: number;
+ tagId: number;
+ title: string;
+
+ constructor(params: NavParams) {
+ this.userId = params.get('userId');
+ this.courseId = params.get('courseId');
+ this.cmId = params.get('cmId');
+ this.entryId = params.get('entryId');
+ this.groupId = params.get('groupId');
+ this.tagId = params.get('tagId');
+
+ if (!this.userId && !this.courseId && !this.cmId && !this.entryId && !this.groupId && !this.tagId) {
+ this.title = 'addon.blog.siteblogheading';
+ } else {
+ this.title = 'addon.blog.blogentries';
+ }
+ }
+}
diff --git a/src/addon/blog/providers/blog.ts b/src/addon/blog/providers/blog.ts
new file mode 100644
index 000000000..adc15b1f2
--- /dev/null
+++ b/src/addon/blog/providers/blog.ts
@@ -0,0 +1,113 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+
+/**
+ * Service to handle blog entries.
+ */
+@Injectable()
+export class AddonBlogProvider {
+ static ENTRIES_PER_PAGE = 10;
+ static COMPONENT = 'blog';
+ protected ROOT_CACHE_KEY = 'addonBlog:';
+ protected logger;
+
+ constructor(logger: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider) {
+ this.logger = logger.getInstance('AddonBlogProvider');
+ }
+
+ /**
+ * 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 {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with true if enabled, resolved with false or rejected otherwise.
+ */
+ isPluginEnabled(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.wsAvailable('core_blog_get_entries') &&
+ site.canUseAdvancedFeature('enableblogs');
+ });
+ }
+
+ /**
+ * Get the cache key for the blog entries.
+ *
+ * @param {any} [filter] Filter to apply on search.
+ * @return {string} Cache key.
+ */
+ getEntriesCacheKey(filter: any = {}): string {
+ return this.ROOT_CACHE_KEY + this.utils.sortAndStringify(filter);
+ }
+
+ /**
+ * Get blog entries.
+ *
+ * @param {any} [filter] Filter to apply on search.
+ * @param {any} [page=0] Page of the blog entries to fetch.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise to be resolved when the entries are retrieved.
+ */
+ getEntries(filter: any = {}, page: number = 0, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const data = {
+ filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value'),
+ page: page,
+ perpage: AddonBlogProvider.ENTRIES_PER_PAGE
+ };
+
+ const preSets = {
+ cacheKey: this.getEntriesCacheKey(filter)
+ };
+
+ return site.read('core_blog_get_entries', data, preSets);
+ });
+ }
+
+ /**
+ * Invalidate blog entries WS call.
+ *
+ * @param {any} [filter] Filter to apply on search
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when data is invalidated.
+ */
+ invalidateEntries(filter: any = {}, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getEntriesCacheKey(filter));
+ });
+ }
+
+ /**
+ * Trigger the blog_entries_viewed event.
+ *
+ * @param {any} [filter] Filter to apply on search.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise to be resolved when done.
+ */
+ logView(filter: any = {}, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const data = {
+ filters: this.utils.objectToArrayOfObjects(filter, 'name', 'value')
+ };
+
+ return site.write('core_blog_view_entries', data);
+ });
+ }
+}
diff --git a/src/addon/blog/providers/course-option-handler.ts b/src/addon/blog/providers/course-option-handler.ts
new file mode 100644
index 000000000..9c28973fb
--- /dev/null
+++ b/src/addon/blog/providers/course-option-handler.ts
@@ -0,0 +1,120 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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, Injector } from '@angular/core';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreFilepoolProvider } from '@providers/filepool';
+import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '@core/course/providers/options-delegate';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreCourseHelperProvider } from '@core/course/providers/helper';
+import { AddonBlogEntriesComponent } from '../components/entries/entries';
+import { AddonBlogProvider } from './blog';
+
+/**
+ * Course nav handler.
+ */
+@Injectable()
+export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler {
+ name = 'AddonBlog';
+ priority = 100;
+
+ constructor(protected coursesProvider: CoreCoursesProvider, protected blogProvider: AddonBlogProvider,
+ protected courseHelper: CoreCourseHelperProvider, protected courseProvider: CoreCourseProvider,
+ protected sitesProvider: CoreSitesProvider, protected filepoolProvider: CoreFilepoolProvider) {}
+
+ /**
+ * Should invalidate the data to determine if the handler is enabled for a certain course.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {Promise} Promise resolved when done.
+ */
+ invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise {
+ return this.courseProvider.invalidateCourseBlocks(courseId);
+ }
+
+ /**
+ * Check if the handler is enabled on a site level.
+ *
+ * @return {boolean} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.blogProvider.isPluginEnabled();
+ }
+
+ /**
+ * Whether or not the handler is enabled for a certain course.
+ *
+ * @param {number} courseId The course ID.
+ * @param {any} accessData Access type and data. Default, guest, ...
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} True or promise resolved with true if enabled.
+ */
+ isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise {
+ return this.courseHelper.hasABlockNamed(courseId, 'blog_menu').then((enabled) => {
+ if (enabled && navOptions && typeof navOptions.blogs != 'undefined') {
+ return navOptions.blogs;
+ }
+
+ return enabled;
+ });
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @param {Injector} injector Injector.
+ * @param {number} courseId The course ID.
+ * @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise {
+ return {
+ title: 'addon.blog.blog',
+ class: 'addon-blog-handler',
+ component: AddonBlogEntriesComponent
+ };
+ }
+
+ /**
+ * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
+ *
+ * @param {any} course The course.
+ * @return {Promise} Promise resolved when done.
+ */
+ prefetch(course: any): Promise {
+ const siteId = this.sitesProvider.getCurrentSiteId();
+
+ return this.blogProvider.getEntries({courseid: course.id}).then((result) => {
+ return result.entries.map((entry) => {
+ let files = [];
+
+ if (entry.attachmentfiles && entry.attachmentfiles.length) {
+ files = entry.attachmentfiles;
+ }
+ if (entry.summaryfiles && entry.summaryfiles.length) {
+ files = files.concat(entry.summaryfiles);
+ }
+
+ if (files.length > 0) {
+ return this.filepoolProvider.addFilesToQueue(siteId, files, entry.module, entry.id);
+ }
+
+ return Promise.resolve();
+ });
+ });
+ }
+}
diff --git a/src/addon/blog/providers/index-link-handler.ts b/src/addon/blog/providers/index-link-handler.ts
new file mode 100644
index 000000000..176aec76e
--- /dev/null
+++ b/src/addon/blog/providers/index-link-handler.ts
@@ -0,0 +1,76 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { AddonBlogProvider } from './blog';
+
+/**
+ * Handler to treat links to blog page.
+ */
+@Injectable()
+export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase {
+ name = 'AddonBlogIndexLinkHandler';
+ featureName = 'CoreUserDelegate_AddonBlog';
+ pattern = /\/blog\/index\.php/;
+
+ constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) {
+ super();
+ }
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+ const pageParams: any = {};
+
+ 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, navCtrl?): void => {
+ // Always use redirect to make it the new history root (to avoid "loops" in history).
+ this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId);
+ }
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param {string} siteId The site ID.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {boolean|Promise} Whether the handler is enabled for the URL and site.
+ */
+ isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
+
+ return this.blogProvider.isPluginEnabled(siteId);
+ }
+}
diff --git a/src/addon/blog/providers/mainmenu-handler.ts b/src/addon/blog/providers/mainmenu-handler.ts
new file mode 100644
index 000000000..e45bfd06f
--- /dev/null
+++ b/src/addon/blog/providers/mainmenu-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { AddonBlogProvider } from './blog';
+import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@core/mainmenu/providers/delegate';
+
+/**
+ * Handler to inject an option into main menu.
+ */
+@Injectable()
+export class AddonBlogMainMenuHandler implements CoreMainMenuHandler {
+ name = 'AddonBlog';
+ priority = 450;
+
+ constructor(private blogProvider: AddonBlogProvider) { }
+
+ /**
+ * Check if the handler is enabled on a site level.
+ *
+ * @return {boolean} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.blogProvider.isPluginEnabled();
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @return {CoreMainMenuHandlerData} Data needed to render the handler.
+ */
+ getDisplayData(): CoreMainMenuHandlerData {
+ return {
+ icon: 'fa-newspaper-o',
+ title: 'addon.blog.siteblogheading',
+ page: 'AddonBlogEntriesPage',
+ class: 'addon-blog-handler'
+ };
+ }
+}
diff --git a/src/addon/blog/providers/user-handler.ts b/src/addon/blog/providers/user-handler.ts
new file mode 100644
index 000000000..039b9ed56
--- /dev/null
+++ b/src/addon/blog/providers/user-handler.ts
@@ -0,0 +1,71 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
+import { AddonBlogProvider } from './blog';
+
+/**
+ * Profile item handler.
+ */
+@Injectable()
+export class AddonBlogUserHandler implements CoreUserProfileHandler {
+ name = 'AddonBlog:blogs';
+ priority = 300;
+ type = CoreUserDelegate.TYPE_NEW_PAGE;
+
+ constructor(protected linkHelper: CoreContentLinksHelperProvider, protected blogProvider: AddonBlogProvider) {
+ }
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.blogProvider.isPluginEnabled();
+ }
+
+ /**
+ * Check if handler is enabled for this user in this context.
+ *
+ * @param {any} user User to check.
+ * @param {number} courseId Course ID.
+ * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+ * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+ * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise.
+ */
+ isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise {
+ return true;
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @return {CoreUserProfileHandlerData} Data needed to render the handler.
+ */
+ getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
+ return {
+ icon: 'fa-newspaper-o',
+ title: 'addon.blog.blogentries',
+ class: 'addon-blog-handler',
+ action: (event, navCtrl, user, courseId): void => {
+ event.preventDefault();
+ event.stopPropagation();
+ // Always use redirect to make it the new history root (to avoid "loops" in history).
+ this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId });
+ }
+ };
+ }
+}
diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts
index 85aa71eb2..3146e6e69 100644
--- a/src/addon/calendar/calendar.module.ts
+++ b/src/addon/calendar/calendar.module.ts
@@ -71,7 +71,7 @@ export class AddonCalendarModule {
newName: AddonCalendarProvider.EVENTS_TABLE,
filterFields: ['id', 'name', 'description', 'format', 'eventtype', 'courseid', 'timestart', 'timeduration',
'categoryid', 'groupid', 'userid', 'instance', 'modulename', 'timemodified', 'repeatid', 'visible', 'uuid',
- 'sequence', 'subscriptionid', 'notificationtime']
+ 'sequence', 'subscriptionid']
});
// Migrate the component name.
diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json
index 3342aaeeb..6ccb04caa 100644
--- a/src/addon/calendar/lang/en.json
+++ b/src/addon/calendar/lang/en.json
@@ -1,6 +1,7 @@
{
"calendar": "Calendar",
"calendarevents": "Calendar events",
+ "calendarreminders": "Calendar reminders",
"defaultnotificationtime": "Default notification time",
"errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.",
@@ -8,7 +9,8 @@
"eventstarttime": "Start time",
"gotoactivity": "Go to activity",
"noevents": "There are no events",
- "notifications": "Notifications",
+ "reminders": "Reminders",
+ "setnewreminder": "Set a new reminder",
"typeclose": "Close event",
"typecourse": "Course event",
"typecategory": "Category event",
diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html
index a9711f678..c00376085 100644
--- a/src/addon/calendar/pages/event/event.html
+++ b/src/addon/calendar/pages/event/event.html
@@ -10,10 +10,10 @@
-
+
-
-
+
diff --git a/src/addon/coursecompletion/components/report/report.ts b/src/addon/coursecompletion/components/report/report.ts
index 69583770f..50d12cf90 100644
--- a/src/addon/coursecompletion/components/report/report.ts
+++ b/src/addon/coursecompletion/components/report/report.ts
@@ -31,6 +31,7 @@ export class AddonCourseCompletionReportComponent implements OnInit {
completionLoaded = false;
completion: any;
showSelfComplete: boolean;
+ tracked = true; // Whether completion is tracked.
constructor(
private sitesProvider: CoreSitesProvider,
@@ -62,8 +63,14 @@ export class AddonCourseCompletionReportComponent implements OnInit {
this.completion = completion;
this.showSelfComplete = this.courseCompletionProvider.canMarkSelfCompleted(this.userId, completion);
- }).catch((message) => {
- this.domUtils.showErrorModalDefault(message, 'addon.coursecompletion.couldnotloadreport', true);
+ this.tracked = true;
+ }).catch((error) => {
+ if (error && error.errorcode == 'notenroled') {
+ // Not enrolled error, probably a teacher.
+ this.tracked = false;
+ } else {
+ this.domUtils.showErrorModalDefault(error, 'addon.coursecompletion.couldnotloadreport', true);
+ }
});
}
diff --git a/src/addon/coursecompletion/lang/en.json b/src/addon/coursecompletion/lang/en.json
index 7607702c6..81ef0272e 100644
--- a/src/addon/coursecompletion/lang/en.json
+++ b/src/addon/coursecompletion/lang/en.json
@@ -12,6 +12,7 @@
"criteriarequiredany": "Any criteria below are required",
"inprogress": "In progress",
"manualselfcompletion": "Manual self completion",
+ "nottracked": "You are currently not being tracked by completion in this course",
"notyetstarted": "Not yet started",
"pending": "Pending",
"required": "Required",
diff --git a/src/addon/messages/components/discussions/discussions.ts b/src/addon/messages/components/discussions/discussions.ts
index bf3eb9f21..b072b719b 100644
--- a/src/addon/messages/components/discussions/discussions.ts
+++ b/src/addon/messages/components/discussions/discussions.ts
@@ -62,7 +62,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
// Update discussions when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
- if (data.userId) {
+ if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => {
return disc.message.user == data.userId;
});
@@ -82,7 +82,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
// Update discussions when a message is read.
this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => {
- if (data.userId) {
+ if (data.userId && this.discussions) {
const discussion = this.discussions.find((disc) => {
return disc.message.user == data.userId;
});
@@ -92,8 +92,8 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
discussion.unread = false;
// Conversations changed, invalidate them and refresh unread counts.
- this.messagesProvider.invalidateConversations();
- this.messagesProvider.refreshUnreadConversationCounts();
+ this.messagesProvider.invalidateConversations(this.siteId);
+ this.messagesProvider.refreshUnreadConversationCounts(this.siteId);
}
}
}, this.siteId);
@@ -145,10 +145,10 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
*/
refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise {
const promises = [];
- promises.push(this.messagesProvider.invalidateDiscussionsCache());
+ promises.push(this.messagesProvider.invalidateDiscussionsCache(this.siteId));
if (refreshUnreadCounts) {
- promises.push(this.messagesProvider.invalidateUnreadConversationCounts());
+ promises.push(this.messagesProvider.invalidateUnreadConversationCounts(this.siteId));
}
return this.utils.allPromises(promises).finally(() => {
@@ -171,7 +171,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
const promises = [];
- promises.push(this.messagesProvider.getDiscussions().then((discussions) => {
+ promises.push(this.messagesProvider.getDiscussions(this.siteId).then((discussions) => {
// Convert to an array for sorting.
const discussionsSorted = [];
for (const userId in discussions) {
@@ -184,7 +184,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
});
}));
- promises.push(this.messagesProvider.getUnreadConversationCounts());
+ promises.push(this.messagesProvider.getUnreadConversationCounts(this.siteId));
return Promise.all(promises).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
@@ -216,7 +216,7 @@ export class AddonMessagesDiscussionsComponent implements OnDestroy {
this.loaded = false;
this.loadingMessage = this.search.loading;
- return this.messagesProvider.searchMessages(query).then((searchResults) => {
+ return this.messagesProvider.searchMessages(query, undefined, undefined, undefined, this.siteId).then((searchResults) => {
this.search.showResults = true;
this.search.results = searchResults.messages;
}).catch((error) => {
diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json
index 2ec162f66..1b542a0c7 100644
--- a/src/addon/messages/lang/en.json
+++ b/src/addon/messages/lang/en.json
@@ -66,11 +66,14 @@
"unabletomessage": "You are unable to message this user",
"unblockuser": "Unblock user",
"unblockuserconfirm": "Are you sure you want to unblock {{$a}}?",
+ "useentertosend": "Use enter to send",
+ "useentertosenddescdesktop": "If disabled, you can use Ctrl+Enter to send the message.",
+ "useentertosenddescmac": "If disabled, you can use Cmd+Enter to send the message.",
"userwouldliketocontactyou": "{{$a}} would like to contact you",
"warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}",
"warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"wouldliketocontactyou": "Would like to contact you",
"you": "You:",
- "youhaveblockeduser": "You have blocked this user in the past",
+ "youhaveblockeduser": "You have blocked this user.",
"yourcontactrequestpending": "Your contact request is pending with {{$a}}"
}
\ No newline at end of file
diff --git a/src/addon/messages/messages.module.ts b/src/addon/messages/messages.module.ts
index 43b3609c6..9ea151d83 100644
--- a/src/addon/messages/messages.module.ts
+++ b/src/addon/messages/messages.module.ts
@@ -109,11 +109,20 @@ export class AddonMessagesModule {
messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => {
// Check if group messaging is enabled, to determine which page should be loaded.
messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => {
+ const pageParams: any = {};
let pageName = 'AddonMessagesIndexPage';
if (enabled) {
pageName = 'AddonMessagesGroupConversationsPage';
}
- linkHelper.goInSite(undefined, pageName, undefined, notification.site);
+
+ // Check if we have enough information to open the conversation.
+ if (notification.convid && enabled) {
+ pageParams.conversationId = Number(notification.convid);
+ } else if (notification.userfromid) {
+ pageParams.discussionUserId = Number(notification.userfromid);
+ }
+
+ linkHelper.goInSite(undefined, pageName, pageParams, notification.site);
});
});
});
diff --git a/src/addon/messages/pages/conversation-info/conversation-info.html b/src/addon/messages/pages/conversation-info/conversation-info.html
index 36af28369..393b078b6 100644
--- a/src/addon/messages/pages/conversation-info/conversation-info.html
+++ b/src/addon/messages/pages/conversation-info/conversation-info.html
@@ -27,7 +27,7 @@
-
+
diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts
index b3151026e..25cdcab13 100644
--- a/src/addon/messages/pages/discussion/discussion.ts
+++ b/src/addon/messages/pages/discussion/discussion.ts
@@ -352,8 +352,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}
// Check if we are at the bottom to scroll it after render.
- this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) ===
- this.domUtils.getContentHeight(this.content);
+ // Use a 5px error margin because in iOS there is 1px difference for some reason.
+ this.scrollBottom = Math.abs(this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) -
+ this.domUtils.getContentHeight(this.content)) < 5;
if (this.messagesBeingSent > 0) {
// Ignore polling due to a race condition.
diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html
index e4d916c38..10d5c3052 100644
--- a/src/addon/messages/pages/group-conversations/group-conversations.html
+++ b/src/addon/messages/pages/group-conversations/group-conversations.html
@@ -100,7 +100,7 @@
-
+
0 || conversation.unreadcount">
0">{{ conversation.unreadcount }}
diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts
index 39be42305..332bae82c 100644
--- a/src/addon/messages/pages/group-conversations/group-conversations.ts
+++ b/src/addon/messages/pages/group-conversations/group-conversations.ts
@@ -70,6 +70,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
protected siteId: string;
protected currentUserId: number;
protected conversationId: number;
+ protected discussionUserId: number;
protected newMessagesObserver: any;
protected pushObserver: any;
protected appResumeSubscription: any;
@@ -89,7 +90,9 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
this.loadingString = translate.instant('core.loading');
this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId();
+ // Conversation to load.
this.conversationId = navParams.get('conversationId') || false;
+ this.discussionUserId = !this.conversationId && (navParams.get('discussionUserId') || false);
// Update conversations when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
@@ -138,8 +141,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
conversation.unreadcount = 0;
// Conversations changed, invalidate them and refresh unread counts.
- this.messagesProvider.invalidateConversations();
- this.messagesProvider.refreshUnreadConversationCounts();
+ this.messagesProvider.invalidateConversations(this.siteId);
+ this.messagesProvider.refreshUnreadConversationCounts(this.siteId);
}
}
}, this.siteId);
@@ -213,13 +216,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Component loaded.
*/
ngOnInit(): void {
- if (this.conversationId) {
+ if (this.conversationId || this.discussionUserId) {
// There is a discussion to load, open the discussion in a new state.
- this.gotoConversation(this.conversationId);
+ this.gotoConversation(this.conversationId, this.discussionUserId);
}
this.fetchData().then(() => {
- if (!this.conversationId && this.splitviewCtrl.isOn()) {
+ if (!this.conversationId && !this.discussionUserId && this.splitviewCtrl.isOn()) {
// Load the first conversation.
let conversation;
const expandedOption = this.getExpandedOption();
@@ -248,12 +251,12 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
const promises = [];
promises.push(this.fetchConversationCounts());
- promises.push(this.messagesProvider.getContactRequestsCount()); // View updated by the event observer.
+ promises.push(this.messagesProvider.getContactRequestsCount(this.siteId)); // View updated by the event observer.
return Promise.all(promises).then(() => {
if (typeof this.favourites.expanded == 'undefined') {
// The expanded status hasn't been initialized. Do it now.
- if (this.conversationId) {
+ if (this.conversationId || this.discussionUserId) {
// A certain conversation should be opened.
// We don't know which option it belongs to, so we need to fetch the data for all of them.
const promises = [];
@@ -264,7 +267,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
return Promise.all(promises).then(() => {
// All conversations have been loaded, find the one we need to load and expand its option.
- const conversation = this.findConversation(this.conversationId);
+ const conversation = this.findConversation(this.conversationId, this.discussionUserId);
if (conversation) {
const option = this.getConversationOption(conversation);
@@ -320,7 +323,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) {
- promises.push(this.messagesProvider.refreshUnreadConversationCounts()); // View updated by the event observer.
+ // View updated by event observer.
+ promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
return Promise.all(promises);
@@ -344,10 +348,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
offlineMessages;
// Get the conversations and, if needed, the offline messages. Always try to get the latest data.
- promises.push(this.messagesProvider.invalidateConversations().catch(() => {
+ promises.push(this.messagesProvider.invalidateConversations(this.siteId).catch(() => {
// Shouldn't happen.
}).then(() => {
- return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom);
+ return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom, this.siteId);
}).then((result) => {
data = result;
}));
@@ -359,7 +363,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
promises.push(this.fetchConversationCounts());
if (refreshUnreadCounts) {
- promises.push(this.messagesProvider.refreshUnreadConversationCounts()); // View updated by the event observer.
+ // View updated by the event observer.
+ promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
}
@@ -389,10 +394,10 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
*/
protected fetchConversationCounts(): Promise {
// Always try to get the latest data.
- return this.messagesProvider.invalidateConversationCounts().catch(() => {
+ return this.messagesProvider.invalidateConversationCounts(this.siteId).catch(() => {
// Shouldn't happen.
}).then(() => {
- return this.messagesProvider.getConversationCounts();
+ return this.messagesProvider.getConversationCounts(this.siteId);
}).then((counts) => {
this.favourites.count = counts.favourites;
this.individual.count = counts.individual;
@@ -607,7 +612,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise {
// Don't invalidate conversations and so, they always try to get latest data.
const promises = [
- this.messagesProvider.invalidateContactRequestsCountCache()
+ this.messagesProvider.invalidateContactRequestsCountCache(this.siteId)
];
return this.utils.allPromises(promises).finally(() => {
diff --git a/src/addon/messages/pages/search/search.html b/src/addon/messages/pages/search/search.html
index 43dfbd4c5..4a9ba7621 100644
--- a/src/addon/messages/pages/search/search.html
+++ b/src/addon/messages/pages/search/search.html
@@ -34,7 +34,7 @@