From 80e88f1e8c173f09c2df0f2869775d620f237bc5 Mon Sep 17 00:00:00 2001 From: Amitosh Swain Mahapatra Date: Mar 17 2019 05:38:15 +0000 Subject: Improve offline caching for WordPress Blog Provider using IndexedDB --- diff --git a/ionic.config.json b/ionic.config.json index 67d9cf5..a3d88e6 100644 --- a/ionic.config.json +++ b/ionic.config.json @@ -14,7 +14,7 @@ }, { "path": "/fedora-magazine", - "proxyUrl": "https://fedoramagazine.org/wp-json/wp/v2/" + "proxyUrl": "https://fedoramagazine.org" }, { "path": "/ask-fedora", @@ -22,7 +22,7 @@ }, { "path": "/community-blog", - "proxyUrl": "https://communityblog.fedoraproject.org/wp-json/wp/v2/" + "proxyUrl": "https://communityblog.fedoraproject.org" }, { "path": "/podcast", diff --git a/package-lock.json b/package-lock.json index 68e2ce4..135c2d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,14 @@ "source-map": "^0.5.6", "typescript": "~2.6.1", "webpack-sources": "^1.0.1" + }, + "dependencies": { + "typescript": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", + "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", + "dev": true + } } }, "@angular/common": { @@ -744,15 +752,15 @@ "dev": true }, "@types/lodash": { - "version": "4.14.123", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.123.tgz", - "integrity": "sha512-pQvPkc4Nltyx7G1Ww45OjVqUsJP4UsZm+GWJpigXgkikZqJgRm4c48g027o6tdgubWHwFRF15iFd+Y4Pmqv6+Q==", + "version": "4.14.121", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.121.tgz", + "integrity": "sha512-ORj7IBWj13iYufXt/VXrCNMbUuCTJfhzme5kx9U/UtcIPdJYuvPDUAlHlbNhz/8lKCLy9XGIZnGrqXOtQbPGoQ==", "dev": true }, "@types/lodash-es": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.3.tgz", - "integrity": "sha512-iHI0i7ZAL1qepz1Y7f3EKg/zUMDwDfTzitx+AlHhJJvXwenP682ZyGbgPSc5Ej3eEAKVbNWKFuwOadCj5vBbYQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.2.tgz", + "integrity": "sha512-Bpl6bh7kF9IdepJEfSd6g/E6OPq99jcIQP5g0xqv47P1FjLOP1+PENCy2vWvrzY+KwnnHwGAbUcTxbEr+WAnUw==", "dev": true, "requires": { "@types/lodash": "*" @@ -3291,6 +3299,11 @@ "repeating": "^2.0.0" } }, + "dexie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-2.0.4.tgz", + "integrity": "sha512-aQ/s1U2wHxwBKRrt2Z/mwFNHMQWhESerFsMYzE+5P5OsIe5o1kgpFMWkzKTtkvkyyEni6mWr/T4HUJuY9xIHLA==" + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -3679,8 +3692,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escope": { "version": "3.6.0", @@ -4013,6 +4025,30 @@ "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", "dev": true }, + "filename-reserved-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", + "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=" + }, + "filenamify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", + "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=", + "requires": { + "filename-reserved-regex": "^1.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "filenamify-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz", + "integrity": "sha1-syvYExnvWGO3MHi+1Q9GpPeXX1A=", + "requires": { + "filenamify": "^1.0.0", + "humanize-url": "^1.0.0" + } + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -5214,6 +5250,15 @@ } } }, + "humanize-url": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz", + "integrity": "sha1-9KuZ4NKIF0yk4eUEB8VfuuRk7/8=", + "requires": { + "normalize-url": "^1.0.0", + "strip-url-auth": "^1.0.0" + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -5493,6 +5538,11 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -7251,6 +7301,17 @@ "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", "dev": true }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -7752,6 +7813,11 @@ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -9034,6 +9100,14 @@ } } }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -9337,6 +9411,19 @@ "get-stdin": "^4.0.1" } }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, + "strip-url-auth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz", + "integrity": "sha1-IrD6OkE4WzO+PzMVUbu4N/oM164=" + }, "supports-color": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", @@ -9522,6 +9609,14 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, + "trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "requires": { + "escape-string-regexp": "^1.0.2" + } + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", diff --git a/package.json b/package.json index e5177f9..15ed947 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,9 @@ "cordova-plugin-whitelist": "^1.3.3", "cordova-plugin-x-socialsharing": "^5.4.3", "cordova-plugin-x-toast": "^2.7.2", + "dexie": "^2.0.4", "es6-promise-plugin": "^4.2.2", + "filenamify-url": "^1.0.0", "ionic-angular": "^3.9.4", "ionic-plugin-keyboard": "^2.2.1", "ionicons": "^3.0.0", @@ -74,7 +76,8 @@ "@angular/compiler-cli": "^5.2.11", "@ionic/app-scripts": "^3.2.0", "@types/jasmine": "ts2.6", - "@types/lodash-es": "^4.17.3", + "@types/lodash-es": "ts2.6", + "@types/lodash": "ts2.6", "@types/node": "^10.12.10", "angular2-template-loader": "^0.6.2", "html-loader": "^0.5.5", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1a1d5ea..6d783ae 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -26,6 +26,7 @@ import { NoResultsComponent } from '../components/no-results/no-results'; import { NoInternetComponent } from '../components/no-internet/no-internet'; import { Browser } from '../providers/browser/browser'; +import { DatabaseProvider } from '../providers/database/database'; @NgModule({ declarations: [ @@ -87,6 +88,7 @@ import { Browser } from '../providers/browser/browser'; StatusBar, Toast, { provide: ErrorHandler, useClass: IonicErrorHandler }, + DatabaseProvider, ] }) export class AppModule { } diff --git a/src/assets/img/logo.svg b/src/assets/img/logo.svg new file mode 100644 index 0000000..2c9e7a5 --- /dev/null +++ b/src/assets/img/logo.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/pages/home/home.html b/src/pages/home/home.html index 8cf028a..f82c0c4 100644 --- a/src/pages/home/home.html +++ b/src/pages/home/home.html @@ -14,8 +14,8 @@ - - + + @@ -32,24 +32,25 @@
- - + + - + + {{ beautifyDateBlog(blogpost.publishedAt) }} - - + +
Fedora - {{update.date}} + {{ beautifyDateSocial(update.date) }}
diff --git a/src/pages/home/home.ts b/src/pages/home/home.ts index 506d898..09e36a9 100644 --- a/src/pages/home/home.ts +++ b/src/pages/home/home.ts @@ -1,55 +1,71 @@ import { Component } from '@angular/core'; import { NavController, ToastController } from 'ionic-angular'; -import { NotificationsPage } from '../../pages/notifications/notifications' +import { NotificationsPage } from '../../pages/notifications/notifications'; import { Browser } from '../../providers/browser/browser'; import { FacebookProvider } from '../../providers/social/facebook'; import { TwitterProvider } from '../../providers/social/twitter'; import { SocialMediaPost } from '../../providers/social/social'; import { combineLatest } from 'rxjs/observable/combineLatest'; -import { FedoraMagazineService, Post, Image } from '../../providers/fedora-magazine/fedora-magazine'; -import { CommunityBlogService } from '../../providers/community-blog/community-blog'; +import { merge } from 'rxjs/observable/merge'; +import { + WordPressBlogProvider, + BlogPost, + Blog, + BLOG +} from '../../providers/word-press-blog/word-press-blog'; +import { CarouselProvider, Image } from '../../providers/carousel/carousel'; +import { beautifyDate } from '../../utils'; const HANDLE = Object.seal({ FB: 'TheFedoraProject', - TWITTER: 'fedora', + TWITTER: 'fedora' }); @Component({ selector: 'page-home', templateUrl: 'home.html', - providers: [FacebookProvider, TwitterProvider, FedoraMagazineService, CommunityBlogService] + providers: [ + FacebookProvider, + TwitterProvider, + CarouselProvider, + WordPressBlogProvider + ] }) - export class HomePage { /** * Set the currently active segment on Home Page as Community Blog */ - latestActive: String = "blog"; + latestActive: String = 'blog'; /** * List of Facebook and Twitter Posts */ - socialposts: SocialMediaPost[]; + socialPosts: SocialMediaPost[] = []; /** * List of images fetched from FedoraMag showing top 5 latest events */ - carousel: Image[]; + carousel: Image[] = []; /** * List of posts from Fedora Community Blog */ - blogposts: Post[]; - - constructor(public navCtrl: NavController, private browser: Browser, - private fb: FacebookProvider, - private twitter: TwitterProvider, - private fedoraMag: FedoraMagazineService, - private communityblog: CommunityBlogService, - private toast: ToastController) { - this.socialposts = []; - this.blogposts = []; - this.carousel = []; + blogPosts: BlogPost[] = []; + + private communityBlog: Blog; + + loadingCommunityBlog = false; + + constructor( + public navCtrl: NavController, + private browser: Browser, + private fb: FacebookProvider, + private twitter: TwitterProvider, + private toast: ToastController, + private carouselProvider: CarouselProvider, + wordpress: WordPressBlogProvider + ) { + this.communityBlog = wordpress.getBlog(BLOG.communityBlog); } /** @@ -58,44 +74,68 @@ export class HomePage { * Currently, we fetch posts from Facebook and Twitter */ private updateSocialMedia(): void { - combineLatest(this.fb.getPosts(HANDLE.FB), this.twitter.getPosts(HANDLE.TWITTER)) - .subscribe(values => { - this.socialposts = [...values[0], ...values[1]] as SocialMediaPost[]; - }, () => this.toast.create({ - message: 'Could not load feed', - position: 'bottom', - duration: 2000, - }).present() - ); + combineLatest( + this.fb.getPosts(HANDLE.FB), + this.twitter.getPosts(HANDLE.TWITTER) + ).subscribe( + values => { + this.socialPosts = [...values[0], ...values[1]] as SocialMediaPost[]; + }, + () => + this.toast + .create({ + message: 'Could not load feed', + position: 'bottom', + duration: 2000 + }) + .present() + ); } /** * Fetch Latest 5 Images from 'Latest from Fedora Section' of Magazine for carousel */ private updateImages(): void { - this.fedoraMag.getImages() - .subscribe(images => { + this.carouselProvider.getImages().subscribe( + images => { this.carousel = images; - }, () => this.toast.create({ - message: 'Could not update header', - position: 'bottom', - duration: 2000, - }).present() + }, + () => + this.toast + .create({ + message: 'Could not update header', + position: 'bottom', + duration: 2000 + }) + .present() ); } /** - * Fetch Blog Posts from the Fedora Community Blog + * Fetch latest posts from Fedor Magazine API */ - private updateBlogPosts(): void { - this.communityblog.getBlogPosts('null') - .subscribe(blogposts => { - this.blogposts = blogposts; - }, () => this.toast.create({ - message: 'Could not load blog posts', - position: 'bottom', - duration: 2000, - }).present() + updateBlogPosts(): void { + this.loadingCommunityBlog = true; + + merge( + this.communityBlog.loadCachedPosts(), + this.communityBlog.updatePosts() + ).subscribe( + posts => { + this.blogPosts = posts; + }, + () => { + this.toast + .create({ + message: 'Could not load blog posts', + position: 'bottom', + duration: 2000 + }) + .present(); + }, + () => { + this.loadingCommunityBlog = false; + } ); } @@ -106,7 +146,7 @@ export class HomePage { } /** - * Opens a update in a browser + * Opens an update in a browser * * Opens the update in an in-app browser in mobile app and in a new tab on desktop. * @@ -120,7 +160,13 @@ export class HomePage { * Open the notifications pane from the home page */ openNotificationPage() { - this.navCtrl.push(NotificationsPage, { animate: true, direction: 'forward' }); + this.navCtrl.push(NotificationsPage, { + animate: true, + direction: 'forward' + }); } + beautifyDateSocial = x => beautifyDate(x, 'social'); + + beautifyDateBlog = x => beautifyDate(x, 'blog'); } diff --git a/src/pages/magazine/magazine.html b/src/pages/magazine/magazine.html index e384403..a0c6a43 100644 --- a/src/pages/magazine/magazine.html +++ b/src/pages/magazine/magazine.html @@ -12,8 +12,10 @@ - Latest Articles from Magazine - @@ -23,7 +25,7 @@ - + {{ beautify(post.publishedAt) }} — {{ post.comments }} comments diff --git a/src/pages/magazine/magazine.ts b/src/pages/magazine/magazine.ts index 2023c32..f0c016a 100644 --- a/src/pages/magazine/magazine.ts +++ b/src/pages/magazine/magazine.ts @@ -1,29 +1,33 @@ import { Component } from '@angular/core'; import { Browser } from '../../providers/browser/browser'; -import { FedoraMagazineService, Post } from '../../providers/fedora-magazine/fedora-magazine'; import { NotificationsPage } from '../../pages/notifications/notifications'; import { NavController, ActionSheetController, ToastController } from 'ionic-angular'; +import { WordPressBlogProvider, Blog, BlogPost, BLOG } from '../../providers/word-press-blog/word-press-blog'; +import { merge } from 'rxjs/observable/merge'; +import { beautifyDate } from '../../utils'; /** * Shows latest posts from Fedora Magazine */ @Component({ templateUrl: 'magazine.html', - providers: [FedoraMagazineService], + providers: [WordPressBlogProvider], }) export class MagazinePage { /** * List of posts from Fedora Magazine */ - private posts: Post[]; + posts: BlogPost[]; /** * Number of posts */ postCount = 15; + currentPage = 1; + //Initialize array for showing calendar icon bookmarkIcon: string[] = []; @@ -34,8 +38,18 @@ export class MagazinePage { //currently Active Sort Parameter sortBy: string = 'latest'; + private blog: Blog; + + private loading: boolean = false; + + private error: boolean = false; + constructor(private browser: Browser, - private fedoraMag: FedoraMagazineService, public navCtrl: NavController, public actionSheetCtrl: ActionSheetController, public toastCtrl: ToastController) { + private navCtrl: NavController, + private actionSheetCtrl: ActionSheetController, + private toastCtrl: ToastController, + wordpress: WordPressBlogProvider) { + this.blog = wordpress.getBlog(BLOG.fedoraMagazine); for (let i = 0; i < 50; i++) { //update src of icon this.bookmarkIcon[i] = this.inactiveIcon; @@ -43,16 +57,25 @@ export class MagazinePage { } ngOnInit() { - this.updatePosts(this.postCount); + this.updateCachedPosts(); } /** - * Fetch latest posts from Fedor Magazine API + * Fetch latest posts from Fedora Magazine API */ - updatePosts(postCount): void { - this.fedoraMag.getPosts(postCount) + + updateCachedPosts(): void { + this.loading = true; + this.error = false; + + merge(this.blog.loadCachedPosts(), this.blog.updatePosts()) .subscribe(posts => { this.posts = posts; + }, error => { + this.error = true; + console.error(error); + }, () => { + this.loading = false; }); } @@ -61,7 +84,8 @@ export class MagazinePage { */ loadMore(postCount): void { this.postCount += 5; - this.updatePosts(this.postCount); + this.blog.fetchPosts({ page: ++this.currentPage }) + .subscribe(posts => this.posts.splice(this.posts.length, 0, ...posts)); } /** @@ -71,14 +95,13 @@ export class MagazinePage { * * @param post post to open */ - openPost(post: Post): void { + openPost(post: BlogPost): void { this.browser.open(post.link); } /** * Create an action sheet to sort the articles */ - presentActionSheet() { let actionSheet = this.actionSheetCtrl.create({ title: 'SORT ARTICLES BY', @@ -143,9 +166,9 @@ export class MagazinePage { } /** - * Function called when someone taps the star to subscribe to the calendar + * Function called when someone taps the star to bookmark the blog post */ - addToBookmark(post: Post, i: number): void { + addToBookmark(post: BlogPost, i: number): void { /** * Declare toasts for showing events */ @@ -169,4 +192,9 @@ export class MagazinePage { unsubscribedToast.present(); } } + + beautify(date:Date) { + return beautifyDate(date, 'blog'); + } + } diff --git a/src/pages/more/more.ts b/src/pages/more/more.ts index eeed3b6..f10039b 100755 --- a/src/pages/more/more.ts +++ b/src/pages/more/more.ts @@ -3,8 +3,7 @@ import { NavController, NavParams } from 'ionic-angular'; import { NotificationsPage } from '../../pages/notifications/notifications'; import { PackageSearchPage } from '../package-search/package-search'; import { Browser } from '../../providers/browser/browser'; -import { Post } from '../../providers/fedora-magazine/fedora-magazine'; -import { CommunityBlogService } from '../../providers/community-blog/community-blog'; +import { WordPressBlogProvider, BlogPost, BLOG, Blog } from '../../providers/word-press-blog/word-press-blog'; import { PodcastService, Podcast } from '../../providers/fedora-podcast/podcast'; @Component({ @@ -144,24 +143,28 @@ export class BookmarksPage { @Component({ selector: 'women-diversity', templateUrl: 'women-diversity.html', - providers: [CommunityBlogService] + providers: [WordPressBlogProvider] }) export class WomenDiversity { /** * List of posts from Fedora Community Blog */ - private blogposts: Post[]; + private blogposts: BlogPost[]; + private blog: Blog; - constructor(private browser: Browser, private communityblog: CommunityBlogService) { + constructor(private browser: Browser, wordPressBlog: WordPressBlogProvider) { this.blogposts = []; + this.blog = wordPressBlog.getBlog(BLOG.communityBlog); } /** * Fetch Blog Posts from the Fedora Community Blog */ private updateBlogPosts(): void { - this.communityblog.getBlogPosts('454') + this.blog.fetchPosts({ + categories: ['454'] + }) .subscribe(blogposts => { this.blogposts = blogposts; }); @@ -349,4 +352,4 @@ export class PodcastPlayer { date.setSeconds(time); return date.toISOString().substr(14, 5); } -} \ No newline at end of file +} diff --git a/src/pages/women/women.html b/src/pages/women/women.html deleted file mode 100644 index 6417a41..0000000 --- a/src/pages/women/women.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - -

Coming Soon

-
diff --git a/src/pages/women/women.scss b/src/pages/women/women.scss deleted file mode 100644 index f590bdb..0000000 --- a/src/pages/women/women.scss +++ /dev/null @@ -1,22 +0,0 @@ - -.women { - background-color: #abc178 ; - -} - -/*Women page */ - - -html, body -{ -height: 100%; -padding: 0; -margin: 0; -} - -.dummy{ - font-size: 3em; - text-align: center; -} - -/*Front page ends*/ diff --git a/src/pages/women/women.ts b/src/pages/women/women.ts deleted file mode 100644 index 0b037c9..0000000 --- a/src/pages/women/women.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; - -/** - * Fedora Women section - * - * Under construction - */ -@Component({ - templateUrl: 'women.html', -}) -export class WomenPage { -} diff --git a/src/providers/carousel/carousel.ts b/src/providers/carousel/carousel.ts new file mode 100644 index 0000000..43f4483 --- /dev/null +++ b/src/providers/carousel/carousel.ts @@ -0,0 +1,188 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Storage } from '@ionic/storage'; +import { BLOG } from '../word-press-blog/word-press-blog'; +import { Observable } from 'rxjs/Observable'; +import { from } from 'rxjs/observable/from'; +import { merge } from 'rxjs/observable/merge'; +import { map, tap, catchError, mergeMap } from 'rxjs/operators'; +import filenamifyUrl from 'filenamify-url'; + +/** + * Represents a image on App Carousel fetched from fedora magazine + */ +export interface Image { + + /** + * Unique ID of the post, supplied by the CMS + */ + id: number, + + /** + * Link to the associated post + */ + postLink: string, + + /** + * Title of the associated post + */ + title: string, + + /** + * URL of the Image + */ + url: string; +} + +/** + * Source (blog) for the images + */ +const SOURCE = BLOG.fedoraMagazine; + +/** + * Images to display in the carousel + */ +const NUM_IMAGES = '5'; + +/** + * Category to load images from + */ +const CATEGORY = '609'; // 'New in Fedora' + +/** + * Ionic storage key prefix + */ +const KEY = 'carousel__images'; + +/** + * Converts a URL into a key for use in Ionic storage + * @param url URL + */ +function urlKey(url: string) { + return KEY + '__' + filenamifyUrl(url); +} + +/** + * Manages loading and caching of images displayed in the home page carousel + */ +@Injectable() +export class CarouselProvider { + + constructor(private http: HttpClient, private storage: Storage) { + } + + /** + * Rehydrate an image from the cache + * + * If the image is not available, return the unchanged obect + * + * @param i image to load + */ + private loadFile = async (i: Image): Promise => { + const fileKey = urlKey(i.url); + const data = await this.storage.get(fileKey); + if (data) { + const reader = new FileReader(); + reader.readAsDataURL(data); + return new Promise((resolve, reject) => { + reader.onload = () => resolve({ ...i, url: reader.result }); + reader.onerror = err => reject(err); + }); + } else { + return i; + }; + } + + /** + * Load images from the cache + */ + private loadCachedImages(): Observable { + return from( + this.storage.get(KEY) + .then((images: Image[]) => Promise.all(images.map(this.loadFile)))) + .pipe(catchError(() => from([]))); + } + + /** + * Fetch the list of images on Fedora Magazine + * + * @returns Observable which emits an array of images + */ + fetchImages(): Observable { + // We get the images from 'New in Fedora' section in Fedora Magazine + return this.http.get(`${SOURCE.endpoint}/wp-json/wp/v2/posts`, { + params: { + per_page: NUM_IMAGES, + categories: CATEGORY, + _embed: '' + } + }).pipe( + // Parse the API response + map((data: any[]) => data.map((post: any) => ({ + id: post.id, + postLink: post.link, + title: post.title, + url: post._embedded['wp:featuredmedia']['0'].media_details.sizes.medium_large.source_url + }))), + // Load images from the cache where possible + mergeMap(images => Promise.all(images.map(this.loadFile))), + + // Save the new set of images + tap(async images => { + // Loads images into the offline cache + try { + await Promise.all( + images.map(async post => { + const fileName = urlKey(post.url); + const exists = await this.storage.get(fileName); + if (exists) { + // Do nothing if image is already in cache + return Promise.resolve(); + } else { + // Workaround for CORS, replace the endpoint with the proxy URL + const url = post.url.replace(SOURCE.originalEndpoint, SOURCE.endpoint); + + return this.http.get(url, { + responseType: 'blob' + }).toPromise() + .then(data => this.storage.set(fileName, data)); + } + })); + + // Garbage collect old images + const oldImages: Image[] = await this.storage.get(KEY); + if (oldImages) { + const newUrls = new Set(images.map(x => x.url)); + const oldUrls = oldImages.map(x => x.url); + + // Find expired posts and delete them + await Promise.all(oldUrls.map(url => newUrls.has(url) ? + Promise.resolve() : + this.storage.remove(urlKey(url)))); + } + await this.storage.set(KEY, images); + + } catch (error) { + // We actually ignore this error, however just log in for debugging + // This is not supposed to happen + console.error('Unable to cache carousel images'); + console.error(error); + } + }) + ); + } + + /** + * Fetch the images for displaying in the carousel + * + * This returns a list of cached values followed by values from the API + * + * @returns Observable which emits an array of images + */ + getImages(): Observable { + return merge( + this.fetchImages(), + this.loadCachedImages() + ); + } +} diff --git a/src/providers/community-blog/community-blog.ts b/src/providers/community-blog/community-blog.ts deleted file mode 100644 index 746139f..0000000 --- a/src/providers/community-blog/community-blog.ts +++ /dev/null @@ -1,42 +0,0 @@ -import 'rxjs/add/operator/map'; -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs/Observable'; -import { chooseEndpoint, beautifyDate } from '../../utils'; -import { Post } from '../fedora-magazine/fedora-magazine'; - -const ENDPOINT = chooseEndpoint('/community-blog', 'https://communityblog.fedoraproject.org/wp-json/wp/v2'); - -/** - * Service for fetching posts from Community Blog API - */ -@Injectable() -export class CommunityBlogService { - constructor(private http: HttpClient) { - } - - /** - * Fetch the list of latest posts on Fedora Community Blog - * - * @param category from which posts are to be fetched - * @returns Observable which emits an array of blog posts - */ - getBlogPosts(category): Observable { - - let request; - //if no category is specified, fetch posts without parameters. - category === 'null' ? request = this.http.get(`${ENDPOINT}/posts`) : request = this.http.get(`${ENDPOINT}/posts`, { params: {'categories': category}}); - - return request.map((data: any[]) => data.map((blogpost: any) => ({ - id: blogpost.id, - link: blogpost.link, - permalink: blogpost.guid.rendered, - title: blogpost.title.rendered, - excerpt: blogpost.excerpt.rendered, - content: blogpost.content.rendered, - publishedAt: beautifyDate(blogpost.date_gmt, 'blog'), - comments: null, - featuredImage:'' - }))); - } -} \ No newline at end of file diff --git a/src/providers/database/database.ts b/src/providers/database/database.ts new file mode 100644 index 0000000..837a57a --- /dev/null +++ b/src/providers/database/database.ts @@ -0,0 +1,35 @@ +import Dexie from 'dexie'; + +const DB_NAME = 'fedora-app'; + +/** + * Provides database support for the application by wrapping IndexedDB using + * Dexie.js + */ +export class DatabaseProvider extends Dexie { + + constructor() { + super(DB_NAME); + this.ensureSchema(); + } + + /** + * Defines database schema and migrations. + * + * Migrations are automatically executed by Dexie when upgrading from previous + * versions. + */ + private ensureSchema(): void { + // NEVER REMOVE OLDER VERSIONS. + // Database migrations are defined incrementally over one another. + + // DB Version 1: 2018-06-25 18:30:54+05:30 amitosh + + this.version(1).stores({ + // define the schema using Dexie's schema syntax: + // + fedoraMagazine: '&id,publishedAt', + communityBlog: '&id,publishedAt', + }); + } +} diff --git a/src/providers/fedora-magazine/fedora-magazine.ts b/src/providers/fedora-magazine/fedora-magazine.ts deleted file mode 100644 index 3ee6c58..0000000 --- a/src/providers/fedora-magazine/fedora-magazine.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Storage } from '@ionic/storage'; -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs/Observable'; -import { map, tap } from 'rxjs/operators'; -import { fromPromise } from 'rxjs/observable/fromPromise'; -import { merge } from 'rxjs/observable/merge'; -import { chooseEndpoint, defaultValue, beautifyDate} from '../../utils'; - -/** - * Endpoint for this service. - * - * Since Fedora Magazine API does not support CORS, we need to use a proxy when - * running on browser platforms. - */ -const ENDPOINT = chooseEndpoint('/fedora-magazine', 'https://fedoramagazine.org/wp-json/wp/v2'); - -/** - * The key used for storing offline content - */ -const STORAGE_KEY = 'fedora-magazine__posts'; - -/** - * Represents a post on Fedora Magazine - */ -export interface Post { - /** - * Unique ID of the post, supplied by the CMS - */ - id: number, - - /** - * A sluggified link to the post - */ - link: string, - - /** - * Permalink to the post - */ - permalink: string, - - /** - * Post title - */ - title: string, - - /** - * A short excerpt of the post - */ - excerpt: string, - - /** - * The content of the post - */ - content: string, - - /** - * Time of publication - */ - publishedAt: string, - - /** - * Number of comments on the article - */ - - comments: number, - - /** - * Featured Image of article - */ - - featuredImage: string -} - -/** - * Represents a image on App Carousel fetched from fedora magazine - */ -export interface Image { - /** - * Unique ID of the post, supplied by the CMS - */ - id: number, - - /** - * A sluggified link to the post - */ - link: string, - - /** - * The Image of the post - */ - - image: string -} - -/** - * Service for fetching posts from Fedora Magazine API - */ -@Injectable() -export class FedoraMagazineService { - - constructor(private http: HttpClient, private storage: Storage) { - } - - /** - * Fetch the list of posts on Fedora Magazine from offline cache - * - * @returns Observable which emits an array of posts - */ - private loadCachedPosts() { - return fromPromise(this.storage.get(STORAGE_KEY)).pipe(defaultValue([])); - } - - /** - * Fetch the list of latest posts on Fedora Magazine from Fedora Magazine API - * - * @returns Observable which emits an array of posts - */ - fetchPosts(postCount): Observable { - return this.http.get(`${ENDPOINT}/posts`, { params: { '_embed': '', 'per_page': postCount } }) - .pipe( - map((data: any[]) => data.map((post: any) => ({ - id: post.id, - link: post.link, - permalink: post.guid.rendered, - title: post.title.rendered, - image: post.featured_media, - excerpt: post.excerpt.rendered, - content: post.content.rendered, - publishedAt: beautifyDate(post.date_gmt, 'blog'), - featuredImage: post._embedded['wp:featuredmedia']['0'].media_details.sizes.medium.source_url, - comments: post._embedded['replies'] !== undefined ? post._embedded['replies']['0'].length : 0, - }))) - ); - } - - /** - * Fetch the list of posts on Fedora Magazine - * - * Loads from offline cache first and then from HTTP. The response of the HTTP - * request is persisted into the disk cache for further requests. - * - * @returns Observable which emits an array of posts - */ - getPosts(postCount) { - return merge(this.loadCachedPosts(), this.fetchPosts(postCount).pipe( - tap(x => this.storage.set(STORAGE_KEY, x))) - ); - } - - /** - * Fetch the list of latest images on Fedora Magazine - * - * @returns Observable which emits an array of images - */ - getImages(): Observable { - - let pageResults = '5'; // get top 5 posts only - let imageCategory = '609'; //get posts from New in Fedora category - - return this.http.get(`${ENDPOINT}/posts`, { params: { 'per_page': pageResults, 'categories': imageCategory, '_embed': '' } }) - .map((data: any[]) => data.map((image: any) => ({ - id: image.id, - link: image.link, - image: image._embedded['wp:featuredmedia']['0'].media_details.sizes.medium_large.source_url - }))); - } -} diff --git a/src/providers/fedora-podcast/podcast.ts b/src/providers/fedora-podcast/podcast.ts index 9dde1bf..11b3174 100755 --- a/src/providers/fedora-podcast/podcast.ts +++ b/src/providers/fedora-podcast/podcast.ts @@ -21,47 +21,47 @@ const PODCAST_ID = 4543 * Represents a podcast fetched from Simplecast */ export interface Podcast { - /** - * Unique ID of the podcast, supplied by the Simplecast API - */ - id: number, + /** + * Unique ID of the podcast, supplied by the Simplecast API + */ + id: number, - /** - * The Time duration of the podcast - */ - duration: number, + /** + * The Time duration of the podcast + */ + duration: number, - /** - * The Podcast Number - */ + /** + * The Podcast Number + */ - number: number, + number: number, - /** - * Title of the podcast - */ + /** + * Title of the podcast + */ - title: string, + title: string, - /** - * Description of the podcast - */ - description: string, + /** + * Description of the podcast + */ + description: string, - /** - * Date of publishing the podcast - */ - publishedAt: Date, + /** + * Date of publishing the podcast + */ + publishedAt: Date, - /** - * URL of Podcast Source File - */ - audioUrl: string, + /** + * URL of Podcast Source File + */ + audioUrl: string, - /** - * Sharing URL of the Podcast Source File - */ - shareUrl: string + /** + * Sharing URL of the Podcast Source File + */ + shareUrl: string } /** @@ -69,28 +69,28 @@ export interface Podcast { */ @Injectable() export class PodcastService { - constructor(private http: HttpClient) { - } + constructor(private http: HttpClient) { + } - /** - * Fetch the list of latest podcastss on Fedora Community - * - * @returns Observable which emits an array of Podcasts - */ - getPodcasts(): Observable { - return this.http.get(`${ENDPOINT}/podcasts/${PODCAST_ID}/episodes.json`, { headers: { 'X-API-KEY': ENV.SIMPLECAST_CONFIG.apiKey } }) - .pipe( - map((data: any[]) => data.map((podcast: any) => ({ - id: podcast.id, - number: podcast.number, - title: podcast.title, - duration: podcast.duration, - displayTime: Math.floor(podcast.duration/60),//convert to minutes - description: podcast.description, - publishedAt: podcast.published_at, - audioUrl: podcast.audio_url, - shareUrl: podcast.share_url - }))) - ); - } -} \ No newline at end of file + /** + * Fetch the list of latest podcastss on Fedora Community + * + * @returns Observable which emits an array of Podcasts + */ + getPodcasts(): Observable { + return this.http.get(`${ENDPOINT}/podcasts/${PODCAST_ID}/episodes.json`, { headers: { 'X-API-KEY': ENV.SIMPLECAST_CONFIG.apiKey } }) + .pipe( + map((data: any[]) => data.map((podcast: any) => ({ + id: podcast.id, + number: podcast.number, + title: podcast.title, + duration: podcast.duration, + displayTime: Math.floor(podcast.duration / 60),//convert to minutes + description: podcast.description, + publishedAt: podcast.published_at, + audioUrl: podcast.audio_url, + shareUrl: podcast.share_url + }))) + ); + } +} diff --git a/src/providers/package-search/package-search.ts b/src/providers/package-search/package-search.ts index b1921a8..f339695 100644 --- a/src/providers/package-search/package-search.ts +++ b/src/providers/package-search/package-search.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; const API_ENDPOINT = 'https://apps.fedoraproject.org/packages/fcomm_connector/xapian/query/search_packages/'; @@ -50,13 +51,15 @@ export class PackageSearchProvider { }); return this.http.get(`${API_ENDPOINT}/${queryObjectStr}`) - .map((res: any) => ({ - matches: res.total_rows, - count: res.visible_rows, - offset: res.start_row, - pageSize: res.rows_per_page, - packages: res.rows.map(mapPackage) - })); + .pipe( + map((res: any) => ({ + matches: res.total_rows, + count: res.visible_rows, + offset: res.start_row, + pageSize: res.rows_per_page, + packages: res.rows.map(mapPackage) + })) + ); } } diff --git a/src/providers/social/facebook.ts b/src/providers/social/facebook.ts index d2f6d9b..57b3b88 100644 --- a/src/providers/social/facebook.ts +++ b/src/providers/social/facebook.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import ENV from '@environment'; import { Storage } from '@ionic/storage'; import { compact } from 'lodash-es'; -import { beautifyDate } from '../../utils'; import { SocialProvider, SocialMediaPost, FACEBOOK } from './social'; import { Observable } from 'rxjs/Observable'; import { fromPromise } from 'rxjs/observable/fromPromise'; @@ -68,7 +67,7 @@ export class FacebookProvider implements SocialProvider { link: 'https://facebook.com/' + p.id, content: p.message, origin: FACEBOOK, - date: beautifyDate(p.created_time,'social') + date: new Date(p.created_time) }; return post.content ? post : null; diff --git a/src/providers/social/social.ts b/src/providers/social/social.ts index e69e753..b93d201 100644 --- a/src/providers/social/social.ts +++ b/src/providers/social/social.ts @@ -37,7 +37,7 @@ export interface SocialMediaPost { /** * Date of the post */ - date: string, + date: Date } /** diff --git a/src/providers/social/twitter.ts b/src/providers/social/twitter.ts index 7a3d9d0..0466a0a 100644 --- a/src/providers/social/twitter.ts +++ b/src/providers/social/twitter.ts @@ -1,6 +1,5 @@ import ENV from '@environment'; import { Injectable } from '@angular/core'; -import { beautifyDate } from '../../utils'; import { SocialProvider, SocialMediaPost } from './social'; import { HttpClient } from '@angular/common/http'; import { Storage } from '@ionic/storage'; @@ -69,7 +68,9 @@ export class TwitterProvider implements SocialProvider { link: 'https://twitter.com/statuses/' + t.id_str, content: t.text, origin: 'twitter', - date: beautifyDate(t.created_at,'social') + // Twitter has a horrible date format + // Credits: https://stackoverflow.com/a/2766516/2328165 + date: new Date(t.created_at.replace(/^\w+ (\w+) (\d+) ([\d:]+) \+0000 (\d+)$/, "$1 $2 $4 $3 UTC")) }))) ); } diff --git a/src/providers/word-press-blog/word-press-blog.ts b/src/providers/word-press-blog/word-press-blog.ts new file mode 100644 index 0000000..d6251aa --- /dev/null +++ b/src/providers/word-press-blog/word-press-blog.ts @@ -0,0 +1,218 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, tap } from 'rxjs/operators'; +import { DatabaseProvider } from '../database/database'; +import { Observable } from 'rxjs/Observable'; +import { from } from 'rxjs/observable/from'; +import { chooseEndpoint } from '../../utils'; + +// Blog definitions +export const BLOG = Object.seal({ + fedoraMagazine: { + id: 'fedoraMagazine', + title: 'Fedora Magazine', + originalEndpoint: 'https://fedoramagazine.org', + endpoint: chooseEndpoint('/fedora-magazine', 'https://fedoramagazine.org') + }, + communityBlog: { + id: 'communityBlog', + title: 'Fedora Community Blog', + originalEndpoint: 'https://communityblog.fedoraproject.org', + endpoint: chooseEndpoint('/community-blog', 'https://communityblog.fedoraproject.org') + } +}); + +/** + * Represents a blog post + */ +export interface BlogPost { + /** + * Unique ID of the post, supplied by the CMS + */ + id: number, + + /** + * A sluggified link to the post + */ + link: string, + + /** + * Permalink to the post + */ + permalink: string, + + /** + * Post title + */ + title: string, + + /** + * A short excerpt of the post + */ + excerpt: string, + + /** + * The content of the post + */ + content: string, + + /** + * Time of publication + */ + publishedAt: Date + + /** + * Number of comments on the article + */ + comments: number, + + /** + * Featured Image of article + */ + featuredImage: string +} + +interface BlogPostFetchArgs { + 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$; + + constructor(public readonly id: string, public readonly endpoint: string, + private http: HttpClient, private db: DatabaseProvider) { + } + + /** + * Fetch the list of latest posts on the blog using the WordPress API + * + * @returns Observable which emits an array of posts + */ + public fetchPosts(funcParams: BlogPostFetchArgs = {}): Observable { + const { + page = 1, + orderBy = 'date', + order = 'desc', + categories = [], + tags = [] + } = funcParams; + // WordPress API parameters + const params: any = { + _embed: '', + page, + orderby: orderBy, + order, + per_page: this.postsPerPage, + } + + if (categories.length > 0) { + params.categories = categories.join(',') + } + + if (tags.length > 0) { + 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 { + id: post.id as number, + link: post.link as string, + permalink: post.guid.rendered as string, + title: post.title.rendered as string, + 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, + }; + })), + ); + } + + /** + * Updates the list of posts in the offline cache from the WordPress API + * + * @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); + } + }) + ); + } + + /** + * Fetch the list of posts on the Blog from offline cache + * + * @returns Observable which emits an array of posts + */ + public loadCachedPosts(): Observable { + return from( + this.db.table(this.id) + .orderBy('publishedAt').reverse().toArray() + ); + } +} + +/** + * Provider for WordPress API + */ +@Injectable() +export class WordPressBlogProvider { + + private blogs: Map + + constructor(private http: HttpClient, private db: DatabaseProvider) { + this.blogs = new Map(); + } + + /** + * Get a cached instance to a Blog for a blog definition + * + * @param blog A blog definition + */ + 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)); + } + + return this.blogs.get(blog.id); + } +}