Browse Source

Feed provider for home and user timeline

Carsten Porth 5 years ago
parent
commit
51aea88047

+ 3 - 1
app/src/app/app.module.ts

@@ -26,6 +26,7 @@ import { WriteTweetPage } from '../pages/write-tweet/write-tweet';
 import { QuotedStatusComponent } from '../components/quoted-status/quoted-status';
 import { P2pStorageIpfsProvider } from '../providers/p2p-storage-ipfs/p2p-storage-ipfs';
 import { P2pDatabaseGunProvider } from '../providers/p2p-database-gun/p2p-database-gun';
+import { FeedProvider } from '../providers/feed/feed';
 
 @NgModule({
   declarations: [
@@ -69,7 +70,8 @@ import { P2pDatabaseGunProvider } from '../providers/p2p-database-gun/p2p-databa
     AuthProvider,
     TwitterApiProvider,
     P2pStorageIpfsProvider,
-    P2pDatabaseGunProvider
+    P2pDatabaseGunProvider,
+    FeedProvider
   ]
 })
 export class AppModule { }

+ 2 - 2
app/src/components/feed/feed.ts

@@ -22,9 +22,9 @@ export class FeedComponent {
   @Input()
   data: any[];
   @Input()
-  enableRefresh: boolean;
+  enableRefresh: boolean = true;
   @Input()
-  enableInfiniteScroll: boolean;
+  enableInfiniteScroll: boolean = true;
   @Output()
   onRefresh: EventEmitter<any> = new EventEmitter<any>();
   @Output()

+ 1 - 1
app/src/pages/home/home.html

@@ -8,7 +8,7 @@
 </ion-header>
 
 <ion-content>
-  <feed [data]="data" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)"></feed>
+  <feed [data]="tweets" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)"></feed>
 
   <ion-fab bottom right>
     <button ion-fab (click)="writeTweet()">

+ 50 - 49
app/src/pages/home/home.ts

@@ -3,13 +3,12 @@ import {
   IonicPage,
   NavController,
   MenuController,
-  InfiniteScroll
+  InfiniteScroll,
+  Refresher
 } from "ionic-angular";
 import { Storage } from "@ionic/storage";
-import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
 import { WriteTweetPage } from "../write-tweet/write-tweet";
-import { P2pDatabaseGunProvider } from "../../providers/p2p-database-gun/p2p-database-gun";
-import { P2pStorageIpfsProvider } from "../../providers/p2p-storage-ipfs/p2p-storage-ipfs";
+import { FeedProvider } from "../../providers/feed/feed";
 
 @IonicPage()
 @Component({
@@ -18,77 +17,79 @@ import { P2pStorageIpfsProvider } from "../../providers/p2p-storage-ipfs/p2p-sto
 })
 export class HomePage {
   menuController: MenuController;
-  data: any[] = [];
+  tweets;
+  userId: string;
 
   constructor(
     public navCtrl: NavController,
-    private twitter: TwitterApiProvider,
     private menuCtrl: MenuController,
-    private gun: P2pDatabaseGunProvider,
-    private ipfs: P2pStorageIpfsProvider,
-    private storage: Storage
-  ) {}
+    private storage: Storage,
+    private feed: FeedProvider
+  ) {
+    this.storage.get("userId").then(userId => (this.userId = userId));
+  }
 
   ionViewDidLoad() {
     this.menuCtrl.enable(true, "sideNav");
   }
 
-  async ionViewDidEnter() {
-    this.twitter.fetchHomeFeed().then(res => {
-      this.data = res.data.sort((a, b) => this.sortByDateAsc(a, b));
-      console.log(res);
-    });
-    // this.gun
-    //   .getLastTweetFromUser("username")
-    //   .then(hash => this.ipfs.fetchTweet(hash))
-    //   .then(tweet => this.addUserObject(tweet))
-    //   .then(
-    //     res =>
-    //       (this.data = this.data
-    //         .concat(res)
-    //         .sort((a, b) => this.sortByDateAsc(a, b)))
-    //   );
-    this.twitter
-      .fetchFriends(await this.storage.get("userId"))
-      .then(res => console.log(res));
+  ionViewDidEnter() {
+    this.feed
+      .loadHomeTimeline(this.userId)
+      .then(tweets => (this.tweets = tweets));
   }
 
-  doRefresh(refresher) {
-    this.twitter.fetchHomeFeed().then(res => {
-      this.data = res.data;
-      console.log(res);
+  doRefresh(refresher: Refresher) {
+    this.feed.loadHomeTimeline(this.userId).then(tweets => {
+      this.tweets = tweets;
       refresher.complete();
     });
   }
 
   loadMore(infiniteScroll: InfiniteScroll) {
-    this.twitter.fetchHomeFeed(this.data[this.data.length - 1].id).then(res => {
-      this.data = this.data
-        .concat(res.data)
-        .sort((a, b) => this.sortByDateAsc(a, b));
-      infiniteScroll.complete();
-    });
+    console.log("triggered");
+    this.feed
+      .loadHomeTimeline(
+        this.userId,
+        this.oldestPublicTweet,
+        this.oldestPrivateTweet
+      )
+      .then(tweets => {
+        this.tweets = this.tweets.concat(tweets);
+        infiniteScroll.complete();
+      });
   }
 
   writeTweet() {
     this.navCtrl.push(WriteTweetPage);
   }
 
-  private async addUserObject(tweet) {
-    tweet.user = await this.twitter.fetchUser(tweet.user_id);
-    return tweet;
+  get publicTweets() {
+    return this.tweets.filter(tweet => !tweet.private_tweet);
+  }
+
+  get privateTweets() {
+    return this.tweets.filter(tweet => tweet.private_tweet);
   }
 
-  private sortByDateAsc(a, b) {
-    const dateA = new Date(a.created_at);
-    const dateB = new Date(b.created_at);
+  get oldestPublicTweet() {
+    if (this.publicTweets.length > 0) {
+      return this.publicTweets.reduce(
+        (acc, cur) => (acc.id < cur.id ? acc : cur)
+      );
+    } else {
+      return undefined;
+    }
+  }
 
-    if (dateA > dateB) {
-      return -1;
-    } else if (dateA < dateB) {
-      return 1;
+  get oldestPrivateTweet() {
+    if (this.privateTweets.length > 0) {
+      return this.privateTweets.reduce(
+        (acc, cur) =>
+          new Date(acc.created_at) < new Date(cur.created_at) ? acc : cur
+      );
     } else {
-      return 0;
+      return undefined;
     }
   }
 }

+ 35 - 77
app/src/pages/profile/profile.ts

@@ -8,8 +8,7 @@ import {
   LoadingController
 } from "ionic-angular";
 import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
-import { P2pStorageIpfsProvider } from "../../providers/p2p-storage-ipfs/p2p-storage-ipfs";
-import { P2pDatabaseGunProvider } from "../../providers/p2p-database-gun/p2p-database-gun";
+import { FeedProvider } from "../../providers/feed/feed";
 
 @IonicPage()
 @Component({
@@ -29,10 +28,9 @@ export class ProfilePage {
   constructor(
     public navCtrl: NavController,
     private loadingCtrl: LoadingController,
-    public navParams: NavParams,
+    private navParams: NavParams,
     private twitter: TwitterApiProvider,
-    private ipfs: P2pStorageIpfsProvider,
-    private gun: P2pDatabaseGunProvider
+    private feed: FeedProvider
   ) {}
 
   ionViewDidLoad() {
@@ -47,15 +45,12 @@ export class ProfilePage {
     this.twitter.fetchUser(userId).then(res => (this.user = res));
 
     // Load user's timeline from Twitter and P2P
-    this.loadTimeline(userId).then(res => {
+    this.feed.loadUserTimeline(userId).then(res => {
       if (res.length > 0) {
         // Store tweets
         this.tweets = res;
-
-        // Save oldest tweet's id for next load more
-        this.oldestLoadedTweetId = res
-          .filter(tweet => !tweet.private_tweet)
-          .reduce((acc, cur) => (acc.id < cur.id ? acc : cur))["id"];
+      } else {
+        this.enableInfiniteScroll = false;
       }
       // Hide loading indicator
       loading.dismiss();
@@ -63,15 +58,10 @@ export class ProfilePage {
   }
 
   doRefresh(refresher) {
-    this.loadTimeline(this.user.id_str).then(res => {
+    this.feed.loadUserTimeline(this.user.id_str).then(res => {
       if (res.length > 0) {
         // Replace tweets
         this.tweets = res;
-
-        // Save oldest tweet's id for next load more
-        this.oldestLoadedTweetId = res
-          .filter(tweet => !tweet.private_tweet)
-          .reduce((acc, cur) => (acc.id < cur.id ? acc : cur))["id"];
       }
 
       // Hide loading icon
@@ -81,89 +71,57 @@ export class ProfilePage {
 
   loadMore(infiniteScroll: InfiniteScroll) {
     if (this.enableInfiniteScroll) {
-      this.loadTimeline(this.user.id_str, this.oldestLoadedTweetId).then(
-        res => {
+      this.feed
+        .loadUserTimeline(
+          this.user.id_str,
+          this.oldestPublicTweet,
+          this.oldestPrivateTweet
+        )
+        .then(res => {
           if (res.length > 0) {
             // Append loaded tweets
             this.tweets = this.tweets.concat(res);
-
-            // Save oldest tweet's id for next load more
-            this.oldestLoadedTweetId = res
-              .filter(tweet => !tweet.private_tweet)
-              .reduce((acc, cur) => (acc.id < cur.id ? acc : cur))["id"];
+          } else {
+            this.enableInfiniteScroll = false;
           }
           // Hide loading icon
           infiniteScroll.complete();
-        }
-      );
+        });
     } else {
       // Hide loading icon
       infiniteScroll.complete();
     }
   }
 
-  private async loadTimeline(userId, oldestLoadedTweetId?) {
-    // Fetch tweets from Twitter
-    const tweets = await this.twitter.fetchUserTimeline(
-      userId,
-      oldestLoadedTweetId
-    );
-
-    // Determine end of time interval to look for private tweets
-    let intervalEnd: Date;
-    if (tweets.length < 20) {
-      // End of timeline is reached
-      this.enableInfiniteScroll = false;
-      intervalEnd = new Date("2018-04-01T00:00:00");
-    } else {
-      const lastTweetTimestamp = tweets[tweets.length - 1].created_at;
-      intervalEnd = new Date(lastTweetTimestamp);
-    }
-
-    // Fetch private tweet hashs from P2P DB for corresponding interval
-    const privateTweetHashs: string[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
-      userId,
-      // TODO: timestamp vom vorigen oldestLoadedTweet statt new Date()
-      new Date(),
-      intervalEnd
-    );
+  get publicTweets() {
+    return this.tweets.filter(tweet => !tweet.private_tweet);
+  }
 
-    if (privateTweetHashs.length) {
-      // Load private tweets from P2P storage
-      let privateTweets = await this.ipfs.fetchTweets(privateTweetHashs);
+  get privateTweets() {
+    return this.tweets.filter(tweet => tweet.private_tweet);
+  }
 
-      // Add user object to private tweets
-      privateTweets = await Promise.all(
-        privateTweets.map(async tweet => await this.addUserToTweet(tweet))
+  get oldestPublicTweet() {
+    if (this.publicTweets.length > 0) {
+      return this.publicTweets.reduce(
+        (acc, cur) => (acc.id < cur.id ? acc : cur)
       );
-
-      // Combine and sort tweets
-      return tweets
-        .concat(privateTweets)
-        .sort((a, b) => this.sortByDateAsc(a, b));
     } else {
-      return tweets;
+      return undefined;
     }
   }
 
-  private sortByDateAsc(a, b) {
-    const dateA = new Date(a.created_at);
-    const dateB = new Date(b.created_at);
-
-    if (dateA > dateB) {
-      return -1;
-    } else if (dateA < dateB) {
-      return 1;
+  get oldestPrivateTweet() {
+    if (this.privateTweets.length > 0) {
+      return this.privateTweets.reduce(
+        (acc, cur) =>
+          new Date(acc.created_at) < new Date(cur.created_at) ? acc : cur
+      );
     } else {
-      return 0;
+      return undefined;
     }
   }
 
-  private async addUserToTweet(tweet) {
-    tweet.user = await this.twitter.fetchUser(tweet.user_id);
-    return tweet;
-  }
-
   onScroll(event) {
     this.enableRefresh = event.scrollTop === 0;
   }

+ 141 - 0
app/src/providers/feed/feed.ts

@@ -0,0 +1,141 @@
+import { HttpClient } from "@angular/common/http";
+import { Injectable } from "@angular/core";
+import { TwitterApiProvider } from "../twitter-api/twitter-api";
+import { P2pDatabaseGunProvider } from "../p2p-database-gun/p2p-database-gun";
+import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs";
+
+@Injectable()
+export class FeedProvider {
+  constructor(
+    private http: HttpClient,
+    private twitter: TwitterApiProvider,
+    private gun: P2pDatabaseGunProvider,
+    private ipfs: P2pStorageIpfsProvider
+  ) {}
+
+  public async loadUserTimeline(
+    userId,
+    oldestPublicTweet?,
+    oldestPrivateTweet?
+  ) {
+    const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
+    // Fetch tweets from Twitter
+    let tweets = await this.twitter.fetchUserTimeline(userId, maxId);
+    tweets = tweets.filter(tweet => tweet.id_str != maxId);
+
+    // Determine start and end of time interval to look for private tweets
+    const intervalStart: Date = oldestPrivateTweet
+      ? new Date(oldestPrivateTweet["created_at"])
+      : new Date();
+    const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
+
+    // Fetch private tweet hashs from P2P DB for corresponding interval
+    const privateTweetHashs: string[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
+      userId,
+      intervalStart,
+      intervalEnd
+    );
+
+    if (privateTweetHashs.length) {
+      // Load private tweets from P2P storage
+      let privateTweets = await this.ipfs.fetchTweets(privateTweetHashs);
+
+      // Add user object to private tweets
+      privateTweets = await Promise.all(
+        privateTweets.map(async tweet => await this.addUserToTweet(tweet))
+      );
+
+      // Combine and sort tweets
+      return tweets
+        .concat(privateTweets)
+        .sort((a, b) => this.sortByDateAsc(a, b));
+    } else {
+      return tweets;
+    }
+  }
+
+  public async loadHomeTimeline(
+    userId,
+    oldestPublicTweet?,
+    oldestPrivateTweet?
+  ) {
+    // Fetch tweets from Twitter
+    const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
+    let tweets = await this.twitter.fetchHomeFeed(maxId);
+    tweets = tweets.filter(tweet => tweet.id_str != maxId);
+
+    // Determine start and end of time interval to look for private tweets
+    const intervalStart: Date = oldestPrivateTweet
+      ? new Date(oldestPrivateTweet["created_at"])
+      : new Date();
+    const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
+
+    // Fetch user's friends
+    const friends = await this.twitter.fetchFriends(userId);
+
+    let privateTweetHashs = [];
+    friends.forEach(async friend => {
+      privateTweetHashs = privateTweetHashs.concat(
+        await this.gun.fetchPrivateTweetHashsForUserInInterval(
+          friend.id_str,
+          intervalStart,
+          intervalEnd
+        )
+      );
+    });
+
+    // Add users private tweets
+    privateTweetHashs = privateTweetHashs.concat(
+      await this.gun.fetchPrivateTweetHashsForUserInInterval(
+        userId,
+        intervalStart,
+        intervalEnd
+      )
+    );
+
+    if (privateTweetHashs.length) {
+      // Load private tweets from P2P storage
+      let privateTweets = await this.ipfs.fetchTweets(privateTweetHashs);
+
+      // Add user object to private tweets
+      privateTweets = await Promise.all(
+        privateTweets.map(async tweet => await this.addUserToTweet(tweet))
+      );
+
+      // Combine and sort tweets
+      return tweets
+        .concat(privateTweets)
+        .sort((a, b) => this.sortByDateAsc(a, b));
+    } else {
+      return tweets;
+    }
+  }
+
+  private async addUserToTweet(tweet) {
+    tweet.user = await this.twitter.fetchUser(tweet.user_id);
+    return tweet;
+  }
+
+  private getOldestTweetTimestamp(tweets): Date {
+    if (tweets.length < 15) {
+      // End of timeline is reached - load all private tweets
+      return new Date("2018-04-01T00:00:00");
+    } else {
+      const lastTweetTimestamp = tweets[tweets.length - 1].created_at;
+      return new Date(lastTweetTimestamp);
+    }
+  }
+
+  private sortByDateAsc(a, b) {
+    const dateA = new Date(a.created_at);
+    const dateB = new Date(b.created_at);
+
+    if (dateA > dateB) {
+      return -1;
+    } else if (dateA < dateB) {
+      return 1;
+    } else {
+      return 0;
+    }
+  }
+}

+ 2 - 2
app/src/providers/p2p-database-gun/p2p-database-gun.ts

@@ -56,8 +56,8 @@ export class P2pDatabaseGunProvider {
       return entries
         .filter(
           entry =>
-            new Date(entry["created_at"]) <= intervalStart &&
-            new Date(entry["created_at"]) > intervalEnd
+            new Date(entry["created_at"]) < intervalStart &&
+            new Date(entry["created_at"]) >= intervalEnd
         )
         .map(entry => entry["hash"]);
     } else {

+ 2 - 1
app/src/providers/twitter-api/twitter-api.ts

@@ -24,12 +24,13 @@ export class TwitterApiProvider {
   }
 
   public async fetchHomeFeed(maxId?) {
-    return await this.client.get("statuses/home_timeline", {
+    const res = await this.client.get("statuses/home_timeline", {
       count: 20,
       include_entities: true,
       tweet_mode: "extended",
       max_id: maxId
     });
+    return res.data;
   }
 
   public async fetchUser(userId) {