Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

MOBILE-4547 blog: Support offline blog #4043

Merged
merged 1 commit into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/addons/blog/blog.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
import { AddonBlogUserHandler } from './services/handlers/user';
import { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { BLOG_OFFLINE_SITE_SCHEMA } from './services/database/blog';
import { CoreCronDelegate } from '@services/cron';
import { AddonBlogSyncCronHandler } from './services/handlers/sync-cron';

const routes: Routes = [
{
Expand All @@ -44,6 +48,11 @@ const routes: Routes = [
CoreCourseIndexRoutingModule.forChild({ children: routes }),
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [BLOG_OFFLINE_SITE_SCHEMA],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
Expand All @@ -54,6 +63,7 @@ const routes: Routes = [
CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
CoreCourseOptionsDelegate.registerHandler(AddonBlogCourseOptionHandler.instance);
CoreCronDelegate.register(AddonBlogSyncCronHandler.instance);
},
},
],
Expand Down
3 changes: 3 additions & 0 deletions src/addons/blog/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@

export const ADDON_BLOG_MAINMENU_PAGE_NAME = 'blog';
export const ADDON_BLOG_ENTRY_UPDATED = 'blog_entry_updated';
export const ADDON_BLOG_AUTO_SYNCED = 'addon_blog_autom_synced';
export const ADDON_BLOG_MANUAL_SYNCED = 'addon_blog_manual_synced';
export const ADDON_BLOG_SYNC_ID = 'blog';
212 changes: 163 additions & 49 deletions src/addons/blog/pages/edit-entry/edit-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
import { ContextLevel } from '@/core/constants';
import { CoreSharedModule } from '@/core/shared.module';
import { ADDON_BLOG_ENTRY_UPDATED } from '@addons/blog/constants';
import { ADDON_BLOG_ENTRY_UPDATED, ADDON_BLOG_SYNC_ID } from '@addons/blog/constants';
import {
AddonBlog,
AddonBlogAddEntryOption,
Expand All @@ -22,26 +22,31 @@ import {
AddonBlogProvider,
AddonBlogPublishState,
} from '@addons/blog/services/blog';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { AddonBlogOffline } from '@addons/blog/services/blog-offline';
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AddonBlogSync } from '@addons/blog/services/blog-sync';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { CoreError } from '@classes/errors/error';
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseBasicData } from '@features/courses/services/courses';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
import { CanLeave } from '@guards/can-leave';
import { CoreLoadings } from '@services/loadings';
import { CoreNavigator } from '@services/navigator';
import { CoreNetwork } from '@services/network';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSFile } from '@services/ws';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import { CoreForms } from '@singletons/form';
import { CoreFileEntry } from '@services/file-helper';
import { CoreTimeUtils } from '@services/utils/time';

@Component({
selector: 'addon-blog-edit-entry',
Expand All @@ -54,7 +59,7 @@ import { CoreForms } from '@singletons/form';
CoreTagComponentsModule,
],
})
export class AddonBlogEditEntryPage implements CanLeave, OnInit {
export class AddonBlogEditEntryPage implements CanLeave, OnInit, OnDestroy {

@ViewChild('editEntryForm') formElement!: ElementRef;

Expand All @@ -70,11 +75,11 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
associateWithModule: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }),
});

entry?: AddonBlogPost;
entry?: AddonBlogPost | AddonBlogEditEntryFormattedOfflinePost;
loaded = false;
maxFiles = 99;
initialFiles: CoreWSFile[] = [];
files: CoreWSFile[] = [];
initialFiles: CoreFileEntry[] = [];
files: CoreFileEntry[] = [];
courseId?: number;
modId?: number;
userId?: number;
Expand All @@ -88,6 +93,7 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
component = AddonBlogProvider.COMPONENT;
siteHomeId?: number;
forceLeave = false;
isOfflineEntry = false;

/**
* Gives if the form is not pristine. (only for existing entries)
Expand Down Expand Up @@ -130,15 +136,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
return CoreNavigator.back();
}

const entryId = CoreNavigator.getRouteNumberParam('id');
const entryId = CoreNavigator.getRouteParam('id');
const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
const courseId = CoreNavigator.getRouteNumberParam('courseId');
const cmId = CoreNavigator.getRouteNumberParam('cmId');
this.userId = CoreNavigator.getRouteNumberParam('userId');
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
this.isOfflineEntry = entryId?.startsWith('new-') ?? false;
const entryIdParsed = Number(entryId);

if (!entryId) {
if (entryIdParsed === 0) {
this.loaded = true;

try {
Expand All @@ -162,11 +170,27 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
}

try {
this.entry = await this.getEntry({ filters, lastModified, entryId });
this.files = this.entry.attachmentfiles ?? [];
await AddonBlogSync.waitForSync(ADDON_BLOG_SYNC_ID);

if (!this.isOfflineEntry) {
const offlineContent = await this.getFormattedBlogOfflineEntry({ id: entryIdParsed });
this.entry = offlineContent ?? await this.getEntry({ filters, lastModified, entryId: entryIdParsed });
} else {
this.entry = await this.getFormattedBlogOfflineEntry({ created: Number(entryId?.slice(4)) });

if (!this.entry) {
throw new CoreError('This offline entry no longer exists.');
}
}

this.files = [...(this.entry.attachmentfiles ?? [])];
this.initialFiles = [...this.files];
this.courseId = this.courseId || this.entry.courseid;
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;

if (this.entry) {
CoreSync.blockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
this.courseId = this.courseId || this.entry.courseid;
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;
}

if (this.courseId) {
this.form.controls.associateWithCourse.setValue(true);
Expand Down Expand Up @@ -198,6 +222,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
this.loaded = true;
}

/**
* @inheritdoc
*/
ngOnDestroy(): void {
if (!this.entry) {
return;
}

CoreSync.unblockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
}

/**
* Retrieves blog entry.
*
Expand Down Expand Up @@ -270,45 +305,93 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {

const loading = await CoreLoadings.show('core.sending', true);

alfonso-salces marked this conversation as resolved.
Show resolved Hide resolved
if (this.entry) {
if (this.entry?.id) {
try {
if (!CoreNetwork.isOnline()) {
const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id });

return await this.saveEntry({ attachmentsId });
}

if (!CoreFileUploader.areFileListDifferent(this.files, this.initialFiles)) {
return await this.saveEntry();
return await this.saveEntry({});
}

const { attachmentsid } = await AddonBlog.prepareEntryForEdition({ entryid: this.entry.id });
const removedFiles = CoreFileUploader.getFilesToDelete(this.initialFiles, this.files);

const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
const entry = this.entry && 'attachment' in this.entry
? this.entry
: await CoreUtils.ignoreErrors(this.getEntry({ filters, lastModified, entryId: this.entry.id }));

const removedFiles = CoreFileUploader.getFilesToDelete(entry?.attachmentfiles ?? [], this.files);

if (removedFiles.length) {
await CoreFileUploader.deleteDraftFiles(attachmentsid, removedFiles);
}

await CoreFileUploader.uploadFiles(attachmentsid, this.files);

return await this.saveEntry(attachmentsid);
return await this.saveEntry({ attachmentsId: attachmentsid });
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
if (CoreUtils.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it.
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
alfonso-salces marked this conversation as resolved.
Show resolved Hide resolved

return;
}

const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id, forceStorage: true });

return await this.saveEntry({ attachmentsId, forceOffline: true });
} finally {
await loading.dismiss();
}

return;
}

const created = this.entry?.created ?? CoreTimeUtils.timestamp();

try {
if (!this.files.length) {
return await this.saveEntry();
return await this.saveEntry({ created });
}

const attachmentId = await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
await this.saveEntry(attachmentId);
const attachmentsId = await this.uploadOrStoreFiles({ created });
await this.saveEntry({ created, attachmentsId });
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
if (CoreUtils.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it.
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');

return;
}

const attachmentsId = await this.uploadOrStoreFiles({ created, forceStorage: true });

return await this.saveEntry({ attachmentsId, forceOffline: true });
} finally {
await loading.dismiss();
}
}

/**
* Upload or store locally files.
*
* @param param Folder where files will be located.
* @returns folder where files will be located.
*/
async uploadOrStoreFiles(param: AddonBlogEditEntryUploadOrStoreFilesParam): Promise<number | CoreFileUploaderStoreFilesResult> {
if (CoreNetwork.isOnline() && !param.forceStorage) {
return await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
}

const folder = 'entryId' in param ? { id: param.entryId } : { created: param.created };
const folderPath = await AddonBlogOffline.getOfflineEntryFilesFolderPath(folder);

return await CoreFileUploader.storeFilesToUpload(folderPath, this.files);
}

/**
* Expand or collapse associations.
*/
Expand Down Expand Up @@ -336,27 +419,13 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
return true;
}

/**
* Add attachment to options list.
*
* @param attachmentsId Attachment ID.
* @param options Options list.
*/
addAttachments(attachmentsId: number | undefined, options: AddonBlogAddEntryOption[]): void {
if (attachmentsId === undefined) {
return;
}

options.push({ name: 'attachmentsid', value: attachmentsId });
}

/**
* Create or update entry.
*
* @param attachmentsId Attachments.
* @param params Creation date and attachments ID.
* @returns Promise resolved when done.
*/
async saveEntry(attachmentsId?: number): Promise<void> {
async saveEntry(params: AddonBlogEditEntrySaveEntryParams): Promise<void> {
const { summary, subject, publishState } = this.form.value;

if (!summary || !subject || !publishState) {
Expand All @@ -369,11 +438,30 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
{ name: 'modassoc', value: this.form.controls.associateWithModule.value && this.modId ? this.modId : 0 },
];

this.addAttachments(attachmentsId, options);
if (params.attachmentsId) {
options.push({ name: 'attachmentsid', value: params.attachmentsId });
}

this.entry
? await AddonBlog.updateEntry({ subject, summary, summaryformat: 1, options , entryid: this.entry.id })
: await AddonBlog.addEntry({ subject, summary, summaryformat: 1, options });
if (!this.entry?.id) {
await AddonBlog.addEntry({
subject,
summary,
summaryformat: 1,
options,
created: params.created ?? CoreTimeUtils.timestamp(),
forceOffline: params.forceOffline,
});
} else {
await AddonBlog.updateEntry({
subject,
summary,
summaryformat: 1,
options,
forceOffline: params.forceOffline,
entryid: this.entry.id,
created: this.entry.created,
});
}

CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
this.forceLeave = true;
Expand All @@ -382,10 +470,36 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
return CoreNavigator.back();
}

/**
* Retrieves a formatted blog offline entry.
*
* @param params Entry creation date or entry ID.
* @returns Formatted entry.
*/
async getFormattedBlogOfflineEntry(
params: AddonBlogEditGetFormattedBlogOfflineEntryParams,
): Promise<AddonBlogEditEntryFormattedOfflinePost | undefined> {
const entryRecord = await AddonBlogOffline.getOfflineEntry(params);

return entryRecord ? await AddonBlog.formatOfflineEntry(entryRecord) : undefined;
}

}

type AddonBlogEditEntryGetEntryParams = {
entryId: number;
filters?: AddonBlogFilter;
lastModified?: number;
type AddonBlogEditGetFormattedBlogOfflineEntryParams = { id: number } | { created: number };

type AddonBlogEditEntryUploadOrStoreFilesParam = ({ entryId: number } | { created: number }) & { forceStorage?: boolean };

type AddonBlogEditEntryGetEntryParams = { entryId: number; filters?: AddonBlogFilter; lastModified?: number };

type AddonBlogEditEntryPost = Omit<AddonBlogPost, 'id'> & { id?: number };

type AddonBlogEditEntrySaveEntryParams = {
created?: number;
attachmentsId?: number | CoreFileUploaderStoreFilesResult;
forceOffline?: boolean;
};

type AddonBlogEditEntryFormattedOfflinePost = Omit<
AddonBlogEditEntryPost, | 'attachment' | 'attachmentfiles' | 'rating' | 'format' | 'usermodified' | 'module'
> & { attachmentfiles?: CoreFileEntry[] };
Loading