Skip to content

Commit

Permalink
MOBILE-4547 blog: Support offline blog
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonso-salces committed Jul 12, 2024
1 parent a842065 commit 4c0cb67
Show file tree
Hide file tree
Showing 10 changed files with 959 additions and 79 deletions.
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
4 changes: 4 additions & 0 deletions src/addons/blog/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@

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_UNDELETE_ENTRY = 'addon_blog_undelete_entry';
export const ADDON_BLOG_SYNC_ID = 'blog';
121 changes: 100 additions & 21 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,7 +22,9 @@ 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';
Expand All @@ -34,7 +36,9 @@ import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
import { CanLeave } from '@guards/can-leave';
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';
Expand All @@ -53,7 +57,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 Down Expand Up @@ -197,6 +201,17 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
this.loaded = true;
}

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

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

/**
* Retrieves blog entry.
*
Expand Down Expand Up @@ -271,20 +286,36 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {

if (this.entry) {
try {
if (!CoreFileUploader.areFileListDifferent(this.files, this.initialFiles)) {
return await this.saveEntry();
}
await AddonBlogSync.waitForSync(ADDON_BLOG_SYNC_ID).then(async () => {

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

if (removedFiles.length) {
await CoreFileUploader.deleteDraftFiles(attachmentsid, removedFiles);
}
const siteId = CoreSites.getCurrentSiteId();
CoreSync.blockOperation(AddonBlogProvider.COMPONENT, this.entry.created, siteId);

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({});
}

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

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

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

return await this.saveEntry({ attachmentsId: attachmentsid });
});
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error updating entry.');
} finally {
Expand All @@ -295,19 +326,52 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
}

try {
const created = new Date().getTime();

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.');
} 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> {
if (CoreNetwork.isOnline()) {
return await CoreFileUploader.uploadOrReuploadFiles(this.files, this.component);
}

const folder = 'entryId' in param ? param.entryId : param.created;
const folderPath = await AddonBlogOffline.getOfflineEntryFilesFolderPath(folder);
const result = await CoreFileUploader.storeFilesToUpload(folderPath, this.files);

if (!this.entry?.id) {
return folder;
}

const options: AddonBlogAddEntryOption[] = [
{ name: 'publishstate', value: this.form.controls.publishState.value },
{ name: 'courseassoc', value: this.form.controls.associateWithCourse.value && this.courseId ? this.courseId : 0 },
{ name: 'modassoc', value: this.form.controls.associateWithModule.value && this.modId ? this.modId : 0 },
];

this.addAttachments(result.offline, options);
await AddonBlogOffline.updateOfflineEntry({ ...this.entry, options: JSON.stringify(options), summaryformat: 1 });

return folder;
}

/**
* Expand or collapse associations.
*/
Expand Down Expand Up @@ -352,10 +416,10 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {
/**
* 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: { created?: number; attachmentsId?: number }): Promise<void> {
const { summary, subject, publishState } = this.form.value;

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

this.addAttachments(attachmentsId, options);
this.addAttachments(params.attachmentsId, options);

this.entry
? await AddonBlog.updateEntry({ subject, summary, summaryformat: 1, options , entryid: this.entry.id })
: await AddonBlog.addEntry({ subject, summary, summaryformat: 1, options });
? await AddonBlog.updateEntry({
subject,
summary,
summaryformat: 1,
options,
entryid: this.entry.id,
created: this.entry.created,
})
: await AddonBlog.addEntry({
subject,
summary,
summaryformat: 1,
options,
created: params.created ?? new Date().getTime(),
});

CoreEvents.trigger(ADDON_BLOG_ENTRY_UPDATED);
this.forceLeave = true;
Expand All @@ -383,6 +460,8 @@ export class AddonBlogEditEntryPage implements CanLeave, OnInit {

}

type AddonBlogEditEntryUploadOrStoreFilesParam = { entryId: number } | { created: number };

type AddonBlogEditEntryGetEntryParams = {
entryId: number;
filters?: AddonBlogFilter;
Expand Down
44 changes: 33 additions & 11 deletions src/addons/blog/pages/index/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<h1>{{ title | translate }}</h1>
</ion-title>
<ion-buttons slot="end">
<core-context-menu>
<core-context-menu-item [hidden]="!loaded || !hasOffline || !isOnline" [priority]="400"
[content]="'core.settings.synchronizenow' | translate" (action)="refresh($event)" [iconAction]="syncIcon"
[closeOnClick]="false" />
</core-context-menu>
<core-user-menu-button />
</ion-buttons>
</ion-toolbar>
Expand All @@ -27,8 +32,17 @@ <h1>{{ title | translate }}</h1>
</ion-item>
}

@for (entry of entries; track entry.id) {
<div class="entry ion-padding-start ion-padding-top ion-padding-end" [id]="'entry-' + entry.id">
@if (hasEntriesToUpdate) {
<ion-card class="core-warning-card">
<ion-item>
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: 'addon.blog.blog'} }}</ion-label>
</ion-item>
</ion-card>
}

@for (entry of entries; track entry.created) {
<div class="entry ion-padding-start ion-padding-top ion-padding-end" [id]="'entry-' + entry.created">
<div class="entry-subject flex ion-text-wrap ion-justify-content-between ion-align-items-center">
<h3>
<core-format-text [text]="entry.subject" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
Expand All @@ -38,6 +52,15 @@ <h3>
<ion-badge color="warning"> {{ 'addon.blog.publishtonoone' | translate }} </ion-badge>
</span>
}

@if (!getEntryId(entry)) {
<span class="entry-draft">
<ion-badge color="light">
<ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate" />
{{ 'core.notsent' | translate }}
</ion-badge>
</span>
}
</h3>

@if (entry.userid === currentUserId && optionsAvailable) {
Expand All @@ -64,8 +87,8 @@ <h3>

<div class="entry-summary" [collapsible-item]="64">
<div class="ion-margin-bottom">
<core-format-text [text]="entry.summary" [component]="component" [componentId]="entry.id" [contextLevel]="contextLevel"
[contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
<core-format-text [text]="entry.summary" [component]="component" [componentId]="getEntryId(entry) ?? entry.created"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="entry.courseid" />
</div>

@if (tagsEnabled && entry.tags && entry.tags!.length > 0) {
Expand All @@ -77,9 +100,7 @@ <h3>
</ion-item>
}

@for (file of entry.attachmentfiles; track $index) {
<core-file [file]="file" [component]="this.component" [componentId]="entry.id" />
}
<core-files [files]="entry.attachmentfiles" [component]="component" [componentId]="getEntryId(entry) ?? entry.created" />

@if (entry.uniquehash) {
<ion-item [href]="entry.uniquehash" core-link [detail]="true" lines="none">
Expand All @@ -106,8 +127,8 @@ <h3>
</ion-item>
}

@if (commentsEnabled) {
<core-comments [component]="this.component" [itemId]="entry.id" area="format_blog" [instanceId]="entry.userid"
@if (getEntryId(entry) && commentsEnabled) {
<core-comments [component]="this.component" [itemId]="getEntryId(entry)" area="format_blog" [instanceId]="entry.userid"
contextLevel="user" [showItem]="true" [courseId]="entry.courseid" />
}

Expand All @@ -121,12 +142,13 @@ <h3>
</core-loading>

<!-- Create a blog entry. -->
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end"
*ngIf="loaded && optionsAvailable && (!filter.userid || (filter.userid && currentUserId === filter.userid))">
@if ((filter.userid === currentUserId || showMyEntriesToggle) && loaded && optionsAvailable) {
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
<ion-fab-button (click)="createNewEntry()" [attr.aria-label]="'addon.blog.addnewentry' | translate">
<ion-icon name="fas-pen-to-square" aria-hidden="true" />
<span class="sr-only">{{ 'addon.blog.addnewentry' | translate }}</span>
</ion-fab-button>
</ion-fab>
}

</ion-content>
Loading

0 comments on commit 4c0cb67

Please sign in to comment.