From 463a930e0949dd0d2cc634f2389c8e06226ad253 Mon Sep 17 00:00:00 2001
From: Amitosh Swain Mahapatra <amitosh.swain@gmail.com>
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 @@
 <ion-header no-border>
   <ion-navbar>
     <ion-title>
-      <img src="./assets/img/Fedora.svg" height="24px" alt="Fedora">
+      <img src="./assets/img/Fedora.svg" height="24px" alt="Fedora" />
     </ion-title>
     <ion-buttons end>
       <button tappable (click)="openNotificationPage()">
-        <img src="./assets/img/notification.svg">
+        <img src="./assets/img/notification.svg" />
       </button>
     </ion-buttons>
   </ion-navbar>
@@ -16,24 +16,44 @@
     Latest Articles from Magazine
     <button (click)="presentActionSheet()">
       Sort
-      <img src='./assets/img/sort.svg'>
+      <img src="./assets/img/sort.svg" />
     </button>
   </ion-label>
   <ion-list>
-    <ion-card *ngFor="let post of posts let i = index">
+    <ion-card
+      class="post"
+      *ngFor="let post of posts; let i = index"
+      [ngClass]="{ new: post.newPost }"
+    >
       <ion-row>
         <ion-col col-7>
-          <ion-card-header [innerHTML]="post.title" class="body-title" text-wrap tappable (click)="openPost(post)"></ion-card-header>
+          <ion-card-header
+            [innerHTML]="post.title"
+            class="body-title"
+            text-wrap
+            tappable
+            (click)="openPost(post)"
+          ></ion-card-header>
           <ion-card-content>
-            <span class="body-subtitle">{{ beautify(post.publishedAt) }} — {{ post.comments }} comments</span>
-            <img [src]=bookmarkIcon[i] tappable (click)="addToBookmark(post,i)" class="bookmark">
+            <span class="body-subtitle"
+              >{{ beautify(post.publishedAt) }} —
+              {{ post.comments }} comments</span
+            >
+            <img
+              [src]="bookmarkIcon[i]"
+              tappable
+              (click)="addToBookmark(post, i)"
+              class="bookmark"
+            />
           </ion-card-content>
         </ion-col>
         <ion-col col-5 tappable (click)="openPost(post)">
-          <img src="{{post.featuredImage}}" class="card-image">
+          <img src="{{ post.featuredImage }}" class="card-image" />
         </ion-col>
       </ion-row>
     </ion-card>
   </ion-list>
-  <button (click)="loadMore()" class="load" ng-if="posts.length!==0">Load More Posts</button>
+  <button (click)="loadMore()" class="load" ng-if="posts.length!==0">
+    Load More Posts
+  </button>
 </ion-content>
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<Date> {
+    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<BlogPost[]> {
+  public fetchPosts(
+    funcParams: BlogPostFetchArgs = {}
+  ): Observable<BlogPost[]> {
     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<BlogPost[]> {
-    return this.fetchPosts()
-      .pipe(
-        tap(
-          async posts => {
-            // Store data in IndexedDB
-            try {
-              const table = this.db.table<BlogPost>(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<BlogPost>(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<BlogPost[]> {
     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<string, Blog>;
 
-  private blogs: Map<string, Blog>
-
-  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);

