Skip to content

Commit 660636d

Browse files
authored
Merge pull request #4043 from alfonso-salces/MOBILE-4547
MOBILE-4547 blog: Support offline blog
2 parents a1c547b + fa32fde commit 660636d

File tree

10 files changed

+1251
-153
lines changed

10 files changed

+1251
-153
lines changed

src/addons/blog/blog.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import { AddonBlogMainMenuHandler } from './services/handlers/mainmenu';
2929
import { AddonBlogTagAreaHandler } from './services/handlers/tag-area';
3030
import { AddonBlogUserHandler } from './services/handlers/user';
3131
import { ADDON_BLOG_MAINMENU_PAGE_NAME } from './constants';
32+
import { CORE_SITE_SCHEMAS } from '@services/sites';
33+
import { BLOG_OFFLINE_SITE_SCHEMA } from './services/database/blog';
34+
import { CoreCronDelegate } from '@services/cron';
35+
import { AddonBlogSyncCronHandler } from './services/handlers/sync-cron';
3236

3337
const routes: Routes = [
3438
{
@@ -44,6 +48,11 @@ const routes: Routes = [
4448
CoreCourseIndexRoutingModule.forChild({ children: routes }),
4549
],
4650
providers: [
51+
{
52+
provide: CORE_SITE_SCHEMAS,
53+
useValue: [BLOG_OFFLINE_SITE_SCHEMA],
54+
multi: true,
55+
},
4756
{
4857
provide: APP_INITIALIZER,
4958
multi: true,
@@ -54,6 +63,7 @@ const routes: Routes = [
5463
CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
5564
CoreTagAreaDelegate.registerHandler(AddonBlogTagAreaHandler.instance);
5665
CoreCourseOptionsDelegate.registerHandler(AddonBlogCourseOptionHandler.instance);
66+
CoreCronDelegate.register(AddonBlogSyncCronHandler.instance);
5767
},
5868
},
5969
],

src/addons/blog/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@
1414

1515
export const ADDON_BLOG_MAINMENU_PAGE_NAME = 'blog';
1616
export const ADDON_BLOG_ENTRY_UPDATED = 'blog_entry_updated';
17+
export const ADDON_BLOG_AUTO_SYNCED = 'addon_blog_autom_synced';
18+
export const ADDON_BLOG_MANUAL_SYNCED = 'addon_blog_manual_synced';
19+
export const ADDON_BLOG_SYNC_ID = 'blog';

src/addons/blog/pages/edit-entry/edit-entry.ts

Lines changed: 163 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414
import { ContextLevel } from '@/core/constants';
1515
import { CoreSharedModule } from '@/core/shared.module';
16-
import { ADDON_BLOG_ENTRY_UPDATED } from '@addons/blog/constants';
16+
import { ADDON_BLOG_ENTRY_UPDATED, ADDON_BLOG_SYNC_ID } from '@addons/blog/constants';
1717
import {
1818
AddonBlog,
1919
AddonBlogAddEntryOption,
@@ -22,26 +22,31 @@ import {
2222
AddonBlogProvider,
2323
AddonBlogPublishState,
2424
} from '@addons/blog/services/blog';
25-
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
25+
import { AddonBlogOffline } from '@addons/blog/services/blog-offline';
26+
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
27+
import { AddonBlogSync } from '@addons/blog/services/blog-sync';
2628
import { FormControl, FormGroup, Validators } from '@angular/forms';
2729
import { CoreError } from '@classes/errors/error';
2830
import { CoreCommentsComponentsModule } from '@features/comments/components/components.module';
2931
import { CoreCourse } from '@features/course/services/course';
3032
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
3133
import { CoreCourseBasicData } from '@features/courses/services/courses';
3234
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
33-
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
35+
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
3436
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
3537
import { CanLeave } from '@guards/can-leave';
3638
import { CoreLoadings } from '@services/loadings';
3739
import { CoreNavigator } from '@services/navigator';
40+
import { CoreNetwork } from '@services/network';
3841
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
42+
import { CoreSync } from '@services/sync';
3943
import { CoreDomUtils } from '@services/utils/dom';
4044
import { CoreUtils } from '@services/utils/utils';
41-
import { CoreWSFile } from '@services/ws';
4245
import { Translate } from '@singletons';
4346
import { CoreEvents } from '@singletons/events';
4447
import { CoreForms } from '@singletons/form';
48+
import { CoreFileEntry } from '@services/file-helper';
49+
import { CoreTimeUtils } from '@services/utils/time';
4550

4651
@Component({
4752
selector: 'addon-blog-edit-entry',
@@ -54,7 +59,7 @@ import { CoreForms } from '@singletons/form';
5459
CoreTagComponentsModule,
5560
],
5661
})
57-
export class AddonBlogEditEntryPage implements CanLeave, OnInit {
62+
export class AddonBlogEditEntryPage implements CanLeave, OnInit, OnDestroy {
5863

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

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

73-
entry?: AddonBlogPost;
78+
entry?: AddonBlogPost | AddonBlogEditEntryFormattedOfflinePost;
7479
loaded = false;
7580
maxFiles = 99;
76-
initialFiles: CoreWSFile[] = [];
77-
files: CoreWSFile[] = [];
81+
initialFiles: CoreFileEntry[] = [];
82+
files: CoreFileEntry[] = [];
7883
courseId?: number;
7984
modId?: number;
8085
userId?: number;
@@ -88,6 +93,7 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
8893
component = AddonBlogProvider.COMPONENT;
8994
siteHomeId?: number;
9095
forceLeave = false;
96+
isOfflineEntry = false;
9197

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

133-
const entryId = CoreNavigator.getRouteNumberParam('id');
139+
const entryId = CoreNavigator.getRouteParam('id');
134140
const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
135141
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
136142
const courseId = CoreNavigator.getRouteNumberParam('courseId');
137143
const cmId = CoreNavigator.getRouteNumberParam('cmId');
138144
this.userId = CoreNavigator.getRouteNumberParam('userId');
139145
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
146+
this.isOfflineEntry = entryId?.startsWith('new-') ?? false;
147+
const entryIdParsed = Number(entryId);
140148

141-
if (!entryId) {
149+
if (entryIdParsed === 0) {
142150
this.loaded = true;
143151

144152
try {
@@ -162,11 +170,27 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
162170
}
163171

164172
try {
165-
this.entry = await this.getEntry({ filters, lastModified, entryId });
166-
this.files = this.entry.attachmentfiles ?? [];
173+
await AddonBlogSync.waitForSync(ADDON_BLOG_SYNC_ID);
174+
175+
if (!this.isOfflineEntry) {
176+
const offlineContent = await this.getFormattedBlogOfflineEntry({ id: entryIdParsed });
177+
this.entry = offlineContent ?? await this.getEntry({ filters, lastModified, entryId: entryIdParsed });
178+
} else {
179+
this.entry = await this.getFormattedBlogOfflineEntry({ created: Number(entryId?.slice(4)) });
180+
181+
if (!this.entry) {
182+
throw new CoreError('This offline entry no longer exists.');
183+
}
184+
}
185+
186+
this.files = [...(this.entry.attachmentfiles ?? [])];
167187
this.initialFiles = [...this.files];
168-
this.courseId = this.courseId || this.entry.courseid;
169-
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;
188+
189+
if (this.entry) {
190+
CoreSync.blockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
191+
this.courseId = this.courseId || this.entry.courseid;
192+
this.modId = CoreNavigator.getRouteNumberParam('cmId') || this.entry.coursemoduleid;
193+
}
170194

171195
if (this.courseId) {
172196
this.form.controls.associateWithCourse.setValue(true);
@@ -198,6 +222,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
198222
this.loaded = true;
199223
}
200224

225+
/**
226+
* @inheritdoc
227+
*/
228+
ngOnDestroy(): void {
229+
if (!this.entry) {
230+
return;
231+
}
232+
233+
CoreSync.unblockOperation(AddonBlogProvider.COMPONENT, this.entry.id ?? this.entry.created);
234+
}
235+
201236
/**
202237
* Retrieves blog entry.
203238
*
@@ -270,45 +305,93 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
270305

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

273-
if (this.entry) {
308+
if (this.entry?.id) {
274309
try {
310+
if (!CoreNetwork.isOnline()) {
311+
const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id });
312+
313+
return await this.saveEntry({ attachmentsId });
314+
}
315+
275316
if (!CoreFileUploader.areFileListDifferent(this.files, this.initialFiles)) {
276-
return await this.saveEntry();
317+
return await this.saveEntry({});
277318
}
278319

279320
const { attachmentsid } = await AddonBlog.prepareEntryForEdition({ entryid: this.entry.id });
280-
const removedFiles = CoreFileUploader.getFilesToDelete(this.initialFiles, this.files);
321+
322+
const lastModified = CoreNavigator.getRouteNumberParam('lastModified');
323+
const filters: AddonBlogFilter | undefined = CoreNavigator.getRouteParam('filters');
324+
const entry = this.entry && 'attachment' in this.entry
325+
? this.entry
326+
: await CoreUtils.ignoreErrors(this.getEntry({ filters, lastModified, entryId: this.entry.id }));
327+
328+
const removedFiles = CoreFileUploader.getFilesToDelete(entry?.attachmentfiles ?? [], this.files);
281329

282330
if (removedFiles.length) {
283331
await CoreFileUploader.deleteDraftFiles(attachmentsid, removedFiles);
284332
}
285333

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

288-
return await this.saveEntry(attachmentsid);
336+
return await this.saveEntry({ attachmentsId: attachmentsid });
289337
} catch (error) {
290-
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
338+
if (CoreUtils.isWebServiceError(error)) {
339+
// It's a WebService error, the user cannot send the message so don't store it.
340+
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
341+
342+
return;
343+
}
344+
345+
const attachmentsId = await this.uploadOrStoreFiles({ entryId: this.entry.id, forceStorage: true });
346+
347+
return await this.saveEntry({ attachmentsId, forceOffline: true });
291348
} finally {
292349
await loading.dismiss();
293350
}
294-
295-
return;
296351
}
297352

353+
const created = this.entry?.created ?? CoreTimeUtils.timestamp();
354+
298355
try {
299356
if (!this.files.length) {
300-
return await this.saveEntry();
357+
return await this.saveEntry({ created });
301358
}
302359

303-
const attachmentId = await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
304-
await this.saveEntry(attachmentId);
360+
const attachmentsId = await this.uploadOrStoreFiles({ created });
361+
await this.saveEntry({ created, attachmentsId });
305362
} catch (error) {
306-
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
363+
if (CoreUtils.isWebServiceError(error)) {
364+
// It's a WebService error, the user cannot send the message so don't store it.
365+
CoreDomUtils.showErrorModalDefault(error, 'Error creating entry.');
366+
367+
return;
368+
}
369+
370+
const attachmentsId = await this.uploadOrStoreFiles({ created, forceStorage: true });
371+
372+
return await this.saveEntry({ attachmentsId, forceOffline: true });
307373
} finally {
308374
await loading.dismiss();
309375
}
310376
}
311377

378+
/**
379+
* Upload or store locally files.
380+
*
381+
* @param param Folder where files will be located.
382+
* @returns folder where files will be located.
383+
*/
384+
async uploadOrStoreFiles(param: AddonBlogEditEntryUploadOrStoreFilesParam): Promise<number | CoreFileUploaderStoreFilesResult> {
385+
if (CoreNetwork.isOnline() && !param.forceStorage) {
386+
return await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
387+
}
388+
389+
const folder = 'entryId' in param ? { id: param.entryId } : { created: param.created };
390+
const folderPath = await AddonBlogOffline.getOfflineEntryFilesFolderPath(folder);
391+
392+
return await CoreFileUploader.storeFilesToUpload(folderPath, this.files);
393+
}
394+
312395
/**
313396
* Expand or collapse associations.
314397
*/
@@ -336,27 +419,13 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
336419
return true;
337420
}
338421

339-
/**
340-
* Add attachment to options list.
341-
*
342-
* @param attachmentsId Attachment ID.
343-
* @param options Options list.
344-
*/
345-
addAttachments(attachmentsId: number | undefined, options: AddonBlogAddEntryOption[]): void {
346-
if (attachmentsId === undefined) {
347-
return;
348-
}
349-
350-
options.push({ name: 'attachmentsid', value: attachmentsId });
351-
}
352-
353422
/**
354423
* Create or update entry.
355424
*
356-
* @param attachmentsId Attachments.
425+
* @param params Creation date and attachments ID.
357426
* @returns Promise resolved when done.
358427
*/
359-
async saveEntry(attachmentsId?: number): Promise<void> {
428+
async saveEntry(params: AddonBlogEditEntrySaveEntryParams): Promise<void> {
360429
const { summary, subject, publishState } = this.form.value;
361430

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

372-
this.addAttachments(attachmentsId, options);
441+
if (params.attachmentsId) {
442+
options.push({ name: 'attachmentsid', value: params.attachmentsId });
443+
}
373444

374-
this.entry
375-
? await AddonBlog.updateEntry({ subject, summary, summaryformat: 1, options , entryid: this.entry.id })
376-
: await AddonBlog.addEntry({ subject, summary, summaryformat: 1, options });
445+
if (!this.entry?.id) {
446+
await AddonBlog.addEntry({
447+
subject,
448+
summary,
449+
summaryformat: 1,
450+
options,
451+
created: params.created ?? CoreTimeUtils.timestamp(),
452+
forceOffline: params.forceOffline,
453+
});
454+
} else {
455+
await AddonBlog.updateEntry({
456+
subject,
457+
summary,
458+
summaryformat: 1,
459+
options,
460+
forceOffline: params.forceOffline,
461+
entryid: this.entry.id,
462+
created: this.entry.created,
463+
});
464+
}
377465

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

473+
/**
474+
* Retrieves a formatted blog offline entry.
475+
*
476+
* @param params Entry creation date or entry ID.
477+
* @returns Formatted entry.
478+
*/
479+
async getFormattedBlogOfflineEntry(
480+
params: AddonBlogEditGetFormattedBlogOfflineEntryParams,
481+
): Promise<AddonBlogEditEntryFormattedOfflinePost | undefined> {
482+
const entryRecord = await AddonBlogOffline.getOfflineEntry(params);
483+
484+
return entryRecord ? await AddonBlog.formatOfflineEntry(entryRecord) : undefined;
485+
}
486+
385487
}
386488

387-
type AddonBlogEditEntryGetEntryParams = {
388-
entryId: number;
389-
filters?: AddonBlogFilter;
390-
lastModified?: number;
489+
type AddonBlogEditGetFormattedBlogOfflineEntryParams = { id: number } | { created: number };
490+
491+
type AddonBlogEditEntryUploadOrStoreFilesParam = ({ entryId: number } | { created: number }) & { forceStorage?: boolean };
492+
493+
type AddonBlogEditEntryGetEntryParams = { entryId: number; filters?: AddonBlogFilter; lastModified?: number };
494+
495+
type AddonBlogEditEntryPost = Omit<AddonBlogPost, 'id'> & { id?: number };
496+
497+
type AddonBlogEditEntrySaveEntryParams = {
498+
created?: number;
499+
attachmentsId?: number | CoreFileUploaderStoreFilesResult;
500+
forceOffline?: boolean;
391501
};
502+
503+
type AddonBlogEditEntryFormattedOfflinePost = Omit<
504+
AddonBlogEditEntryPost, | 'attachment' | 'attachmentfiles' | 'rating' | 'format' | 'usermodified' | 'module'
505+
> & { attachmentfiles?: CoreFileEntry[] };

0 commit comments

Comments
 (0)