Browse Source

parse tweet entities and build tweet

Carsten Porth 5 years ago
parent
commit
c6c58d87b6

+ 33 - 29
app/src/app/app.module.ts

@@ -1,32 +1,34 @@
-import { BrowserModule } from '@angular/platform-browser';
-import { ErrorHandler, NgModule } from '@angular/core';
-import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
-import { SplashScreen } from '@ionic-native/splash-screen';
-import { StatusBar } from '@ionic-native/status-bar';
-import { HttpClient, HttpClientModule } from '@angular/common/http';
-import { IonicStorageModule } from '@ionic/storage';
+import { BrowserModule } from "@angular/platform-browser";
+import { ErrorHandler, NgModule } from "@angular/core";
+import { IonicApp, IonicErrorHandler, IonicModule } from "ionic-angular";
+import { SplashScreen } from "@ionic-native/splash-screen";
+import { StatusBar } from "@ionic-native/status-bar";
+import { HttpClient, HttpClientModule } from "@angular/common/http";
+import { IonicStorageModule } from "@ionic/storage";
 
-import { AuthProvider } from '../providers/auth/auth';
+import { AuthProvider } from "../providers/auth/auth";
 
-import { MyApp } from './app.component';
-import { HomePage } from '../pages/home/home';
-import { SearchPage } from '../pages/search/search';
-import { SettingsPage } from '../pages/settings/settings';
-import { LoginPage } from '../pages/login/login';
-import { TwitterApiProvider } from '../providers/twitter-api/twitter-api';
-import { FeedComponent } from '../components/feed/feed';
-import { TweetComponent } from '../components/tweet/tweet';
-import { TweetHeaderComponent } from '../components/tweet-header/tweet-header';
-import { TweetBodyComponent } from '../components/tweet-body/tweet-body';
-import { TweetActionsComponent } from '../components/tweet-actions/tweet-actions';
-import { ProfilePage } from '../pages/profile/profile';
-import { ProfileHeaderComponent } from '../components/profile-header/profile-header';
-import { PipesModule } from '../pipes/pipes.module';
-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';
+import { MyApp } from "./app.component";
+import { HomePage } from "../pages/home/home";
+import { SearchPage } from "../pages/search/search";
+import { SettingsPage } from "../pages/settings/settings";
+import { LoginPage } from "../pages/login/login";
+import { TwitterApiProvider } from "../providers/twitter-api/twitter-api";
+import { FeedComponent } from "../components/feed/feed";
+import { TweetComponent } from "../components/tweet/tweet";
+import { TweetHeaderComponent } from "../components/tweet-header/tweet-header";
+import { TweetBodyComponent } from "../components/tweet-body/tweet-body";
+import { TweetActionsComponent } from "../components/tweet-actions/tweet-actions";
+import { ProfilePage } from "../pages/profile/profile";
+import { ProfileHeaderComponent } from "../components/profile-header/profile-header";
+import { PipesModule } from "../pipes/pipes.module";
+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";
+import { MentionComponent } from "../components/mention/mention";
+import { HashtagComponent } from "../components/hashtag/hashtag";
 
 @NgModule({
   declarations: [
@@ -43,7 +45,9 @@ import { FeedProvider } from '../providers/feed/feed';
     TweetBodyComponent,
     TweetActionsComponent,
     ProfileHeaderComponent,
-    QuotedStatusComponent
+    QuotedStatusComponent,
+    MentionComponent,
+    HashtagComponent
   ],
   imports: [
     BrowserModule,
@@ -74,4 +78,4 @@ import { FeedProvider } from '../providers/feed/feed';
     FeedProvider
   ]
 })
-export class AppModule { }
+export class AppModule {}

+ 39 - 43
app/src/app/app.scss

@@ -12,52 +12,48 @@
 // To declare rules for a specific mode, create a child rule
 // for the .md, .ios, or .wp mode classes. The mode class is
 // automatically applied to the <body> element in the app.
-.hashtag {
-  color: color($colors, primary, base);
-}
-
 .icon.icon-verified {
-  color: #1da1f2;
+    color: #1da1f2;
 }
 
 #sideNav {
-  .user-info {
-    .user-banner {
-      position: relative;
-      height: 140px;
-      margin-bottom: 20px;
-      .svg-triangle {
-        position: absolute;
-        bottom: 0;
-      }
-      .user-avatar {
-        position: absolute;
-        bottom: -10px;
-        left: 15px;
-        border-radius: 50%;
-      }
-    }
     .user-info {
-      padding: 0 15px;
-      font-size: 15px;
-      font-weight: 500;
-      margin-bottom: 10px;
-      .handle {
-        color: #ababab;
-        font-weight: 300;
-        font-size: 11px;
-      }
+        .user-banner {
+            position: relative;
+            height: 140px;
+            margin-bottom: 20px;
+            .svg-triangle {
+                position: absolute;
+                bottom: 0;
+            }
+            .user-avatar {
+                position: absolute;
+                bottom: -10px;
+                left: 15px;
+                border-radius: 50%;
+            }
+        }
+        .user-info {
+            padding: 0 15px;
+            font-size: 15px;
+            font-weight: 500;
+            margin-bottom: 10px;
+            .handle {
+                color: #ababab;
+                font-weight: 300;
+                font-size: 11px;
+            }
+        }
     }
-  }
-  .label-md {
-    margin-left: 8px;
-    display: flex;
-    align-items: center;
-  }
-  .item-inner {
-    padding-left: 8px;
-  }
-  .ion-icon {
-    line-height: 1;
-  }
-}
+    .label-md {
+        margin-left: 8px;
+        display: flex;
+        align-items: center;
+    }
+    .item-inner {
+        padding-left: 8px;
+    }
+    .ion-icon {
+        line-height: 1;
+    }
+}

+ 1 - 1
app/src/components/hashtag/hashtag.html

@@ -1,2 +1,2 @@
 <!-- Generated template for the HashtagComponent component -->
-<span class="hashtag" (click)="search(hashtag)">#{{ hashtag }}</span>
+<span class="hashtag" (click)="search(hashtag)">{{ hashtag }}</span>

+ 3 - 1
app/src/components/hashtag/hashtag.scss

@@ -1,3 +1,5 @@
 hashtag {
-
+  .hashtag {
+    color: color($colors, primary, base);
+  }
 }

+ 1 - 7
app/src/components/hashtag/hashtag.ts

@@ -1,13 +1,7 @@
 import { Component, Input } from "@angular/core";
-import { NavController } from "../../../node_modules/ionic-angular/umd";
+import { NavController } from "ionic-angular";
 import { SearchPage } from "../../pages/search/search";
 
-/**
- * Generated class for the HashtagComponent component.
- *
- * See https://angular.io/api/core/Component for more info on Angular
- * Components.
- */
 @Component({
   selector: "hashtag",
   templateUrl: "hashtag.html"

+ 1 - 1
app/src/components/mention/mention.html

@@ -1,2 +1,2 @@
 <!-- Generated template for the MentionComponent component -->
-<span class="mention" (click)="showProfile(username)">@{{ username }}</span>
+<span class="mention" (click)="showProfile(userId)">{{ username }}</span>

+ 3 - 1
app/src/components/mention/mention.scss

@@ -1,3 +1,5 @@
 mention {
-
+  .mention {
+    color: color($colors, primary, base);
+  }
 }

+ 5 - 10
app/src/components/mention/mention.ts

@@ -1,14 +1,7 @@
 import { Component, Input } from "@angular/core";
-import { copyInputAttributes } from "../../../node_modules/ionic-angular/umd/util/dom";
-import { NavController } from "../../../node_modules/ionic-angular/umd";
+import { NavController } from "ionic-angular";
 import { ProfilePage } from "../../pages/profile/profile";
 
-/**
- * Generated class for the MentionComponent component.
- *
- * See https://angular.io/api/core/Component for more info on Angular
- * Components.
- */
 @Component({
   selector: "mention",
   templateUrl: "mention.html"
@@ -16,10 +9,12 @@ import { ProfilePage } from "../../pages/profile/profile";
 export class MentionComponent {
   @Input()
   username: string;
+  @Input()
+  userId: string;
 
   constructor(public navCtrl: NavController) {}
 
-  showProfile(username) {
-    this.navCtrl.push(ProfilePage, { username });
+  showProfile(userId) {
+    this.navCtrl.push(ProfilePage, { userId });
   }
 }

+ 1 - 1
app/src/components/quoted-status/quoted-status.html

@@ -7,7 +7,7 @@
     <span class="timestamp">{{ data.created_at | diffForHumans }}</span>
   </div>
   <div class="body">
-    <p [innerHTML]="data.full_text | replaceUrls: data.entities.urls | replaceHashtags: data.entities.hashtags"></p>
+    <p>{{ data.full_text }}</p>
     <img *ngIf="hasPhoto" src="{{ data.entities.media[0]['media_url_https'] }}" alt="Photo">
   </div>
 </div>

+ 6 - 1
app/src/components/tweet-body/tweet-body.html

@@ -1,6 +1,11 @@
 <!-- Generated template for the TweetBodyComponent component -->
 <div>
-  <p [innerHTML]="status"></p>
+  <div class="tweet-array" *ngFor="let part of tweetArray">
+    <span *ngIf="part.type =='text'" class="text">{{ part.text }}</span>
+    <a *ngIf="part.type == 'url'" href="{{ part.url }}">{{ part.text }}</a>
+    <mention *ngIf="part.type == 'user_mention'" [username]="part.text" [userId]="part.userId"></mention>
+    <hashtag *ngIf="part.type == 'hashtag'" [hashtag]="part.text"></hashtag>
+  </div>
   <img *ngIf="hasPhoto" src="{{ entities.media[0]['media_url_https'] }}" alt="Photo" class="photo">
   <video *ngIf="isGif" src="{{ extended_entities.media[0]['video_info']['variants'][0]['url'] }}" autoplay loop></video>
   <quoted-status *ngIf="data.quoted_status" [data]="data.quoted_status"></quoted-status>

+ 6 - 3
app/src/components/tweet-body/tweet-body.scss

@@ -1,13 +1,16 @@
 tweet-body {
+  span.text {
+    white-space: pre-wrap;
+  }
+  .tweet-array {
+    display: inline;
+  }
   .photo {
     margin-top: 5px;
     padding: 2px;
     border-radius: 3px;
     border: 1px solid #dfdfdf;
   }
-  p {
-    white-space: pre-wrap;
-  }
   video {
     width: 100%;
   }

+ 154 - 7
app/src/components/tweet-body/tweet-body.ts

@@ -18,14 +18,9 @@ export class TweetBodyComponent {
 
   get status(): string {
     if (this.data["retweeted_status"]) {
-      const range = this.data["retweeted_status"]["display_text_range"];
-      return this.data["retweeted_status"]["full_text"].substr(
-        range[0],
-        range[1]
-      );
+      return this.data["retweeted_status"]["full_text"];
     } else {
-      const range = this.data["display_text_range"];
-      return this.data["full_text"].substr(range[0], range[1]);
+      return this.data["full_text"];
     }
   }
 
@@ -44,6 +39,13 @@ export class TweetBodyComponent {
       return this.data["extended_entities"];
     }
   }
+  get range() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["display_text_range"];
+    } else {
+      return this.data["display_text_range"];
+    }
+  }
 
   get hasPhoto() {
     return (
@@ -61,4 +63,149 @@ export class TweetBodyComponent {
       this.extended_entities["media"][0]["type"] === "animated_gif"
     );
   }
+
+  get tweetArray() {
+    let tweetArray = [];
+    tweetArray = tweetArray.concat(this.getUserMentionsForTweetArray());
+    tweetArray = tweetArray.concat(this.getHashtagsForTweetArray());
+    tweetArray = tweetArray.concat(this.getUrlsForTweetArray());
+    tweetArray = tweetArray.concat(
+      this.getTextPartsForTweetArray(
+        tweetArray.sort((a, b) => a["start"] - b["start"])
+      )
+    );
+    tweetArray = this.cutToTextRange(tweetArray);
+    return tweetArray.sort((a, b) => a["start"] - b["start"]);
+  }
+
+  private getUserMentionsForTweetArray() {
+    return this.entities["user_mentions"].map(mention => ({
+      start: mention["indices"][0],
+      stop: mention["indices"][1],
+      type: "user_mention",
+      text: "@" + mention["screen_name"],
+      userId: mention["id_str"]
+    }));
+  }
+
+  private getHashtagsForTweetArray() {
+    return this.entities["hashtags"].map(hashtag => ({
+      start: hashtag["indices"][0],
+      stop: hashtag["indices"][1],
+      type: "hashtag",
+      text: "#" + hashtag["text"]
+    }));
+  }
+
+  private getUrlsForTweetArray() {
+    return this.entities["urls"].map(url => ({
+      start: url["indices"][0],
+      stop: url["indices"][1],
+      type: "url",
+      text: url["display_url"],
+      url: url["url"]
+    }));
+  }
+
+  private getTextPartsForTweetArray(specialParts) {
+    if (!specialParts.length) {
+      // text only
+      return [
+        {
+          start: 0,
+          stop: this.status.length,
+          type: "text",
+          text: this.status
+        }
+      ];
+    } else {
+      let res = [];
+      let start = 0;
+      for (let i = 0; i < specialParts.length; i++) {
+        if (start != specialParts[i].start) {
+          res.push({
+            start: start,
+            stop: specialParts[i]["start"],
+            type: "text",
+            text: this.status.substring(start, specialParts[i]["start"])
+          });
+        }
+        start = specialParts[i]["stop"];
+      }
+      const stopOfLastSpecialPart =
+        specialParts[specialParts.length - 1]["stop"];
+      const textUntilEnd = this.status.substring(
+        stopOfLastSpecialPart,
+        this.status.length
+      );
+      if (textUntilEnd.length) {
+        res.push({
+          start: stopOfLastSpecialPart,
+          stop: this.status.length,
+          type: "text",
+          text: textUntilEnd
+        });
+      }
+
+      return res;
+    }
+  }
+
+  private cutToTextRange(tweetArray) {
+    const res = [];
+    for (let i = 0; i < tweetArray.length; i++) {
+      if (
+        tweetArray[i]["start"] >= this.range[0] &&
+        tweetArray[i]["stop"] <= this.range[1]
+      ) {
+        // Inside the range
+        res.push(tweetArray[i]);
+      } else if (
+        tweetArray[i]["start"] < this.range[0] &&
+        tweetArray[i]["stop"] <= this.range[1]
+      ) {
+        // Beginning needs to be cut
+        res.push({
+          start: this.range[0],
+          stop: tweetArray[i]["stop"],
+          type: tweetArray[i]["type"],
+          text: tweetArray[i]["text"].substring(
+            this.range[0],
+            tweetArray[i]["stop"]
+          )
+        });
+      } else if (
+        tweetArray[i]["start"] >= this.range[0] &&
+        tweetArray[i]["stop"] > this.range[1]
+      ) {
+        // End needs to be cut
+        if (this.range[1] - tweetArray[i]["start"] > 0) {
+          res.push({
+            start: tweetArray[i]["start"],
+            stop: this.range[1],
+            type: tweetArray[i]["type"],
+            text: tweetArray[i]["text"].substring(
+              0,
+              this.range[1] - tweetArray[i]["start"]
+            )
+          });
+        }
+      } else if (
+        tweetArray[i]["start"] < this.range[0] &&
+        tweetArray[i]["stop"] > this.range[1]
+      ) {
+        // Start and end needs to be cut
+        res.push({
+          start: this.range[0],
+          stop: this.range[1],
+          type: tweetArray[i]["type"],
+          text: tweetArray[i]["text"].substring(
+            this.range[1] - tweetArray[i]["stop"],
+            tweetArray[i]["stop"] - this.range[1]
+          )
+        });
+      }
+    }
+    return res;
+  }
 }