From 463a930e0949dd0d2cc634f2389c8e06226ad253 Mon Sep 17 00:00:00 2001 From: Amitosh Swain Mahapatra Date: Mar 17 2019 06:10:44 +0000 Subject: Identify new blog posts since last sync --- diff --git a/src/pages/magazine/magazine.html b/src/pages/magazine/magazine.html index a0c6a43..79f4bb6 100644 --- a/src/pages/magazine/magazine.html +++ b/src/pages/magazine/magazine.html @@ -1,11 +1,11 @@ - Fedora + Fedora @@ -16,24 +16,44 @@ Latest Articles from Magazine - + - + - {{ beautify(post.publishedAt) }} — {{ post.comments }} comments - + {{ beautify(post.publishedAt) }} — + {{ post.comments }} comments + - + - + diff --git a/src/pages/magazine/magazine.scss b/src/pages/magazine/magazine.scss index c7f9616..dcdf97a 100644 --- a/src/pages/magazine/magazine.scss +++ b/src/pages/magazine/magazine.scss @@ -9,6 +9,13 @@ ion-card-header { padding: 0 !important; } + + &.new { + .body-title { + font-weight: bold; + } + } + ion-card-content { margin-top: 4px; padding: 0 !important; diff --git a/src/pages/magazine/magazine.ts b/src/pages/magazine/magazine.ts index f0c016a..24da6a2 100644 --- a/src/pages/magazine/magazine.ts +++ b/src/pages/magazine/magazine.ts @@ -40,9 +40,9 @@ export class MagazinePage { private blog: Blog; - private loading: boolean = false; + loading: boolean = false; - private error: boolean = false; + error: boolean = false; constructor(private browser: Browser, private navCtrl: NavController, diff --git a/src/providers/word-press-blog/word-press-blog.ts b/src/providers/word-press-blog/word-press-blog.ts index d6251aa..78bcbf2 100644 --- a/src/providers/word-press-blog/word-press-blog.ts +++ b/src/providers/word-press-blog/word-press-blog.ts @@ -4,7 +4,9 @@ import { map, tap } from 'rxjs/operators'; import { DatabaseProvider } from '../database/database'; import { Observable } from 'rxjs/Observable'; import { from } from 'rxjs/observable/from'; +import { forkJoin } from 'rxjs/observable/forkJoin'; import { chooseEndpoint } from '../../utils'; +import { Storage } from '@ionic/storage'; // Blog definitions export const BLOG = Object.seal({ @@ -18,7 +20,10 @@ export const BLOG = Object.seal({ id: 'communityBlog', title: 'Fedora Community Blog', originalEndpoint: 'https://communityblog.fedoraproject.org', - endpoint: chooseEndpoint('/community-blog', 'https://communityblog.fedoraproject.org') + endpoint: chooseEndpoint( + '/community-blog', + 'https://communityblog.fedoraproject.org' + ) } }); @@ -29,68 +34,91 @@ export interface BlogPost { /** * Unique ID of the post, supplied by the CMS */ - id: number, + id: number; /** * A sluggified link to the post */ - link: string, + link: string; /** * Permalink to the post */ - permalink: string, + permalink: string; /** * Post title */ - title: string, + title: string; /** * A short excerpt of the post */ - excerpt: string, + excerpt: string; /** * The content of the post */ - content: string, + content: string; /** * Time of publication */ - publishedAt: Date + publishedAt: Date; /** * Number of comments on the article */ - comments: number, + comments: number; /** * Featured Image of article */ - featuredImage: string + featuredImage: string; } interface BlogPostFetchArgs { - page?: number, - orderBy?: 'date' | 'title', - order?: 'asc' | 'desc', - categories?: string[], - tags?: string[] + page?: number; + orderBy?: 'date' | 'title'; + order?: 'asc' | 'desc'; + categories?: string[]; + tags?: string[]; } /** * Represents a single wordpress blog */ export class Blog { - public postsPerPage = 10; - public readonly posts$; + private lastUpdatedStorageKey = ''; + + constructor( + public readonly id: string, + public readonly endpoint: string, + private http: HttpClient, + private db: DatabaseProvider, + private storage: Storage + ) { + this.lastUpdatedStorageKey = `wordpress_blog__${id}__last_updated`; + } + + /** + * Returns a promise yielding a date when the blog was last updated. + * + * The last updated time is 1st January 1970, if the blog is fetched for the + * first time. + * + * @return Promise which yields the last updated date and time + */ + lastUpdated(): Promise { + return this.storage + .get(this.lastUpdatedStorageKey) + .then(d => (d ? d : new Date(0))); + } - constructor(public readonly id: string, public readonly endpoint: string, - private http: HttpClient, private db: DatabaseProvider) { + private setLastUpdated(time: Date) { + return this.storage.set(this.lastUpdatedStorageKey, time); } /** @@ -98,7 +126,9 @@ export class Blog { * * @returns Observable which emits an array of posts */ - public fetchPosts(funcParams: BlogPostFetchArgs = {}): Observable { + public fetchPosts( + funcParams: BlogPostFetchArgs = {} + ): Observable { const { page = 1, orderBy = 'date', @@ -112,21 +142,31 @@ export class Blog { page, orderby: orderBy, order, - per_page: this.postsPerPage, - } + per_page: this.postsPerPage + }; if (categories.length > 0) { - params.categories = categories.join(',') + params.categories = categories.join(','); } if (tags.length > 0) { - params.tags = tags.join(',') + params.tags = tags.join(','); } - return this.http.get(`${this.endpoint}/wp-json/wp/v2/posts`, { params }) - .pipe( - map((data: any[]) => data.map((post: any) => { - const featuredMedia = post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0]; + return forkJoin( + this.http.get(`${this.endpoint}/wp-json/wp/v2/posts`, { params }), + this.lastUpdated() + ).pipe( + map(([data, lastUpdated]: [any[], Date]) => + data.map((post: any) => { + const publishedAt = new Date(post.date_gmt + 'Z'); + + // We can do lt,gt comparisons on Date objects, but not a === comparison + const newPost = lastUpdated <= publishedAt; + + const featuredMedia = + post._embedded['wp:featuredmedia'] && + post._embedded['wp:featuredmedia'][0]; return { id: post.id as number, link: post.link as string, @@ -135,14 +175,19 @@ export class Blog { excerpt: post.excerpt.rendered as string, content: post.content.rendered as string, publishedAt: new Date(post.date_gmt + 'Z'), - featuredImage: featuredMedia && ( - featuredMedia.media_details.sizes.medium ? - featuredMedia.media_details.sizes.medium.source_url : - featuredMedia.media_details.source_url - ), - comments: post._embedded['replies'] !== undefined ? post._embedded['replies']['0'].length : 0, + newPost, + featuredImage: + featuredMedia && + (featuredMedia.media_details.sizes.medium + ? featuredMedia.media_details.sizes.medium.source_url + : featuredMedia.media_details.source_url), + comments: + post._embedded['replies'] !== undefined + ? post._embedded['replies']['0'].length + : 0 }; - })), + }) + ) ); } @@ -152,29 +197,34 @@ export class Blog { * @returns Observable which emits the updated list of posts */ public updatePosts(): Observable { - return this.fetchPosts() - .pipe( - tap( - async posts => { - // Store data in IndexedDB - try { - const table = this.db.table(this.id); - await this.db.transaction('rw', table, async () => { - // We perform an upsert using bulkPut - await table.bulkPut(posts); - const count = await table.count(); - if (count > this.postsPerPage) { - // Delete old posts - await table.orderBy('publishedAt').reverse().offset(this.postsPerPage).delete(); - } - }); - } catch (error) { - // We ignore this error, but log it for debugging - console.error('Unable to persist posts'); - console.error(error); - } - }) - ); + return this.fetchPosts().pipe( + tap(async posts => { + // Store data in IndexedDB + try { + const table = this.db.table(this.id); + await Promise.all([ + this.db.transaction('rw', table, async () => { + // We perform an upsert using bulkPut + await table.bulkPut(posts); + const count = await table.count(); + if (count > this.postsPerPage) { + // Delete old posts + await table + .orderBy('publishedAt') + .reverse() + .offset(this.postsPerPage) + .delete(); + } + }), + this.setLastUpdated(new Date()) + ]); + } catch (error) { + // We ignore this error, but log it for debugging + console.error('Unable to persist posts'); + console.error(error); + } + }) + ); } /** @@ -184,8 +234,11 @@ export class Blog { */ public loadCachedPosts(): Observable { return from( - this.db.table(this.id) - .orderBy('publishedAt').reverse().toArray() + this.db + .table(this.id) + .orderBy('publishedAt') + .reverse() + .toArray() ); } } @@ -195,10 +248,13 @@ export class Blog { */ @Injectable() export class WordPressBlogProvider { + private blogs: Map; - private blogs: Map - - constructor(private http: HttpClient, private db: DatabaseProvider) { + constructor( + private http: HttpClient, + private db: DatabaseProvider, + private storage: Storage + ) { this.blogs = new Map(); } @@ -207,10 +263,12 @@ export class WordPressBlogProvider { * * @param blog A blog definition */ - getBlog(blog: { id: string, endpoint: string }) { - + getBlog(blog: { id: string; endpoint: string }) { if (!this.blogs.has(blog.id)) { - this.blogs.set(blog.id, new Blog(blog.id, blog.endpoint, this.http, this.db)); + this.blogs.set( + blog.id, + new Blog(blog.id, blog.endpoint, this.http, this.db, this.storage) + ); } return this.blogs.get(blog.id);