Browse Source

encrytion flow for searching keys and encrypting while private tweet

rohit.gowda 4 years ago
parent
commit
d879fed5af
100 changed files with 25913 additions and 276 deletions
  1. 2 0
      app/src/app/app.module.ts
  2. 2 2
      app/src/components/feed/feed.ts
  3. 19 48
      app/src/pages/settings/settings.ts
  4. 1 10
      app/src/pages/write-tweet/write-tweet.html
  5. 37 157
      app/src/pages/write-tweet/write-tweet.ts
  6. 17 29
      app/src/providers/crypto/crypto.ts
  7. 35 19
      app/src/providers/feed/feed.ts
  8. 8 8
      app/src/providers/p2p-database-gun/p2p-database-gun.ts
  9. 12 3
      app/src/providers/p2p-storage-ipfs/p2p-storage-ipfs.ts
  10. 100 0
      app/src/providers/pgp-key-server/pgp-key-server.ts
  11. 3 0
      package-lock.json
  12. 1112 0
      patch.patch
  13. 94 0
      src1/app/app.component.ts
  14. 34 0
      src1/app/app.html
  15. 104 0
      src1/app/app.module.ts
  16. 60 0
      src1/app/app.scss
  17. 5 0
      src1/app/main.ts
  18. BIN
      src1/assets/icon/favicon.ico
  19. BIN
      src1/assets/imgs/background-login-sm.png
  20. BIN
      src1/assets/imgs/background-login.png
  21. BIN
      src1/assets/imgs/logo.png
  22. 21544 0
      src1/assets/scripts/openpgp.js
  23. 145 0
      src1/assets/scripts/openpgp.worker.js
  24. 36 0
      src1/components/components.module.ts
  25. 13 0
      src1/components/feed/feed.html
  26. 0 0
      src1/components/feed/feed.scss
  27. 36 0
      src1/components/feed/feed.ts
  28. 2 0
      src1/components/hashtag/hashtag.html
  29. 5 0
      src1/components/hashtag/hashtag.scss
  30. 18 0
      src1/components/hashtag/hashtag.ts
  31. 2 0
      src1/components/mention/mention.html
  32. 5 0
      src1/components/mention/mention.scss
  33. 20 0
      src1/components/mention/mention.ts
  34. 28 0
      src1/components/profile-header/profile-header.html
  35. 25 0
      src1/components/profile-header/profile-header.scss
  36. 45 0
      src1/components/profile-header/profile-header.ts
  37. 13 0
      src1/components/quoted-status/quoted-status.html
  38. 43 0
      src1/components/quoted-status/quoted-status.scss
  39. 24 0
      src1/components/quoted-status/quoted-status.ts
  40. 24 0
      src1/components/tweet-actions/tweet-actions.html
  41. 33 0
      src1/components/tweet-actions/tweet-actions.scss
  42. 93 0
      src1/components/tweet-actions/tweet-actions.ts
  43. 15 0
      src1/components/tweet-body/tweet-body.html
  44. 22 0
      src1/components/tweet-body/tweet-body.scss
  45. 204 0
      src1/components/tweet-body/tweet-body.ts
  46. 14 0
      src1/components/tweet-header/tweet-header.html
  47. 39 0
      src1/components/tweet-header/tweet-header.scss
  48. 107 0
      src1/components/tweet-header/tweet-header.ts
  49. 7 0
      src1/components/tweet/tweet.html
  50. 23 0
      src1/components/tweet/tweet.scss
  51. 34 0
      src1/components/tweet/tweet.ts
  52. 51 0
      src1/index.html
  53. 13 0
      src1/manifest.json
  54. 11 0
      src1/pages/about/about.html
  55. 13 0
      src1/pages/about/about.module.ts
  56. 3 0
      src1/pages/about/about.scss
  57. 11 0
      src1/pages/about/about.ts
  58. 18 0
      src1/pages/home/home.html
  59. 9 0
      src1/pages/home/home.module.ts
  60. 0 0
      src1/pages/home/home.scss
  61. 91 0
      src1/pages/home/home.ts
  62. 13 0
      src1/pages/login/login.html
  63. 9 0
      src1/pages/login/login.module.ts
  64. 33 0
      src1/pages/login/login.scss
  65. 53 0
      src1/pages/login/login.ts
  66. 15 0
      src1/pages/profile/profile.html
  67. 9 0
      src1/pages/profile/profile.module.ts
  68. 3 0
      src1/pages/profile/profile.scss
  69. 128 0
      src1/pages/profile/profile.ts
  70. 4 0
      src1/pages/search-results-tweets-popular/search-results-tweets-popular.html
  71. 13 0
      src1/pages/search-results-tweets-popular/search-results-tweets-popular.module.ts
  72. 5 0
      src1/pages/search-results-tweets-popular/search-results-tweets-popular.scss
  73. 80 0
      src1/pages/search-results-tweets-popular/search-results-tweets-popular.ts
  74. 4 0
      src1/pages/search-results-tweets-recent/search-results-tweets-recent.html
  75. 13 0
      src1/pages/search-results-tweets-recent/search-results-tweets-recent.module.ts
  76. 5 0
      src1/pages/search-results-tweets-recent/search-results-tweets-recent.scss
  77. 78 0
      src1/pages/search-results-tweets-recent/search-results-tweets-recent.ts
  78. 6 0
      src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.html
  79. 13 0
      src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.module.ts
  80. 2 0
      src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.scss
  81. 21 0
      src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.ts
  82. 24 0
      src1/pages/search-results-users/search-results-users.html
  83. 13 0
      src1/pages/search-results-users/search-results-users.module.ts
  84. 6 0
      src1/pages/search-results-users/search-results-users.scss
  85. 67 0
      src1/pages/search-results-users/search-results-users.ts
  86. 13 0
      src1/pages/search/search.html
  87. 0 0
      src1/pages/search/search.scss
  88. 26 0
      src1/pages/search/search.ts
  89. 40 0
      src1/pages/settings/settings.html
  90. 0 0
      src1/pages/settings/settings.scss
  91. 151 0
      src1/pages/settings/settings.ts
  92. 45 0
      src1/pages/write-tweet/write-tweet.html
  93. 9 0
      src1/pages/write-tweet/write-tweet.module.ts
  94. 53 0
      src1/pages/write-tweet/write-tweet.scss
  95. 379 0
      src1/pages/write-tweet/write-tweet.ts
  96. 18 0
      src1/pipes/diff-for-humans/diff-for-humans.ts
  97. 19 0
      src1/pipes/friendly-number/friendly-number.ts
  98. 15 0
      src1/pipes/high-resolution/high-resolution.ts
  99. 24 0
      src1/pipes/pipes.module.ts
  100. 20 0
      src1/pipes/replace-hashtags/replace-hashtags.ts

+ 2 - 0
app/src/app/app.module.ts

@@ -29,6 +29,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 { PgpKeyServerProvider } from "../providers/pgp-key-server/pgp-key-server";
 import { FeedProvider } from "../providers/feed/feed";
 import { MentionComponent } from "../components/mention/mention";
 import { HashtagComponent } from "../components/hashtag/hashtag";
@@ -96,6 +97,7 @@ import { CryptoProvider } from "../providers/crypto/crypto";
     TwitterApiProvider,
     P2pStorageIpfsProvider,
     P2pDatabaseGunProvider,
+    PgpKeyServerProvider,
     FeedProvider,
     CryptoProvider,
     Vibration

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

@@ -20,9 +20,9 @@ export class FeedComponent {
   @Input()
   enableInfiniteScroll: boolean = true;
   @Output()
-  onRefresh: EventEmitter<any> = new EventEmitter<any>();
+  onRefresh: EventEmitter < any > = new EventEmitter < any > ();
   @Output()
-  onLoadMore: EventEmitter<any> = new EventEmitter<any>();
+  onLoadMore: EventEmitter < any > = new EventEmitter < any > ();
 
   constructor() {}
 

+ 19 - 48
app/src/pages/settings/settings.ts

@@ -8,7 +8,7 @@ import {
 import { Storage } from "@ionic/storage";
 import { CryptoProvider } from "../../providers/crypto/crypto";
 import { SocialSharing } from "@ionic-native/social-sharing";
-import * as openpgp from 'openpgp';
+import { PgpKeyServerProvider } from "../../providers/pgp-key-server/pgp-key-server";
 
 @Component({
   selector: "page-settings",
@@ -18,8 +18,8 @@ export class SettingsPage {
   keywords: string;
   privateKey: string;
   publicKey: string;
+  revocationCertificate:string;
   email: string;
-  hkp = new openpgp.HKP('https://sks-keyservers.net/');
 
   constructor(
     public navCtrl: NavController,
@@ -28,7 +28,8 @@ export class SettingsPage {
     private storage: Storage,
     private loadingCtrl: LoadingController,
     private sharing: SocialSharing,
-    private alertCtrl: AlertController
+    private alertCtrl: AlertController,
+    private openpgp: PgpKeyServerProvider
   ) {
     this.loadValuesFromStorage();
   }
@@ -41,19 +42,16 @@ export class SettingsPage {
   }
 
   generateKeys() {
-    if (!this.email){
-          console.log("email is not provided or not valid");
-          return;
-    }
-    else{
+    if (!this.email) {
+      console.log("email is not provided or not valid");
+      return;
+    } else {
       this.storage.set("email", this.email);
       if (this.publicKey || this.privateKey) {
         const alert = this.alertCtrl.create({
           title: "Are you sure?",
-          subTitle:
-            "You already have keys entered. Do you want to overwrite them?",
-          buttons: [
-            {
+          subTitle: "You already have keys entered. Do you want to overwrite them?",
+          buttons: [{
               text: "No",
               role: "cancel"
             },
@@ -74,55 +72,28 @@ export class SettingsPage {
   }
 
   private async startKeyGeneration() {
-    const keys = await this.cryptoUtils.generatePgpKeys(this.email);
-    
+    const keys = await this.openpgp.generateKey("passphrase",this.email);
+
     this.privateKey = keys.privateKeyArmored;
     this.publicKey = keys.publicKeyArmored;
-    console.log("public key", this.privateKey);
+    this.revocationCertificate = keys.revocationCertificate;
+
+    console.log("private key", this.privateKey);
     console.log("public key", this.publicKey);
-    // this.privateKey = await this.cryptoUtils.extractPrivateKey(keys);
   }
 
+
   save() {
     this.storage.set("publicKey", this.publicKey);
     this.storage.set("privateKey", this.privateKey);
+    this.storage.set("revocationCert", this.revocationCertificate);
     this.storage.set("keywords", this.keywords ? this.keywords.trim() : "");
 
     this.showToast("Successfully saved!");
   }
 
-  async publishPublicKey() {    
-    await this.publishPublicKey2();
-    await this.lookupKeys(this.email);
-  }
-  
-  async publishPublicKey2() {      
-    const loading = this.loadingCtrl.create();
-      loading.present();
-      // console.log("Uploding publish public key", this.publicKey);
-      if(!this.publicKey) return;
-      this.hkp.upload(this.publicKey).then(function() {
-        console.log("Uploding public key");
-      });
-      loading.dismiss();
-      this.showToast("Public key has been published!");
-      //lookup key to verify it has been pubblished
-      // await this.lookupKeys(this.email);
-  }
-
-  public async lookupKeys(email:string){
-
-    var options = {
-        query: email
-    };
-
-    let armoredPubkey = await this.hkp.lookup(options);
-    let pubkey = await openpgp.key.readArmored(armoredPubkey);
-
-     console.log('Found public key:',pubkey);
-     // if(! (email == 'rohit.hosn@gmail.com'))
-     //   this.pk.push(pubkey.publicKeyArmored);
-     // return pubkey;
+  async publishPublicKey() {
+    await this.openpgp.publishPubKey(this.publicKey);
   }
 
   exportPrivateKey() {

+ 1 - 10
app/src/pages/write-tweet/write-tweet.html

@@ -3,43 +3,34 @@
     <ion-title>Tweet something</ion-title>
   </ion-navbar>
 </ion-header>
-
 <ion-content padding>
   <!-- Show tweet to retweet or quote (if passed to the page) -->
   <ion-label *ngIf="retweet" color="primary">Retweet</ion-label>
   <quoted-status *ngIf="retweet" [data]="retweet.data"></quoted-status>
-
   <!-- Show tweet to reply to -->
   <ion-label *ngIf="replyTweet" color="primary">Reply to</ion-label>
   <quoted-status *ngIf="replyTweet" [data]="replyTweet.data"></quoted-status>
-
   <!-- Form to write a tweet -->
   <form [formGroup]="tweet" (ngSubmit)="submitTweet()">
     <ion-item class="padding-0">
       <ion-label color="primary" stacked>Your tweet</ion-label>
       <ion-textarea type="text" formControlName="text" maxlength="140" [attr.rows]="4"></ion-textarea>
     </ion-item>
-
     <div class="actions">
       <span class="progress">
         <svg width="20" height="20" class="progress-circle">
           <circle class="background-stroke" cx="10" cy="10" r="8"></circle>
           <circle class="progress-stroke" cx="10" cy="10" r="8" transform="rotate(-90, 10, 10)" [style.strokeDashoffset]="tweetCharProgress"></circle>
         </svg>
-
         <span class="progress-stats">{{ (tweet.value.text).length }}/140</span>
       </span>
-
       <span class="network-switch">
         <ion-icon name="logo-twitter"></ion-icon>
         <ion-toggle checked="false" formControlName="p2p" color="dark"></ion-toggle>
         <ion-icon name="glasses"></ion-icon>
       </span>
-
       <ion-icon name="warning" *ngIf="showTrigger" class="warning" (click)="showTriggerInfo()"></ion-icon>
-
       <button ion-button type="submit" class="submit-tweet">tweet!</button>
     </div>
   </form>
-
-</ion-content>
+</ion-content>

+ 37 - 157
app/src/pages/write-tweet/write-tweet.ts

@@ -17,6 +17,7 @@ import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
 import { Storage } from "@ionic/storage";
 import { P2pStorageIpfsProvider } from "../../providers/p2p-storage-ipfs/p2p-storage-ipfs";
 import { P2pDatabaseGunProvider } from "../../providers/p2p-database-gun/p2p-database-gun";
+import { PgpKeyServerProvider } from "../../providers/pgp-key-server/pgp-key-server";
 import twittertext from "twitter-text";
 import { CryptoProvider } from "../../providers/crypto/crypto";
 import * as openpgp from 'openpgp';
@@ -35,10 +36,10 @@ export class WriteTweetPage {
   openpgp;
   privateKey;
   publicKey;
-  pk: any[]=[];
-  passp = 'super long and hard to guess secret' ;
+  pk: any[] = [];
+  passp = 'super long and hard to guess secret';
   hkp = new openpgp.HKP('https://sks-keyservers.net/');
- 
+
 
   constructor(
     public navCtrl: NavController,
@@ -50,6 +51,7 @@ export class WriteTweetPage {
     private ipfs: P2pStorageIpfsProvider,
     private gun: P2pDatabaseGunProvider,
     private cryptoUtils: CryptoProvider,
+    private opnpgp: PgpKeyServerProvider,
     private alertCtrl: AlertController
   ) {
     this.retweetId = this.navParams.get("tweetId");
@@ -61,106 +63,7 @@ export class WriteTweetPage {
     });
 
     this.addValidators();
-    // this.encryptDecryptFunction();
-  }
-
-
-//    public async encryptDecryptFunction () {
-//      await openpgp.initWorker({path:'assets/scripts/openpgp.worker.js'});
-//     let a =  await this.generateKeys();
-//     console.log('a is:',a.publicKeyArmored);
-//     let b  =  await this.generateKeys();
-//     console.log('b is:',b.publicKeyArmored);
-//     let c  =  await this.generateKeys();
-//     this.privateKey =c.privateKeyArmored;
-//     this.publicKey = c.publicKeyArmored;
-
-//     this.pk.push(a.publicKeyArmored);
-//     this.pk.push(b.publicKeyArmored);
-// //     this.pk = [`----BEGIN PGP PUBLIC KEY BLOCK-----
-// // Version: OpenPGP.js v4.7.1
-// // Comment: https://openpgpjs.org
-
-// // xjMEXfAn1xYJKwYBBAHaRw8BAQdAAMVNOABw8MBtrtYR8KC3tSro3wITyApT
-// // TVjKVCppD+DNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
-// // BQJd8CfXBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRD+efBRXzuMsfA7
-// // AQCEgoToFzv2hT9BREdiQp531/AHSyoZWmWvSZSvmga40gD8C+zwbCySnkhQ
-// // pb4L0DCKtSDa7pLg2g0OcxJlbSZWHQ3OOARd8CfXEgorBgEEAZdVAQUBAQdA
-// // p4mVY17dPWf6VCBqW10Ybk5JgUO6FK0OsETWw3gG2zcDAQgHwmEEGBYIAAkF
-// // Al3wJ9cCGwwACgkQ/nnwUV87jLFHbAD9GyoL7dcTDGQoqtrhKozdgnzfugTb
-// // er0bwU15WNMjefkA/jEqK9YUNcRrFKIuac9PVibGgutL8ak7ukysw6iTcCsM
-// // =fmhE
-// // -----END PGP PUBLIC KEY BLOCK-----`,
-// // `-----BEGIN PGP PUBLIC KEY BLOCK-----
-// // Version: OpenPGP.js v4.7.1
-// // Comment: https://openpgpjs.org
-
-// // xjMEXfAn1hYJKwYBBAHaRw8BAQdAsF1ivpd0HU8ogj02LDv6BTOxNMWGZaEc
-// // OyZBwqoYJPrNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
-// // BQJd8CfWBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRDEruv77flRJ32B
-// // AP93GIBcUW2okROoZZhdPVeqjRD72Ft64imXpdZ0jx4ohgEA5Kv9vs2kV73q
-// // k6fcdf7qD/i5gMExU0+vV05c9VxBYwfOOARd8CfWEgorBgEEAZdVAQUBAQdA
-// // 1J7E03ZopUnsIeNzeiZvba6qxhhUbpmBZ1aN1HhWUlEDAQgHwmEEGBYIAAkF
-// // Al3wJ9YCGwwACgkQxK7r++35USdTqQD/ZEg8X5tMx75nQe4mGlyiRjmmtWLw
-// // n9bslTdjBIszs/EA/R1WIm6ji4Ru1dJWc3ISisz78xTM2H8U7fnP8yjFcWcD
-// // =hgnW
-// // -----END PGP PUBLIC KEY BLOCK-----`];
-//     console.log('array of pub keys is :',this.pk);
-//      this.pk = this.pk.map(async (key) => {
-//       return (await openpgp.key.readArmored(key)).keys[0]
-//     });
-
-//     console.log('priv key: ',this.privateKey,'this.pubkey',this.pk);
-//     let encrypted;
-//     const privKeyObj = (await openpgp.key.readArmored(this.privateKey)).keys[0];
-//     console.log('privKeyObj',privKeyObj);
-//     const bla = await privKeyObj.decrypt(this.passp);
-
-//     // const options = {
-//     //     message: openpgp.message.fromText('Hello, World!'),       // input as Message object
-//     //     publicKeys: (await openpgp.key.readArmored(this.publicKey)).keys, // for encryption
-//     //     privateKeys: [privKeyObj]                                 // for signing (optional)
-//     // }
-
-//     const options = {
-//         message: openpgp.message.fromText('Hello, World!'),       // input as Message object
-//         publicKeys: await Promise.all(this.pk), // for encryption
-//         privateKeys: [privKeyObj]                                 // for signing (optional)
-//     }
-
-//     const ciphertext = await openpgp.encrypt(options);
-//       encrypted = ciphertext.data; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
-//         console.log('encrypted is:',encrypted);
-   
-//    let aprivKeyObj = (await openpgp.key.readArmored(a.privateKeyArmored)).keys[0];
-//    await aprivKeyObj.decrypt(this.passp);
-
-//     const options2 = {
-//         message: await openpgp.message.readArmored(encrypted),    // parse armored message
-//         privateKeys: [aprivKeyObj]                                 // for decryption
-//     }
-//     console.log('options2 is: ',options2);
-//     let plaintext = await openpgp.decrypt(options2);
-//     console.log('decrypted text is:',plaintext,plaintext.data);
-//     return plaintext.data // 'Hello, World!'
-
-//   }
-
-  // public async generateKeys(){
-  //   let options = {
-  //     userIds: [{ name:'Jon Smith', email:'jon@example.com' }], // multiple user IDs
-  //     curve: "ed25519",                                         // ECC curve name
-  //     passphrase: this.passp        // protects the private key
-  //   };
-   
-  //    let a = await openpgp.generateKey(options);
-  //    return a;
-  //     // console.log('resolved a = ',a);
-  //     //     this.privateKey =a.privateKeyArmored;
-  //     //     this.publicKey = a.publicKeyArmored;
-  //     //     this.encryptDecryptFunction();
-  // }
-
+   }
 
   private async addValidators() {
     const triggerWords = await this.storage.get("keywords");
@@ -172,7 +75,9 @@ export class WriteTweetPage {
   }
 
   private containsTriggerWord(triggerWords: string): ValidatorFn {
-    return (control: AbstractControl): { [key: string]: any } | null => {
+    return (control: AbstractControl): {
+      [key: string]: any
+    } | null => {
       if (triggerWords) {
         const regexList = triggerWords
           .toLowerCase()
@@ -180,9 +85,8 @@ export class WriteTweetPage {
           .join("|");
         const regex = new RegExp(regexList);
         const containsTriggerWord = regex.test(control.value.toLowerCase());
-        return containsTriggerWord
-          ? { containsTriggerWord: { value: control.value } }
-          : null;
+        return containsTriggerWord ? { containsTriggerWord: { value: control.value } } :
+          null;
       } else {
         return null;
       }
@@ -220,8 +124,7 @@ export class WriteTweetPage {
     this.alertCtrl
       .create({
         title: "Watch Out!",
-        message:
-          "Your tweet contains words you have previously defined to only share securely via P2P. Currently P2P mode is not selected.",
+        message: "Your tweet contains words you have previously defined to only share securely via P2P. Currently P2P mode is not selected.",
         buttons: ["OK"]
       })
       .present();
@@ -238,14 +141,19 @@ export class WriteTweetPage {
         (await this.cryptoUtils.isPrivateKeySet()) &&
         (await this.cryptoUtils.isPublicKeyPublished())
       ) {
+        console.log("yes in if");
         loading.setContent("Publish private tweet...");
-        await this.tweetPrivate();
-        } else {
+        let result = await this.tweetPrivate();
+        console.log("this result is", result, result.data);
+
+        // this.storeIPFS(result);
+
+      } else {
+        console.log("in else block");
         loading.dismiss();
         const alert = this.alertCtrl.create({
           title: "Oooops...",
-          message:
-            "Please verify that you have set a private and public key in the settings and that your latest public key was published."
+          message: "Please verify that you have set a private and public key in the settings and that your latest public key was published."
         });
         alert.present();
         return;
@@ -263,57 +171,27 @@ export class WriteTweetPage {
     this.navCtrl.pop();
   }
 
-   public async lookupKeys(email:string){
-     var options = {
-        query: email
-    };
-
-    let armoredPubkey = await this.hkp.lookup(options);
-    console.log('armord pubkey',armoredPubkey);
-
-    let pubkey = (await openpgp.key.readArmored(armoredPubkey));
-    console.log('array of opubkes returened from server',pubkey);
-
-     pubkey = (await openpgp.key.readArmored(armoredPubkey)).keys[0];
-    console.log('latest  pubkey is:',pubkey);
-     // console.log('Found public key:',pubkey);
-    
-       this.pk.push(pubkey);
-
-  }
-
   private async tweetPrivate() {
     const tweet = await this.buildPrivateTweet();
-    console.log('tweet is:',tweet.full_text);
+    console.log('private tweet is:', tweet.full_text);
     const privateKey = await this.storage.get("privateKey");
     //fetch followers and their public keys
     //assuming the email id of rohit.shiva.gowda
-    await this.lookupKeys("rohit.hosn@gmail.com");
-     console.log('array of pub keys is :', this.pk);
-    this.pk = this.pk.map(async (key) => {
-      console.log('key is:',key);
-      return (await openpgp.key.readArmored(key)).keys[0]
-    });
-
-    console.log("after mapping", this.pk);
+    await this.opnpgp.lookupKeys("rohit.hosn@gmail.com");
+    await this.opnpgp.lookupKeys("rohit.shiva.gowda@gmail.com");
+    //encrypt the tweet with multiple keys
+    let encryptedTweet = await this.opnpgp.encrypt(tweet.full_text);
 
-    const options = {
-      message: openpgp.message.fromText(tweet), // input as Message object
-      publicKeys: await Promise.all(this.pk), // for encryption
-      // privateKeys: [privKeyObj] // for signing (optional)
-    }
-
-    const ciphertext = await openpgp.encrypt(options);
-
-    const encryptedTweet = ciphertext.data;
-    console.log('encrypted tweet is:',encryptedTweet);
+    this.storeIPFS(encryptedTweet, tweet)
+    return await this.opnpgp.encrypt(tweet.full_text);
+    
+    
+  }
 
-    // const encryptedTweet = this.cryptoUtils.encrypt(
-    //   JSON.stringify(tweet),
-    //   privateKey
-    // );
-    const res = await this.ipfs.storeTweet(encryptedTweet);
+  private async storeIPFS(result, tweet){
 
+    const res = await this.ipfs.storeTweet(result);
+    console.log("pvt tweet hash is",res);
     this.gun.storeLastTweetHashForUser(
       tweet.user_id,
       res["Hash"],
@@ -321,6 +199,8 @@ export class WriteTweetPage {
     );
 
     this.gun.publishHashtags(tweet.entities.hashtags);
+
+
   }
 
   private async buildPrivateTweet() {
@@ -363,7 +243,7 @@ export class WriteTweetPage {
       } catch (err) {
         console.error(
           "There is no user signed up to twitter with username: " +
-            mention.screenName
+          mention.screenName
         );
       }
       return mention;

+ 17 - 29
app/src/providers/crypto/crypto.ts

@@ -124,8 +124,7 @@ export class CryptoProvider {
    * Generates a RSA key pair object
    */
   public async generateRsaKeys() {
-    return await crypto.subtle.generateKey(
-      {
+    return await crypto.subtle.generateKey({
         name: "RSA-OAEP",
         modulusLength: 1024,
         publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
@@ -136,28 +135,24 @@ export class CryptoProvider {
     );
   }
 
-  public async generatePgpKeys(email){
+  public async generatePgpKeys(email) {
     console.log("this is the mail of the iser ", email);
     let options = {
-      userIds: [{ name: 'Rohithosn', email:email }], // multiple user IDs
-      curve: "ed25519",                                         // ECC curve name
-      passphrase: "This is phas"        // protects the private key
+      userIds: [{ name: 'Rohithosn', email: email }], // multiple user IDs
+      curve: "ed25519", // ECC curve name
+      passphrase: "This is phas" // protects the private key
     };
-   
-     let a = await openpgp.generateKey(options);
-     console.log('resolved a = ',a);
-     return a;
-      
-      //     this.privateKey =a.privateKeyArmored;
-      //     this.publicKey = a.publicKeyArmored;
-      //     this.encryptDecryptFunction();
+
+    let a = await openpgp.generateKey(options);
+    console.log('resolved a = ', a);
+    return a;
   }
 
   /**
    * extracts the private key from the key object and transforms it to readable text
    * @param keys key object
    */
-  public async extractPrivateKey(keys): Promise<string> {
+  public async extractPrivateKey(keys): Promise < string > {
     return btoa(
       String.fromCharCode.apply(
         null,
@@ -170,7 +165,7 @@ export class CryptoProvider {
    * extracts the public key from the key object and transforms it to readable text
    * @param keys key object
    */
-  public async extractPublicKey(keys): Promise<string> {
+  public async extractPublicKey(keys): Promise < string > {
     return btoa(
       String.fromCharCode.apply(
         null,
@@ -182,25 +177,18 @@ export class CryptoProvider {
   /**
    * checks if the latest published key is the same as the one saved in app settings
    */
-  public async isPublicKeyPublished(): Promise<boolean> {
+  public async isPublicKeyPublished(): Promise < boolean > {
     const publicKey = await this.storage.get("publicKey");
-    return publicKey;
-    // const keyHistory = await this.getKeyHistory(this.ownUserId);
-
-    // if (keyHistory && publicKey.length) {
-    //   const newestPublicKey = keyHistory["keys"].reverse()[0]["key"];
-    //   return newestPublicKey === publicKey;
-    // } else {
-    //   return false;
-    // }
+    return publicKey?true:false;
+
   }
 
   /**
    * checks if a private key is already set
    */
-  public async isPrivateKeySet(): Promise<boolean> {
+  public async isPrivateKeySet(): Promise < boolean > {
     const privateKey = await this.storage.get("privateKey");
-    return privateKey;
+    return privateKey?true:false;
   }
 
   /**
@@ -231,7 +219,7 @@ export class CryptoProvider {
    * Fetches the public key history for a given user id
    * @param userId user id
    */
-  public async fetchPublicKeyHistoryForUser(userId: string): Promise<object[]> {
+  public async fetchPublicKeyHistoryForUser(userId: string): Promise < object[] > {
     const keyHistory = await this.getKeyHistory(userId);
     return keyHistory["keys"].reverse();
   }

+ 35 - 19
app/src/providers/feed/feed.ts

@@ -3,6 +3,7 @@ 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";
 import { CryptoProvider } from "../crypto/crypto";
+import { PgpKeyServerProvider } from "../../providers/pgp-key-server/pgp-key-server";
 import { Storage } from "@ionic/storage";
 @Injectable()
 export class FeedProvider {
@@ -14,6 +15,7 @@ export class FeedProvider {
     private gun: P2pDatabaseGunProvider,
     private ipfs: P2pStorageIpfsProvider,
     private cryptoUtils: CryptoProvider,
+    private opnpgp: PgpKeyServerProvider,
     private storage: Storage
   ) {
     this.storage.get("userId").then(userId => (this.userId = userId));
@@ -28,8 +30,8 @@ export class FeedProvider {
    */
   public async loadUserTimeline(
     userId,
-    oldestPublicTweet?,
-    oldestPrivateTweet?
+    oldestPublicTweet ? ,
+    oldestPrivateTweet ?
   ) {
     const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
     // Fetch tweets from Twitter
@@ -38,9 +40,9 @@ export class FeedProvider {
     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 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
@@ -68,16 +70,16 @@ export class FeedProvider {
    * @param oldestPublicTweet oldest public tweet
    * @param oldestPrivateTweet oldest private tweet
    */
-  public async loadHomeTimeline(oldestPublicTweet?, oldestPrivateTweet?) {
+  public async loadHomeTimeline(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 intervalStart: Date = oldestPrivateTweet ?
+      new Date(oldestPrivateTweet["created_at"]) :
+      new Date();
     const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
 
     // Fetch user's friends
@@ -88,8 +90,9 @@ export class FeedProvider {
       .map(friend => friend.id_str)
       .concat([this.userId]);
 
+      console.log('fetch hometimeline private tweets');
     // Fetch ipfs hashs for period
-    const promises: Promise<object[]>[] = friendsAndUserIds.map(accountId => {
+    const promises: Promise < object[] > [] = friendsAndUserIds.map(accountId => {
       return this.gun.fetchPrivateTweetHashsForUserInInterval(
         accountId,
         intervalStart,
@@ -97,6 +100,8 @@ export class FeedProvider {
       );
     });
 
+    console.log('fetched prvt tweet hashes ');
+
     const resolvedPromises = await Promise.all(promises);
     const privateTweetHashs = resolvedPromises.reduce(
       (privateTweets, el) => privateTweets.concat(el),
@@ -105,7 +110,7 @@ export class FeedProvider {
 
     if (privateTweetHashs.length > 0) {
       const privateTweets = await this.fetchPrivateTweets(privateTweetHashs);
-
+      if(!privateTweets) return tweets;
       // Combine and sort tweets
       return tweets
         .concat(privateTweets)
@@ -116,6 +121,7 @@ export class FeedProvider {
   }
 
   private async fetchPrivateTweets(privateTweetsData: object[]) {
+    console.log('fetching private tweets');
     const privateTweets = [];
 
     // Load private tweets from P2P storage
@@ -126,6 +132,12 @@ export class FeedProvider {
 
       // fetch from IPFS
       const encryptedTweet = await this.ipfs.fetchTweet(hash);
+      console.log('fetching private tweet',encryptedTweet);
+
+      if(!encryptedTweet){
+        console.log('encrypted tweet:',encryptedTweet)
+        return;
+      }
 
       // Fetch public key history for user
       const publicKeyHistory: object[] = await this.cryptoUtils.fetchPublicKeyHistoryForUser(
@@ -133,10 +145,14 @@ export class FeedProvider {
       );
 
       // Decrypt tweets
-      const decryptedTweet = this.cryptoUtils.decrypt(
-        encryptedTweet,
-        this.getPublicKeyAt(timestamp, publicKeyHistory)
-      );
+      // const decryptedTweet = this.cryptoUtils.decrypt(
+      //   encryptedTweet,
+      //   this.getPublicKeyAt(timestamp, publicKeyHistory)
+      // );
+      console.log('decrypting private tweets');
+    let pvtKey = await this.storage.get("privateKey");
+
+       const decryptedTweet = await this.opnpgp.decrypt(encryptedTweet,pvtKey);
       privateTweets.push(JSON.parse(decryptedTweet));
     }
 
@@ -169,12 +185,12 @@ export class FeedProvider {
     return "";
   }
 
-  private async addUserToTweet(tweet: object): Promise<object> {
+  private async addUserToTweet(tweet: object): Promise < object > {
     tweet["user"] = await this.twitter.fetchUser(tweet["user_id"]);
     return tweet;
   }
 
-  private async addQuotedStatusToTweet(tweet: object): Promise<object> {
+  private async addQuotedStatusToTweet(tweet: object): Promise < object > {
     if (!tweet["quoted_status_id"]) return tweet;
     const quoted_status = await this.twitter.fetchTweet(
       tweet["quoted_status_id"]
@@ -183,13 +199,13 @@ export class FeedProvider {
     return tweet;
   }
 
-  private async addOriginalStatusToTweet(tweet: object): Promise<object> {
+  private async addOriginalStatusToTweet(tweet: object): Promise < object > {
     if (!tweet["in_reply_to_status_id"]) return tweet;
     const originalTweet = await this.twitter.fetchTweet(
       tweet["in_reply_to_status_id"]
     );
     tweet["in_reply_to_screen_name"] =
-      originalTweet["data"]["user"]["screen_name"];
+    originalTweet["data"]["user"]["screen_name"];
     return tweet;
   }
 

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

@@ -15,7 +15,7 @@ export class P2pDatabaseGunProvider {
    * Hashtags are stored without reference to the users to provide these information on an extra dashboard to twitter
    * @param hashtagEntity extracted hashtags
    */
-  public async publishHashtags(hashtagEntity): Promise<void> {
+  public async publishHashtags(hashtagEntity): Promise < void > {
     const timestamp = new Date().setHours(0, 0, 1, 0);
     const hashtagsSeparated = hashtagEntity
       .map(el => el.hashtag)
@@ -29,10 +29,10 @@ export class P2pDatabaseGunProvider {
       .put({ hashtags: hashtagsSeparated });
 
     this.gun
-      .get(this.osnPrefix)
-      .get("hashtags")
-      .get(timestamp)
-      .set(hashtags);
+    .get(this.osnPrefix)
+    .get("hashtags")
+    .get(timestamp)
+    .set(hashtags);
   }
 
   /**
@@ -63,7 +63,7 @@ export class P2pDatabaseGunProvider {
     userId,
     intervalStart,
     intervalEnd
-  ): Promise<object[]> {
+  ): Promise < object[] > {
     const privateTweets = await this.gun
       .get(this.osnPrefix)
       .get("tweets")
@@ -73,8 +73,8 @@ export class P2pDatabaseGunProvider {
     if (privateTweets) {
       const entries = await Promise.all(
         Object.keys(privateTweets)
-          .filter(key => key !== "_")
-          .map(key => this.gun.get(key).then())
+        .filter(key => key !== "_")
+        .map(key => this.gun.get(key).then())
       );
 
       return entries

+ 12 - 3
app/src/providers/p2p-storage-ipfs/p2p-storage-ipfs.ts

@@ -12,6 +12,7 @@ export class P2pStorageIpfsProvider {
    * @param tweet tweet object
    */
   public storeTweet(tweet) {
+    console.log('storing tweet');
     return this.storeOnIPFS(tweet);
   }
 
@@ -46,19 +47,27 @@ export class P2pStorageIpfsProvider {
    * fetch tweet from ipfs for hash
    * @param hash address hash
    */
-  public fetchTweet(hash: string): Promise<string> {
+  public async fetchTweet(hash: string): Promise < string > {
+    console.log('address hash is:', hash);
+    let tweet;
     const options = {
       params: { arg: hash }
     };
 
-    return this.http.get<string>(this.infuraUrl + "cat", options).toPromise();
+    try {
+      tweet = await this.http.get < string > (this.infuraUrl + "cat", options).toPromise();
+    } catch (err) {
+      console.log("failed to resolve get promise",err);
+    }
+
+    return tweet;
   }
 
   /**
    * fetch multiple tweets from ipfs
    * @param hashs array of hashes
    */
-  public async fetchTweets(hashs: string[]): Promise<string[]> {
+  public async fetchTweets(hashs: string[]): Promise < string[] > {
     return await Promise.all(hashs.map(hash => this.fetchTweet(hash)));
   }
 }

+ 100 - 0
app/src/providers/pgp-key-server/pgp-key-server.ts

@@ -0,0 +1,100 @@
+import { Injectable } from "@angular/core";
+import { Storage } from "@ionic/storage";
+import * as openpgp from 'openpgp';
+
+@Injectable()
+export class PgpKeyServerProvider {
+
+  hkp = new openpgp.HKP('https://sks-keyservers.net/');
+  pk: any[] = [];
+  passphrase = "passphrase";
+
+  constructor(private storage: Storage) {}
+
+  public async generateKey(passphrase, email) {
+    let options = {
+      userIds: [{ email: email }], // multiple user IDs
+      curve: "ed25519", // ECC curve name
+      passphrase: this.passphrase // protects the private key
+    };
+
+    let a = await openpgp.generateKey(options);
+    console.log('the key generated is:', a);
+    return a;
+  }
+
+  public async publishPubKey(pubkey) {
+    console.log('passing pubkey to uplaoded : ', pubkey);
+    this.hkp.upload(pubkey).then(function() {
+      console.log('public key successfully uploaded');
+    });
+  }
+
+  public async lookupKeys(email: string) {
+    console.log('looking up keys for',email);
+    var options = {
+      query: email
+    };
+    let armoredPubkey = await this.hkp.lookup(options);
+    let pubkey = (await openpgp.key.readArmored(armoredPubkey)).keys[0];
+    console.log('Found latest public key:', pubkey);
+    this.pk.push(pubkey);
+    console.log('pk is:', this.pk);
+  }
+
+  /**
+   * Encrypt text with RSA
+   * @param plainText plain text
+   * @param privateKey private key
+   */
+  public async encrypt(plainText: string) {
+    // this.lookupKeys('rohit.shiva.gowda@gmail.com');
+    const options = {
+      message: openpgp.message.fromText(plainText), // input as Message object
+      publicKeys: await Promise.all(this.pk), // for encryption
+      // privateKey s: [privKeyObj] // for signing (optional)
+    }
+    console.log('options are:', options);
+    const ciphertext = await openpgp.encrypt(options);
+    console.log('encrypted text is:', ciphertext);
+    return ciphertext.data;
+  }
+
+  public async decrypt(encrypted: string,a) {
+    const privKeyObj = (await openpgp.key.readArmored(a)).keys[0];
+    console.log('privKeyObj', privKeyObj);
+    await privKeyObj.decrypt(this.passphrase);
+    console.log('a is:',a);
+    const options2 = {
+      message: await openpgp.message.readArmored(encrypted), // parse armored message
+      privateKeys: [privKeyObj] // for decryption
+    }
+    console.log('options2 is: ', options2);
+    let plaintext = await openpgp.decrypt(options2);
+    console.log('decrypted text is:', plaintext, plaintext.data);
+    return plaintext.data // 'Hello, World!'
+  }
+
+  public async revokeKey() {
+    //using revocation certificate
+    let pubkey = await this.storage.get("publicKey");
+    let atest = (await openpgp.key.readArmored(pubkey)).keys[0];
+    console.log('inside revoke key pubkey is:', atest);
+    let revocatnCert = this.storage.get("revocationCert");
+    try {
+      var options = {
+        key: atest,
+        revocationCertificate: revocatnCert
+      };
+
+      openpgp.revokeKey(options).then(function(key) {
+        console.log("public key revoked", key);
+      });
+
+    } catch (e) {
+      console.log('revoke failed1', e);
+    }
+
+  }
+
+}

+ 3 - 0
package-lock.json

@@ -0,0 +1,3 @@
+{
+  "lockfileVersion": 1
+}

+ 1112 - 0
patch.patch

@@ -0,0 +1,1112 @@
+diff --git app/ionic.config.json app/ionic.config.json
+index d2e1d8f..1b487ce 100644
+--- app/ionic.config.json
++++ app/ionic.config.json
+@@ -1,7 +1,9 @@
+ {
+   "name": "Hybric OSN",
+-  "integrations": {
+-    "cordova": {}
++  "integrations":
++  {
++    "cordova":
++    {}
+   },
+-  "type": "ionic-angular"
+-}
+\ No newline at end of file
++  "type": "angular"
++}
+diff --git app/package-lock.json app/package-lock.json
+index e686dba..d53d9e7 100644
+--- app/package-lock.json
++++ app/package-lock.json
+@@ -1,6 +1,6 @@
+ {
+-  "name": "HybridOSN",
+-  "version": "0.5.1",
++  "name": "hybrid-osn",
++  "version": "1.0.0",
+   "lockfileVersion": 1,
+   "requires": true,
+   "dependencies": {
+@@ -829,8 +829,7 @@
+     "bn.js": {
+       "version": "4.11.8",
+       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+-      "dev": true
++      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+     },
+     "body": {
+       "version": "5.1.0",
+@@ -2469,7 +2468,8 @@
+         },
+         "ansi-regex": {
+           "version": "2.1.1",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "aproba": {
+           "version": "1.2.0",
+@@ -2487,11 +2487,13 @@
+         },
+         "balanced-match": {
+           "version": "1.0.0",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "brace-expansion": {
+           "version": "1.1.11",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "balanced-match": "^1.0.0",
+             "concat-map": "0.0.1"
+@@ -2504,15 +2506,18 @@
+         },
+         "code-point-at": {
+           "version": "1.1.0",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "concat-map": {
+           "version": "0.0.1",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "console-control-strings": {
+           "version": "1.1.0",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "core-util-is": {
+           "version": "1.0.2",
+@@ -2615,7 +2620,8 @@
+         },
+         "inherits": {
+           "version": "2.0.3",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "ini": {
+           "version": "1.3.5",
+@@ -2625,6 +2631,7 @@
+         "is-fullwidth-code-point": {
+           "version": "1.0.0",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "number-is-nan": "^1.0.0"
+           }
+@@ -2637,17 +2644,20 @@
+         "minimatch": {
+           "version": "3.0.4",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "brace-expansion": "^1.1.7"
+           }
+         },
+         "minimist": {
+           "version": "0.0.8",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "minipass": {
+           "version": "2.2.4",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "safe-buffer": "^5.1.1",
+             "yallist": "^3.0.0"
+@@ -2664,6 +2674,7 @@
+         "mkdirp": {
+           "version": "0.5.1",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "minimist": "0.0.8"
+           }
+@@ -2736,7 +2747,8 @@
+         },
+         "number-is-nan": {
+           "version": "1.0.1",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "object-assign": {
+           "version": "4.1.1",
+@@ -2746,6 +2758,7 @@
+         "once": {
+           "version": "1.4.0",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "wrappy": "1"
+           }
+@@ -2821,7 +2834,8 @@
+         },
+         "safe-buffer": {
+           "version": "5.1.1",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "safer-buffer": {
+           "version": "2.1.2",
+@@ -2851,6 +2865,7 @@
+         "string-width": {
+           "version": "1.0.2",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "code-point-at": "^1.0.0",
+             "is-fullwidth-code-point": "^1.0.0",
+@@ -2868,6 +2883,7 @@
+         "strip-ansi": {
+           "version": "3.0.1",
+           "bundled": true,
++          "optional": true,
+           "requires": {
+             "ansi-regex": "^2.0.0"
+           }
+@@ -2906,11 +2922,13 @@
+         },
+         "wrappy": {
+           "version": "1.0.2",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         },
+         "yallist": {
+           "version": "3.0.2",
+-          "bundled": true
++          "bundled": true,
++          "optional": true
+         }
+       }
+     },
+@@ -3649,6 +3667,11 @@
+       "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+       "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
+     },
++    "imurmurhash": {
++      "version": "0.1.4",
++      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
++      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
++    },
+     "in-publish": {
+       "version": "2.0.0",
+       "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
+@@ -4367,8 +4390,7 @@
+     "minimalistic-assert": {
+       "version": "1.0.1",
+       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+-      "dev": true
++      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+     },
+     "minimalistic-crypto-utils": {
+       "version": "1.0.1",
+@@ -4566,6 +4588,14 @@
+         "vm-browserify": "0.0.4"
+       }
+     },
++    "node-localstorage": {
++      "version": "1.3.1",
++      "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz",
++      "integrity": "sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==",
++      "requires": {
++        "write-file-atomic": "^1.1.4"
++      }
++    },
+     "node-rsa": {
+       "version": "1.0.2",
+       "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.0.2.tgz",
+@@ -4838,6 +4868,33 @@
+         "wrappy": "1"
+       }
+     },
++    "openpgp": {
++      "version": "4.7.1",
++      "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-4.7.1.tgz",
++      "integrity": "sha512-UX4AXpjxUiIHfReStsBvbAcL5XyIWSb3sh7uleGlKdg1VWK/4LOncNd+89fYur6fbjZXORVR6m6YELupeWT1Iw==",
++      "requires": {
++        "asn1.js": "^5.0.0",
++        "node-fetch": "^2.1.2",
++        "node-localstorage": "~1.3.0"
++      },
++      "dependencies": {
++        "asn1.js": {
++          "version": "5.2.0",
++          "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz",
++          "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==",
++          "requires": {
++            "bn.js": "^4.0.0",
++            "inherits": "^2.0.1",
++            "minimalistic-assert": "^1.0.0"
++          }
++        },
++        "node-fetch": {
++          "version": "2.6.0",
++          "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
++          "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
++        }
++      }
++    },
+     "optjs": {
+       "version": "3.2.2",
+       "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz",
+@@ -5009,9 +5066,9 @@
+       "dev": true
+     },
+     "path-to-regexp": {
+-      "version": "1.7.0",
+-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
+-      "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
++      "version": "1.8.0",
++      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
++      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+       "requires": {
+         "isarray": "0.0.1"
+       },
+@@ -5774,6 +5831,11 @@
+       "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+       "dev": true
+     },
++    "slide": {
++      "version": "1.1.6",
++      "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
++      "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc="
++    },
+     "snapdragon": {
+       "version": "0.8.2",
+       "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
+@@ -7405,6 +7467,16 @@
+       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+     },
++    "write-file-atomic": {
++      "version": "1.3.4",
++      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz",
++      "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=",
++      "requires": {
++        "graceful-fs": "^4.1.11",
++        "imurmurhash": "^0.1.4",
++        "slide": "^1.1.5"
++      }
++    },
+     "ws": {
+       "version": "3.3.2",
+       "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz",
+diff --git app/src/app/app.component.ts app/src/app/app.component.ts
+index d35471a..761f19e 100644
+--- app/src/app/app.component.ts
++++ app/src/app/app.component.ts
+@@ -21,7 +21,7 @@ export class MyApp {
+   nav: Nav;
+ 
+   rootPage: any;
+-  pages: Array<{ title: string; icon: string; component: any }>;
++  pages: Array < { title: string;icon: string;component: any } > ;
+   user: any;
+ 
+   constructor(
+@@ -53,17 +53,20 @@ export class MyApp {
+ 
+   async initApp() {
+     const isLoggedIn = await this.authProvider.isLoggedIn();
+-
++    console.log('isLoggedin var is:', isLoggedIn);
++     this.rootPage = LoginPage;
+     if (isLoggedIn) {
+       this.rootPage = HomePage;
+       await this.setUser();
+     } else {
++      console.log('redirecting to login page');
+       this.rootPage = LoginPage;
+     }
+   }
+ 
+   async setUser() {
+     const userId = await this.storage.get("userId");
++    console.log('fetching user');
+     this.user = await this.twitter.fetchUser(userId);
+   }
+ 
+diff --git app/src/components/profile-header/profile-header.ts app/src/components/profile-header/profile-header.ts
+index e2ce94b..e29ffe0 100644
+--- app/src/components/profile-header/profile-header.ts
++++ app/src/components/profile-header/profile-header.ts
+@@ -13,7 +13,24 @@ export class ProfileHeaderComponent {
+   constructor(
+     private twitter: TwitterApiProvider,
+     private photoViewer: PhotoViewer
+-  ) {}
++  ) {
++
++    // if (typeof Worker !== 'undefined') {
++    //   // Create a new
++    //   const worker = new Worker('./profile-header.worker');
++    //   let message = "fuckgi";
++    //     worker.onmessage = (ev: MessageEvent) => {
++    //         worker.terminate();
++    //     };
++    //     worker.postMessage(message);
++    // } else {
++    //   console.log('webworkers are not supported');
++    //   return;
++    //   // Web Workers are not supported in this environment.
++    //   // You should add a fallback so that your program still executes correctly.
++    // }
++
++  }
+ 
+   get banner() {
+     if (this.user.profile_banner_url) {
+diff --git app/src/index.html app/src/index.html
+index c4b4376..64fb4d5 100644
+--- app/src/index.html
++++ app/src/index.html
+@@ -1,51 +1,44 @@
+ <!DOCTYPE html>
+ <html lang="en" dir="ltr">
++
+ <head>
+   <meta charset="UTF-8">
+   <title>Ionic App</title>
+   <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+   <meta name="format-detection" content="telephone=no">
+   <meta name="msapplication-tap-highlight" content="no">
+-
+   <link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico">
+   <link rel="manifest" href="manifest.json">
+   <meta name="theme-color" content="#4e8ef7">
+-
+   <!-- add to homescreen for ios -->
+   <meta name="apple-mobile-web-app-capable" content="yes">
+   <meta name="apple-mobile-web-app-status-bar-style" content="black">
+-
+   <!-- cordova.js required for cordova apps (remove if not needed) -->
+   <script src="cordova.js"></script>
+   <script src="assets/scripts/openpgp.js"></script>
+   <script src="assets/scripts/openpgp.worker.js"></script>
+-
+-  <!-- un-comment this code to enable service worker
+-  <script>
+-    if ('serviceWorker' in navigator) {
+-      navigator.serviceWorker.register('service-worker.js')
+-        .then(() => console.log('service worker installed'))
+-        .catch(err => console.error('Error', err));
+-    }
+-  </script>-->
+-
++  <!-- un-comment this code to enable service worker-->
++ <!--  <script>
++  if ('serviceWorker' in navigator) {
++    navigator.serviceWorker.register('service-worker.js')
++      .then(() => console.log('service worker installed'))
++      .catch(err => console.error('Error', err));
++  }
++
++  </script> -->
+   <link href="build/main.css" rel="stylesheet">
+-
+ </head>
+-<body>
+ 
++<body>
+   <!-- Ionic's root component and where the app will load -->
+   <ion-app></ion-app>
+-
+   <!-- The polyfills js is generated during the build process -->
+   <script src="build/polyfills.js"></script>
+-
+   <!-- The vendor js is generated during the build process
+        It contains all of the dependencies in node_modules -->
+   <script src="build/vendor.js"></script>
+-
+   <!-- The main bundle js is generated during the build process -->
+   <script src="build/main.js"></script>
+-
+ </body>
++
+ </html>
+diff --git app/src/pages/login/login.html app/src/pages/login/login.html
+index fb8f719..6da6d64 100644
+--- app/src/pages/login/login.html
++++ app/src/pages/login/login.html
+@@ -10,4 +10,4 @@
+     <button ion-button outline block (click)="login()">Login</button>
+     <a href="#" (click)="showAbout()">Learn more</a>
+   </div>
+-</ion-content>
+\ No newline at end of file
++</ion-content>
+diff --git app/src/pages/login/login.ts app/src/pages/login/login.ts
+index 691edbc..fac836e 100644
+--- app/src/pages/login/login.ts
++++ app/src/pages/login/login.ts
+@@ -35,8 +35,7 @@ export class LoginPage {
+   login() {
+     const alertText = {
+       title: "Login failed",
+-      subTitle:
+-        "Somthing went wrong while trying to log you in. Please try again.",
++      subTitle: "Somthing went wrong while trying to log you in. Please try again.",
+       buttons: ["OK"]
+     };
+ 
+diff --git app/src/pages/settings/settings.html app/src/pages/settings/settings.html
+index 9e1eba0..8143601 100644
+--- app/src/pages/settings/settings.html
++++ app/src/pages/settings/settings.html
+@@ -24,6 +24,8 @@
+         generate
+         a pair of keys. If you run the app on multiple devices, please enter everywhere the same pair of keys.</p>
+       <button ion-button block (click)="generateKeys()">Generate keys</button>
++      <ion-label color="primary" stacked required>Enter Passphrase:</ion-label>
++      <ion-textarea [(ngModel)]="passphrase"></ion-textarea>
+       <ion-label color="primary" stacked>Private Key:</ion-label>
+       <ion-textarea [(ngModel)]="privateKey"></ion-textarea>
+       <ion-label color="primary" stacked>Public Key:</ion-label>
+@@ -35,4 +37,4 @@
+   </ion-card>
+ 
+   <button ion-button block (click)="save()">Save settings</button>
+-</ion-content>
+\ No newline at end of file
++</ion-content>
+diff --git app/src/pages/settings/settings.ts app/src/pages/settings/settings.ts
+index c2f6bfd..1d966e9 100644
+--- app/src/pages/settings/settings.ts
++++ app/src/pages/settings/settings.ts
+@@ -17,6 +17,7 @@ export class SettingsPage {
+   keywords: string;
+   privateKey: string;
+   publicKey: string;
++  passphrase: string;
+ 
+   constructor(
+     public navCtrl: NavController,
+@@ -33,6 +34,7 @@ export class SettingsPage {
+   async loadValuesFromStorage() {
+     this.privateKey = await this.storage.get("privateKey");
+     this.publicKey = await this.storage.get("publicKey");
++    this.passphrase = await this.storage.get("passphrase");
+     this.keywords = await this.storage.get("keywords");
+   }
+ 
+@@ -63,7 +65,8 @@ export class SettingsPage {
+   }
+ 
+   private async startKeyGeneration() {
+-    const keys = await this.cryptoUtils.generateRsaKeys();
++   // const keys = await this.cryptoUtils.generateRsaKeys();
++    const keys = await this.cryptoUtils.generateKeys(this.passphrase);
+ 
+     this.publicKey = await this.cryptoUtils.extractPublicKey(keys);
+     this.privateKey = await this.cryptoUtils.extractPrivateKey(keys);
+@@ -73,6 +76,7 @@ export class SettingsPage {
+     this.storage.set("publicKey", this.publicKey);
+     this.storage.set("privateKey", this.privateKey);
+     this.storage.set("keywords", this.keywords ? this.keywords.trim() : "");
++    this.storage.set("passphrase", this.passphrase);
+ 
+     this.showToast("Successfully saved!");
+   }
+diff --git app/src/pages/write-tweet/write-tweet.ts app/src/pages/write-tweet/write-tweet.ts
+index f468d43..4b9c8f7 100644
+--- app/src/pages/write-tweet/write-tweet.ts
++++ app/src/pages/write-tweet/write-tweet.ts
+@@ -35,9 +35,10 @@ export class WriteTweetPage {
+   openpgp;
+   privateKey;
+   publicKey;
+-  pk: any[]=[];
+-  passp = 'super long and hard to guess secret' ;
+- 
++  revocationCertificate;
++  pk: any[] = [];
++  passp = 'super long and hard to guess secret';
++
+ 
+   constructor(
+     public navCtrl: NavController,
+@@ -64,100 +65,108 @@ export class WriteTweetPage {
+   }
+ 
+ 
+-   public async encryptDecryptFunction () {
+-     await openpgp.initWorker({path:'assets/scripts/openpgp.worker.js'});
+-    let a =  await this.generateKeys();
+-    console.log('a is:',a.publicKeyArmored);
+-    let b  =  await this.generateKeys();
+-    console.log('b is:',b.publicKeyArmored);
+-    let c  =  await this.generateKeys();
+-    this.privateKey =c.privateKeyArmored;
++  public async encryptDecryptFunction() {
++    await openpgp.initWorker({ path: 'assets/scripts/openpgp.worker.js' });
++    let a = await this.generateKeys();
++    console.log('a is:', a.publicKeyArmored);
++    let b = await this.generateKeys();
++    console.log('b is:', b.publicKeyArmored);
++    let c = await this.generateKeys();
++    this.privateKey = c.privateKeyArmored;
+     this.publicKey = c.publicKeyArmored;
+-
++    await this.uploadPubKeytoServer(this.publicKey);
++    this.revocationCertificate = c.revocationCertificate;
+     this.pk.push(a.publicKeyArmored);
+     this.pk.push(b.publicKeyArmored);
+-//     this.pk = [`----BEGIN PGP PUBLIC KEY BLOCK-----
+-// Version: OpenPGP.js v4.7.1
+-// Comment: https://openpgpjs.org
+-
+-// xjMEXfAn1xYJKwYBBAHaRw8BAQdAAMVNOABw8MBtrtYR8KC3tSro3wITyApT
+-// TVjKVCppD+DNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
+-// BQJd8CfXBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRD+efBRXzuMsfA7
+-// AQCEgoToFzv2hT9BREdiQp531/AHSyoZWmWvSZSvmga40gD8C+zwbCySnkhQ
+-// pb4L0DCKtSDa7pLg2g0OcxJlbSZWHQ3OOARd8CfXEgorBgEEAZdVAQUBAQdA
+-// p4mVY17dPWf6VCBqW10Ybk5JgUO6FK0OsETWw3gG2zcDAQgHwmEEGBYIAAkF
+-// Al3wJ9cCGwwACgkQ/nnwUV87jLFHbAD9GyoL7dcTDGQoqtrhKozdgnzfugTb
+-// er0bwU15WNMjefkA/jEqK9YUNcRrFKIuac9PVibGgutL8ak7ukysw6iTcCsM
+-// =fmhE
+-// -----END PGP PUBLIC KEY BLOCK-----`,
+-// `-----BEGIN PGP PUBLIC KEY BLOCK-----
+-// Version: OpenPGP.js v4.7.1
+-// Comment: https://openpgpjs.org
+-
+-// xjMEXfAn1hYJKwYBBAHaRw8BAQdAsF1ivpd0HU8ogj02LDv6BTOxNMWGZaEc
+-// OyZBwqoYJPrNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
+-// BQJd8CfWBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRDEruv77flRJ32B
+-// AP93GIBcUW2okROoZZhdPVeqjRD72Ft64imXpdZ0jx4ohgEA5Kv9vs2kV73q
+-// k6fcdf7qD/i5gMExU0+vV05c9VxBYwfOOARd8CfWEgorBgEEAZdVAQUBAQdA
+-// 1J7E03ZopUnsIeNzeiZvba6qxhhUbpmBZ1aN1HhWUlEDAQgHwmEEGBYIAAkF
+-// Al3wJ9YCGwwACgkQxK7r++35USdTqQD/ZEg8X5tMx75nQe4mGlyiRjmmtWLw
+-// n9bslTdjBIszs/EA/R1WIm6ji4Ru1dJWc3ISisz78xTM2H8U7fnP8yjFcWcD
+-// =hgnW
+-// -----END PGP PUBLIC KEY BLOCK-----`];
+-    console.log('array of pub keys is :',this.pk);
+-     this.pk = this.pk.map(async (key) => {
++    console.log('array of pub keys is :', this.pk);
++    this.pk = this.pk.map(async (key) => {
+       return (await openpgp.key.readArmored(key)).keys[0]
+     });
+ 
+-    console.log('priv key: ',this.privateKey,'this.pubkey',this.pk);
++    console.log('priv key: ', this.privateKey, 'this.pubkey', this.pk);
+     let encrypted;
+     const privKeyObj = (await openpgp.key.readArmored(this.privateKey)).keys[0];
+-    console.log('privKeyObj',privKeyObj);
+-    const bla = await privKeyObj.decrypt(this.passp);
+-
+-    // const options = {
+-    //     message: openpgp.message.fromText('Hello, World!'),       // input as Message object
+-    //     publicKeys: (await openpgp.key.readArmored(this.publicKey)).keys, // for encryption
+-    //     privateKeys: [privKeyObj]                                 // for signing (optional)
+-    // }
+-
++    console.log('privKeyObj', privKeyObj);
+     const options = {
+-        message: openpgp.message.fromText('Hello, World!'),       // input as Message object
+-        publicKeys: await Promise.all(this.pk), // for encryption
+-        privateKeys: [privKeyObj]                                 // for signing (optional)
++      message: openpgp.message.fromText('Hello, World!'), // input as Message object
++      publicKeys: await Promise.all(this.pk), // for encryption
++      privateKeys: [privKeyObj] // for signing (optional)
+     }
+ 
+     const ciphertext = await openpgp.encrypt(options);
+-      encrypted = ciphertext.data; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
+-        console.log('encrypted is:',encrypted);
+-   
+-   let aprivKeyObj = (await openpgp.key.readArmored(a.privateKeyArmored)).keys[0];
+-   await aprivKeyObj.decrypt(this.passp);
++    encrypted = ciphertext.data; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
++    console.log('encrypted is:', encrypted);
++
++    let aprivKeyObj = (await openpgp.key.readArmored(a.privateKeyArmored)).keys[0];
++    await aprivKeyObj.decrypt(this.passp);
+ 
+     const options2 = {
+-        message: await openpgp.message.readArmored(encrypted),    // parse armored message
+-        privateKeys: [aprivKeyObj]                                 // for decryption
++      message: await openpgp.message.readArmored(encrypted), // parse armored message
++      privateKeys: [aprivKeyObj] // for decryption
+     }
+-    console.log('options2 is: ',options2);
++    console.log('options2 is: ', options2);
+     let plaintext = await openpgp.decrypt(options2);
+-    console.log('decrypted text is:',plaintext,plaintext.data);
++    console.log('decrypted text is:', plaintext, plaintext.data);
++
++    //lookup key
++    await this.lookupKeys();
++    await this.test();
+     return plaintext.data // 'Hello, World!'
+ 
+   }
+ 
+-  public async generateKeys(){
++  public async test(){
++    console.log("----------------INSIDE TEST-----------------");
++    await this.revokeKey();
++    await this.lookupKeys();
++  }
++
++  public async generateKeys() {
+     let options = {
+-      userIds: [{ name:'Jon Smith', email:'jon@example.com' }], // multiple user IDs
+-      curve: "ed25519",                                         // ECC curve name
+-      passphrase: this.passp        // protects the private key
++      userIds: [{ name: 'Rohit Gowda', email: 'rohit.s.gowda91@gmail.com' }], // multiple user IDs
++      curve: "ed25519", // ECC curve name
++      passphrase: this.passp // protects the private key
+     };
+-   
+-     let a = await openpgp.generateKey(options);
+-     return a;
+-      // console.log('resolved a = ',a);
+-      //     this.privateKey =a.privateKeyArmored;
+-      //     this.publicKey = a.publicKeyArmored;
+-      //     this.encryptDecryptFunction();
++
++    let a = await openpgp.generateKey(options);
++    return a;
++  }
++
++  public async uploadPubKeytoServer(pubkey){
++    var hkp = new openpgp.HKP('https://pgp.mit.edu');
++    hkp.upload(pubkey).then(function() { 
++      console.log("pubkey succesfully uploaded");
++    });
++  }
++
++  public async lookupKeys(){
++    var hkp = new openpgp.HKP(); // Defaults to https://keyserver.ubuntu.com, or pass another keyserver URL as a string
++
++    var options = {
++        query: 'rohit.s.gowda91@gmail.com'
++    };
++
++    let armoredPubkey = await hkp.lookup(options);
++    let pubkey = await openpgp.key.readArmored(armoredPubkey);
++    console.log('Found public key:',pubkey);
++  }
++
++  public async revokeKey(){
++    //using revocation certificate
++    var options = {
++        key: openpgp.key.readArmored(this.publicKey).keys[0],
++        revocationCertificate: this.revocationCertificate
++    };
++
++    //alternatively revoke using private key
++    let options2 = {
++            key: openpgp.key.readArmored(this.privateKey).keys[0]
++        };
++      
++      openpgp.revokeKey(options2).then(function(key) {
++          var pubkey = key.publicKeyArmored;   // '-----BEGIN PGP PUBLIC KEY BLOCK ... '
++          console.log("public key revoked");
++      });
+   }
+ 
+ 
+@@ -171,7 +180,8 @@ export class WriteTweetPage {
+   }
+ 
+   private containsTriggerWord(triggerWords: string): ValidatorFn {
+-    return (control: AbstractControl): { [key: string]: any } | null => {
++    return (control: AbstractControl): {
++      [key: string]: any } | null => {
+       if (triggerWords) {
+         const regexList = triggerWords
+           .toLowerCase()
+@@ -179,9 +189,9 @@ export class WriteTweetPage {
+           .join("|");
+         const regex = new RegExp(regexList);
+         const containsTriggerWord = regex.test(control.value.toLowerCase());
+-        return containsTriggerWord
+-          ? { containsTriggerWord: { value: control.value } }
+-          : null;
++        return containsTriggerWord ?
++          { containsTriggerWord: { value: control.value } } :
++          null;
+       } else {
+         return null;
+       }
+@@ -219,8 +229,7 @@ export class WriteTweetPage {
+     this.alertCtrl
+       .create({
+         title: "Watch Out!",
+-        message:
+-          "Your tweet contains words you have previously defined to only share securely via P2P. Currently P2P mode is not selected.",
++        message: "Your tweet contains words you have previously defined to only share securely via P2P. Currently P2P mode is not selected.",
+         buttons: ["OK"]
+       })
+       .present();
+@@ -238,12 +247,11 @@ export class WriteTweetPage {
+       ) {
+         loading.setContent("Publish private tweet...");
+         await this.tweetPrivate();
+-        } else {
++      } else {
+         loading.dismiss();
+         const alert = this.alertCtrl.create({
+           title: "Oooops...",
+-          message:
+-            "Please verify that you have set a private and public key in the settings and that your latest public key was published."
++          message: "Please verify that you have set a private and public key in the settings and that your latest public key was published."
+         });
+         alert.present();
+         return;
+@@ -319,7 +327,7 @@ export class WriteTweetPage {
+       } catch (err) {
+         console.error(
+           "There is no user signed up to twitter with username: " +
+-            mention.screenName
++          mention.screenName
+         );
+       }
+       return mention;
+diff --git app/src/providers/crypto/crypto.ts app/src/providers/crypto/crypto.ts
+index 8714a1d..98a67e8 100644
+--- app/src/providers/crypto/crypto.ts
++++ app/src/providers/crypto/crypto.ts
+@@ -2,6 +2,7 @@ import { Injectable } from "@angular/core";
+ import { TwitterApiProvider } from "../twitter-api/twitter-api";
+ import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs";
+ import { Storage } from "@ionic/storage";
++import * as openpgp from 'openpgp';
+ import NodeRSA from "node-rsa";
+ declare var TextDecoder: any;
+ declare var TextEncoder: any;
+@@ -10,6 +11,7 @@ export class CryptoProvider {
+   ownUserId: string;
+   IV_LENGTH = 12;
+   HYBRID_OSN_AES_KEY = "Z1vxAULQnZdoWhJOvv+hWEvVpyUHzNjD/ichEE2c8i4=";
++  openpgp;
+ 
+   constructor(
+     private twitter: TwitterApiProvider,
+@@ -46,13 +48,19 @@ export class CryptoProvider {
+       };
+     }
+ 
+-    // Ecnrypt key history
+-    const encryptedPublicKeyHistory = await this.aesEncrypt(
+-      JSON.stringify(publicKeyHistory)
+-    );
++    //encrypt key history with openpgp with published public key
++    const options = {
++      message: openpgp.message.fromText(JSON.stringify(publicKeyHistory)), // input as Message object
++      publicKeys: (await openpgp.key.readArmored(key)).keys, // for encryption
++    }
++
++    let encryptedPublicKeyHistory = await openpgp.encrypt(options);
++    let encrypted = await encryptedPublicKeyHistory.data // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
++    console.log('encrypted public key history', encrypted);
++
+ 
+     // Publish updated key history...
+-    const res = await this.ipfs.storePublicKey(encryptedPublicKeyHistory);
++    const res = await this.ipfs.storePublicKey(encrypted);
+ 
+     // tweet ipfs link
+     const tweetResponse = await this.twitter.tweet(
+@@ -85,9 +93,21 @@ export class CryptoProvider {
+     // Fetch public key history
+     if (link.length) {
+       const encryptedKeyHistory = await this.ipfs.fetchJson(link);
+-      // Decrypt key history
+-      const keyHistory = await this.aesDecrypt(encryptedKeyHistory.toString());
+-      return JSON.parse(keyHistory);
++
++      let privateKey = await this.storage.get("privateKey");
++      const privKeyObj = (await openpgp.key.readArmored(privateKey)).keys[0];
++      console.log('privKeyObj', privKeyObj);
++      const options = {
++        message: await openpgp.message.readArmored(encryptedKeyHistory.toString()), // parse armored message
++        // publicKeys: (await openpgp.key.readArmored(pubkey)).keys, // for verification (optional)
++        privateKeys: [privKeyObj] // for decryption
++      }
++
++      openpgp.decrypt(options).then(plaintext => {
++        console.log('decrypted key history: ', plaintext.data)
++        return plaintext.data // 'Hello, World!'
++      })
++
+     } else {
+       return null;
+     }
+@@ -123,8 +143,7 @@ export class CryptoProvider {
+    * Generates a RSA key pair object
+    */
+   public async generateRsaKeys() {
+-    return await crypto.subtle.generateKey(
+-      {
++    return await crypto.subtle.generateKey({
+         name: "RSA-OAEP",
+         modulusLength: 1024,
+         publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+@@ -135,36 +154,40 @@ export class CryptoProvider {
+     );
+   }
+ 
++  /**
++   * Generates an openPGP key pair object
++   */
++
++  public async generateKeys(passp: string) {
++    let options = {
++      userIds: [{ name: 'Rohit Gowda', email: 'rohit.s.gowda91@gmail.com' }], // multiple user IDs
++      curve: "ed25519", // ECC curve name
++      passphrase: passp // protects the private key
++    };
++
++    return await openpgp.generateKey(options);
++  }
++
+   /**
+    * extracts the private key from the key object and transforms it to readable text
+    * @param keys key object
+    */
+-  public async extractPrivateKey(keys): Promise<string> {
+-    return btoa(
+-      String.fromCharCode.apply(
+-        null,
+-        new Uint8Array(await crypto.subtle.exportKey("pkcs8", keys.privateKey))
+-      )
+-    );
++  public async extractPrivateKey(keys): Promise < string > {
++    return keys.privateKeyArmored;
+   }
+ 
+   /**
+    * extracts the public key from the key object and transforms it to readable text
+    * @param keys key object
+    */
+-  public async extractPublicKey(keys): Promise<string> {
+-    return btoa(
+-      String.fromCharCode.apply(
+-        null,
+-        new Uint8Array(await crypto.subtle.exportKey("spki", keys.publicKey))
+-      )
+-    );
++  public async extractPublicKey(keys): Promise < string > {
++    return keys.publicKeyArmored;
+   }
+ 
+   /**
+    * checks if the latest published key is the same as the one saved in app settings
+    */
+-  public async isPublicKeyPublished(): Promise<boolean> {
++  public async isPublicKeyPublished(): Promise < boolean > {
+     const publicKey = await this.storage.get("publicKey");
+     const keyHistory = await this.getKeyHistory(this.ownUserId);
+ 
+@@ -179,7 +202,7 @@ export class CryptoProvider {
+   /**
+    * checks if a private key is already set
+    */
+-  public async isPrivateKeySet(): Promise<boolean> {
++  public async isPrivateKeySet(): Promise < boolean > {
+     const privateKey = await this.storage.get("privateKey");
+     return privateKey;
+   }
+@@ -212,7 +235,7 @@ export class CryptoProvider {
+    * Fetches the public key history for a given user id
+    * @param userId user id
+    */
+-  public async fetchPublicKeyHistoryForUser(userId: string): Promise<object[]> {
++  public async fetchPublicKeyHistoryForUser(userId: string): Promise < object[] > {
+     const keyHistory = await this.getKeyHistory(userId);
+     return keyHistory["keys"].reverse();
+   }
+diff --git app/src/providers/feed/feed.ts app/src/providers/feed/feed.ts
+index e87a22f..d4a3cc8 100644
+--- app/src/providers/feed/feed.ts
++++ app/src/providers/feed/feed.ts
+@@ -28,20 +28,23 @@ export class FeedProvider {
+    */
+   public async loadUserTimeline(
+     userId,
+-    oldestPublicTweet?,
+-    oldestPrivateTweet?
++    oldestPublicTweet ? ,
++    oldestPrivateTweet ?
+   ) {
++    console.log('fetch usertimeline');
+     const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
+     // Fetch tweets from Twitter
+     let tweets = await this.twitter.fetchUserTimeline(userId, maxId);
++    console.log('tweets are:',tweets);
+     if (tweets.length === 0) return tweets;
+     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 intervalStart: Date = oldestPrivateTweet ?
++      new Date(oldestPrivateTweet["created_at"]) :
++      new Date();
+     const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
++    console.log('intervalEnd',intervalEnd);
+ 
+     // Fetch private tweet hashs from P2P DB for corresponding interval
+     const privateTweetHashs: object[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
+@@ -68,16 +71,16 @@ export class FeedProvider {
+    * @param oldestPublicTweet oldest public tweet
+    * @param oldestPrivateTweet oldest private tweet
+    */
+-  public async loadHomeTimeline(oldestPublicTweet?, oldestPrivateTweet?) {
++  public async loadHomeTimeline(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 intervalStart: Date = oldestPrivateTweet ?
++      new Date(oldestPrivateTweet["created_at"]) :
++      new Date();
+     const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
+ 
+     // Fetch user's friends
+@@ -89,7 +92,7 @@ export class FeedProvider {
+       .concat([this.userId]);
+ 
+     // Fetch ipfs hashs for period
+-    const promises: Promise<object[]>[] = friendsAndUserIds.map(accountId => {
++    const promises: Promise < object[] > [] = friendsAndUserIds.map(accountId => {
+       return this.gun.fetchPrivateTweetHashsForUserInInterval(
+         accountId,
+         intervalStart,
+@@ -169,12 +172,12 @@ export class FeedProvider {
+     return "";
+   }
+ 
+-  private async addUserToTweet(tweet: object): Promise<object> {
++  private async addUserToTweet(tweet: object): Promise < object > {
+     tweet["user"] = await this.twitter.fetchUser(tweet["user_id"]);
+     return tweet;
+   }
+ 
+-  private async addQuotedStatusToTweet(tweet: object): Promise<object> {
++  private async addQuotedStatusToTweet(tweet: object): Promise < object > {
+     if (!tweet["quoted_status_id"]) return tweet;
+     const quoted_status = await this.twitter.fetchTweet(
+       tweet["quoted_status_id"]
+@@ -183,13 +186,13 @@ export class FeedProvider {
+     return tweet;
+   }
+ 
+-  private async addOriginalStatusToTweet(tweet: object): Promise<object> {
++  private async addOriginalStatusToTweet(tweet: object): Promise < object > {
+     if (!tweet["in_reply_to_status_id"]) return tweet;
+     const originalTweet = await this.twitter.fetchTweet(
+       tweet["in_reply_to_status_id"]
+     );
+     tweet["in_reply_to_screen_name"] =
+-      originalTweet["data"]["user"]["screen_name"];
++    originalTweet["data"]["user"]["screen_name"];
+     return tweet;
+   }
+ 
+diff --git app/src/providers/p2p-database-gun/p2p-database-gun.ts app/src/providers/p2p-database-gun/p2p-database-gun.ts
+index 213173e..8d80de1 100644
+--- app/src/providers/p2p-database-gun/p2p-database-gun.ts
++++ app/src/providers/p2p-database-gun/p2p-database-gun.ts
+@@ -15,7 +15,7 @@ export class P2pDatabaseGunProvider {
+    * Hashtags are stored without reference to the users to provide these information on an extra dashboard to twitter
+    * @param hashtagEntity extracted hashtags
+    */
+-  public async publishHashtags(hashtagEntity): Promise<void> {
++  public async publishHashtags(hashtagEntity): Promise < void > {
+     const timestamp = new Date().setHours(0, 0, 1, 0);
+     const hashtagsSeparated = hashtagEntity
+       .map(el => el.hashtag)
+@@ -29,10 +29,10 @@ export class P2pDatabaseGunProvider {
+       .put({ hashtags: hashtagsSeparated });
+ 
+     this.gun
+-      .get(this.osnPrefix)
+-      .get("hashtags")
+-      .get(timestamp)
+-      .set(hashtags);
++    .get(this.osnPrefix)
++    .get("hashtags")
++    .get(timestamp)
++    .set(hashtags);
+   }
+ 
+   /**
+@@ -63,7 +63,7 @@ export class P2pDatabaseGunProvider {
+     userId,
+     intervalStart,
+     intervalEnd
+-  ): Promise<object[]> {
++  ): Promise < object[] > {
+     const privateTweets = await this.gun
+       .get(this.osnPrefix)
+       .get("tweets")
+@@ -73,8 +73,8 @@ export class P2pDatabaseGunProvider {
+     if (privateTweets) {
+       const entries = await Promise.all(
+         Object.keys(privateTweets)
+-          .filter(key => key !== "_")
+-          .map(key => this.gun.get(key).then())
++        .filter(key => key !== "_")
++        .map(key => this.gun.get(key).then())
+       );
+ 
+       return entries
+diff --git app/tsconfig.json app/tsconfig.json
+index 5d53142..2ce1629 100644
+--- app/tsconfig.json
++++ app/tsconfig.json
+@@ -1,5 +1,6 @@
+ {
+-  "compilerOptions": {
++  "compilerOptions":
++  {
+     "allowSyntheticDefaultImports": true,
+     "declaration": false,
+     "emitDecoratorMetadata": true,
+@@ -22,7 +23,8 @@
+     "src/**/__tests__/*.ts"
+   ],
+   "compileOnSave": false,
+-  "atom": {
++  "atom":
++  {
+     "rewriteTsconfig": false
+   }
+ }
+diff --git app/tslint.json app/tslint.json
+index dd8e8d8..9754821 100644
+--- app/tslint.json
++++ app/tslint.json
+@@ -1,5 +1,6 @@
+ {
+-  "rules": {
++  "rules":
++  {
+     "no-duplicate-variable": true,
+     "no-unused-variable": [
+       true

+ 94 - 0
src1/app/app.component.ts

@@ -0,0 +1,94 @@
+import { Component, ViewChild } from "@angular/core";
+import { Nav, Platform, Events } from "ionic-angular";
+import { StatusBar } from "@ionic-native/status-bar";
+import { SplashScreen } from "@ionic-native/splash-screen";
+import { Storage } from "@ionic/storage";
+
+import { AuthProvider } from "../providers/auth/auth";
+
+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 { ProfilePage } from "../pages/profile/profile";
+import { TwitterApiProvider } from "../providers/twitter-api/twitter-api";
+
+@Component({
+  templateUrl: "app.html"
+})
+export class MyApp {
+  @ViewChild(Nav)
+  nav: Nav;
+
+  rootPage: any;
+  pages: Array<{ title: string; icon: string; component: any }>;
+  user: any;
+
+  constructor(
+    platform: Platform,
+    statusBar: StatusBar,
+    splashScreen: SplashScreen,
+    private authProvider: AuthProvider,
+    private twitter: TwitterApiProvider,
+    private storage: Storage,
+    private events: Events
+  ) {
+    platform.ready().then(() => {
+      // Okay, so the platform is ready and our plugins are available.
+      // Here you can do any higher level native things you might need.
+
+      statusBar.styleDefault();
+      splashScreen.hide();
+      this.initApp();
+
+      this.events.subscribe("user:login", () => this.setUser());
+    });
+
+    this.pages = [
+      { title: "Home", icon: "home", component: HomePage },
+      { title: "Search", icon: "search", component: SearchPage },
+      { title: "Settings", icon: "settings", component: SettingsPage }
+    ];
+  }
+
+  async initApp() {
+    const isLoggedIn = await this.authProvider.isLoggedIn();
+
+    if (isLoggedIn) {
+      this.rootPage = HomePage;
+      await this.setUser();
+    } else {
+      this.rootPage = LoginPage;
+    }
+  }
+
+  async setUser() {
+    const userId = await this.storage.get("userId");
+    this.user = await this.twitter.fetchUser(userId);
+  }
+
+  showProfile(userId) {
+    this.nav.push(ProfilePage, { userId });
+  }
+
+  openPage(page) {
+    if (page.component === HomePage) {
+      this.nav.setRoot(HomePage);
+    } else {
+      this.nav.push(page.component);
+    }
+  }
+
+  logout() {
+    this.authProvider.logout();
+    this.nav.setRoot(LoginPage);
+  }
+
+  get banner() {
+    if (this.user.profile_banner_url) {
+      return this.user.profile_banner_url;
+    } else {
+      return this.user.profile_background_image_url_https;
+    }
+  }
+}

+ 34 - 0
src1/app/app.html

@@ -0,0 +1,34 @@
+<ion-menu id="sideNav" [content]="content">
+  <ion-header>
+    <ion-toolbar>
+      <ion-title>Menu</ion-title>
+    </ion-toolbar>
+  </ion-header>
+
+  <ion-content>
+    <div class="user-info" *ngIf="user" (click)="showProfile(user.id)" menuClose>
+      <div class="user-banner" [style.background]="'url('+ banner +')'">
+        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px"
+          viewBox="0 0 300 100" xml:space="preserve" width="100%" class="svg-triangle">
+          <polygon points="0,75  0,100 300,100 300,99" fill="#FFFFFF" />
+        </svg>
+        <img src="{{ user.profile_image_url_https | highResolution }}" alt="User" class="user-avatar">
+      </div>
+      <div class="user-info">
+        {{user.name}}<br><span class="handle">@{{user.screen_name}}</span>
+      </div>
+    </div>
+    <ion-list>
+      <button menuClose ion-item no-padding icon-start *ngFor="let page of pages" (click)="openPage(page)">
+        <ion-icon name="{{page.icon}}"></ion-icon>{{page.title}}
+      </button>
+      <button menuClose ion-item no-padding icon-start (click)="logout()">
+        <ion-icon name="log-out"></ion-icon>Logout
+      </button>
+    </ion-list>
+  </ion-content>
+
+</ion-menu>
+
+<!-- Disable swipe-to-go-back because it's poor UX to combine STGB with side menus -->
+<ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>

+ 104 - 0
src1/app/app.module.ts

@@ -0,0 +1,104 @@
+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 { SocialSharing } from "@ionic-native/social-sharing";
+import { PhotoViewer } from "@ionic-native/photo-viewer";
+import { Vibration } from "@ionic-native/vibration";
+
+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 { MentionComponent } from "../components/mention/mention";
+import { HashtagComponent } from "../components/hashtag/hashtag";
+import { SearchResultsUsersPage } from "../pages/search-results-users/search-results-users";
+import { SearchResultsTweetsPopularPage } from "../pages/search-results-tweets-popular/search-results-tweets-popular";
+import { SearchResultsTweetsRecentPage } from "../pages/search-results-tweets-recent/search-results-tweets-recent";
+import { SearchResultsTweetsTabsPage } from "../pages/search-results-tweets-tabs/search-results-tweets-tabs";
+import { AboutPage } from "../pages/about/about";
+import { CryptoProvider } from "../providers/crypto/crypto";
+
+@NgModule({
+  declarations: [
+    MyApp,
+    HomePage,
+    SearchPage,
+    SettingsPage,
+    LoginPage,
+    ProfilePage,
+    WriteTweetPage,
+    SearchResultsTweetsTabsPage,
+    SearchResultsTweetsRecentPage,
+    SearchResultsTweetsPopularPage,
+    SearchResultsUsersPage,
+    AboutPage,
+    FeedComponent,
+    TweetComponent,
+    TweetHeaderComponent,
+    TweetBodyComponent,
+    TweetActionsComponent,
+    ProfileHeaderComponent,
+    QuotedStatusComponent,
+    MentionComponent,
+    HashtagComponent
+  ],
+  imports: [
+    BrowserModule,
+    HttpClientModule,
+    IonicModule.forRoot(MyApp),
+    IonicStorageModule.forRoot(),
+    PipesModule
+  ],
+  bootstrap: [IonicApp],
+  entryComponents: [
+    MyApp,
+    HomePage,
+    SearchPage,
+    SettingsPage,
+    LoginPage,
+    ProfilePage,
+    WriteTweetPage,
+    SearchResultsTweetsTabsPage,
+    SearchResultsTweetsRecentPage,
+    SearchResultsTweetsPopularPage,
+    SearchResultsUsersPage,
+    AboutPage
+  ],
+  providers: [
+    StatusBar,
+    SplashScreen,
+    HttpClient,
+    SocialSharing,
+    PhotoViewer,
+    { provide: ErrorHandler, useClass: IonicErrorHandler },
+    AuthProvider,
+    TwitterApiProvider,
+    P2pStorageIpfsProvider,
+    P2pDatabaseGunProvider,
+    FeedProvider,
+    CryptoProvider,
+    Vibration
+  ]
+})
+export class AppModule {}

+ 60 - 0
src1/app/app.scss

@@ -0,0 +1,60 @@
+// http://ionicframework.com/docs/theming/
+// App Global Sass
+// --------------------------------------------------
+// Put style rules here that you want to apply globally. These
+// styles are for the entire app and not just one component.
+// Additionally, this file can be also used as an entry point
+// to import other Sass files to be included in the output CSS.
+//
+// Shared Sass variables, which can be used to adjust Ionic's
+// default Sass variables, belong in "theme/variables.scss".
+//
+// 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.
+.icon.icon-verified {
+  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%;
+        width: 90px;
+      }
+    }
+    .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;
+  }
+}

+ 5 - 0
src1/app/main.ts

@@ -0,0 +1,5 @@
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app.module';
+
+platformBrowserDynamic().bootstrapModule(AppModule);

BIN
src1/assets/icon/favicon.ico


BIN
src1/assets/imgs/background-login-sm.png


BIN
src1/assets/imgs/background-login.png


BIN
src1/assets/imgs/logo.png


File diff suppressed because it is too large
+ 21544 - 0
src1/assets/scripts/openpgp.js


+ 145 - 0
src1/assets/scripts/openpgp.worker.js

@@ -0,0 +1,145 @@
+(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
+// GPG4Browsers - An OpenPGP implementation in javascript
+// Copyright (C) 2011 Recurity Labs GmbH
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 3.0 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+/* eslint-disable no-restricted-globals */
+/* eslint-disable no-var */
+/* eslint-disable vars-on-top */
+
+/**
+ * @fileoverview Provides functions for communicating with workers
+ * @see module:openpgp.initWorker
+ * @see module:openpgp.getWorker
+ * @see module:openpgp.destroyWorker
+ * @see module:worker/async_proxy
+ * @module worker/worker
+ */
+
+self.window = self; // to make UMD bundles work
+
+importScripts('openpgp.js');
+var openpgp = window.openpgp;
+
+var randomQueue = [];
+var MAX_SIZE_RANDOM_BUFFER = 60000;
+
+/**
+ * Handle random buffer exhaustion by requesting more random bytes from the main window
+ * @returns {Promise<Object>}  Empty promise whose resolution indicates that the buffer has been refilled
+ */
+function randomCallback() {
+
+  if (!randomQueue.length) {
+    self.postMessage({ event: 'request-seed', amount: MAX_SIZE_RANDOM_BUFFER });
+  }
+
+  return new Promise(function(resolve) {
+    randomQueue.push(resolve);
+  });
+}
+
+openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER, randomCallback);
+
+/**
+ * Handle messages from the main window.
+ * @param  {Object} event   Contains event type and data
+ */
+self.onmessage = function(event) {
+  var msg = event.data || {};
+
+  switch (msg.event) {
+    case 'configure':
+      configure(msg.config);
+      break;
+
+    case 'seed-random':
+      seedRandom(msg.buf);
+
+      var queueCopy = randomQueue;
+      randomQueue = [];
+      for (var i = 0; i < queueCopy.length; i++) {
+        queueCopy[i]();
+      }
+
+      break;
+
+    default:
+      delegate(msg.id, msg.event, msg.options || {});
+  }
+};
+
+/**
+ * Set config from main context to worker context.
+ * @param  {Object} config   The openpgp configuration
+ */
+function configure(config) {
+  Object.keys(config).forEach(function(key) {
+    openpgp.config[key] = config[key];
+  });
+}
+
+/**
+ * Seed the library with entropy gathered window.crypto.getRandomValues
+ * as this api is only avalible in the main window.
+ * @param  {ArrayBuffer} buffer   Some random bytes
+ */
+function seedRandom(buffer) {
+  if (!(buffer instanceof Uint8Array)) {
+    buffer = new Uint8Array(buffer);
+  }
+  openpgp.crypto.random.randomBuffer.set(buffer);
+}
+
+/**
+ * Generic proxy function that handles all commands from the public api.
+ * @param  {String} method    The public api function to be delegated to the worker thread
+ * @param  {Object} options   The api function's options
+ */
+function delegate(id, method, options) {
+  if (typeof openpgp[method] !== 'function') {
+    response({ id:id, event:'method-return', err:'Unknown Worker Event' });
+    return;
+  }
+  // construct ReadableStreams from MessagePorts
+  openpgp.util.restoreStreams(options);
+  // parse cloned packets
+  options = openpgp.packet.clone.parseClonedPackets(options, method);
+  openpgp[method](options).then(function(data) {
+    // clone packets (for web worker structured cloning algorithm)
+    response({ id:id, event:'method-return', data:openpgp.packet.clone.clonePackets(data) });
+  }).catch(function(e) {
+    openpgp.util.print_debug_error(e);
+    response({
+      id:id, event:'method-return', err:e.message, stack:e.stack
+    });
+  });
+}
+
+/**
+ * Respond to the main window.
+ * @param  {Object} event  Contains event type and data
+ */
+function response(event) {
+  self.postMessage(event, openpgp.util.getTransferables(event.data, true));
+}
+
+/**
+ * Let the main window know the worker has loaded.
+ */
+postMessage({ event: 'loaded' });
+
+},{}]},{},[1]);

+ 36 - 0
src1/components/components.module.ts

@@ -0,0 +1,36 @@
+import { NgModule } from "@angular/core";
+import { FeedComponent } from "./feed/feed";
+import { TweetComponent } from "./tweet/tweet";
+import { TweetHeaderComponent } from "./tweet-header/tweet-header";
+import { TweetBodyComponent } from "./tweet-body/tweet-body";
+import { TweetActionsComponent } from "./tweet-actions/tweet-actions";
+import { ProfileHeaderComponent } from "./profile-header/profile-header";
+import { QuotedStatusComponent } from "./quoted-status/quoted-status";
+import { HashtagComponent } from "./hashtag/hashtag";
+import { MentionComponent } from "./mention/mention";
+@NgModule({
+  declarations: [
+    FeedComponent,
+    TweetComponent,
+    TweetHeaderComponent,
+    TweetBodyComponent,
+    TweetActionsComponent,
+    ProfileHeaderComponent,
+    QuotedStatusComponent,
+    HashtagComponent,
+    MentionComponent
+  ],
+  imports: [],
+  exports: [
+    FeedComponent,
+    TweetComponent,
+    TweetHeaderComponent,
+    TweetBodyComponent,
+    TweetActionsComponent,
+    ProfileHeaderComponent,
+    QuotedStatusComponent,
+    HashtagComponent,
+    MentionComponent
+  ]
+})
+export class ComponentsModule {}

+ 13 - 0
src1/components/feed/feed.html

@@ -0,0 +1,13 @@
+<ion-content>
+  <ion-refresher (ionRefresh)="doRefresh($event)" enabled="{{enableRefresh}}">
+    <ion-refresher-content pullingText="Pull to refresh" refreshingText="Refreshing..."></ion-refresher-content>
+  </ion-refresher>
+
+  <ion-list>
+    <tweet *ngFor="let tweet of data" [data]="tweet"></tweet>
+  </ion-list>
+
+  <ion-infinite-scroll (ionInfinite)="doInfinite($event)" enabled="{{enableInfiniteScroll}}">
+    <ion-infinite-scroll-content loadingText="Loading more tweets..."></ion-infinite-scroll-content>
+  </ion-infinite-scroll>
+</ion-content>

+ 0 - 0
src1/components/feed/feed.scss


+ 36 - 0
src1/components/feed/feed.ts

@@ -0,0 +1,36 @@
+import {
+  Component,
+  Input,
+  Output,
+  ChangeDetectionStrategy,
+  EventEmitter
+} from "@angular/core";
+import { Refresher } from "ionic-angular";
+
+@Component({
+  selector: "feed",
+  templateUrl: "feed.html",
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class FeedComponent {
+  @Input()
+  data: any[];
+  @Input()
+  enableRefresh: boolean = true;
+  @Input()
+  enableInfiniteScroll: boolean = true;
+  @Output()
+  onRefresh: EventEmitter<any> = new EventEmitter<any>();
+  @Output()
+  onLoadMore: EventEmitter<any> = new EventEmitter<any>();
+
+  constructor() {}
+
+  doRefresh(refresher: Refresher) {
+    this.onRefresh.emit(refresher);
+  }
+
+  doInfinite(infiniteScroll) {
+    this.onLoadMore.emit(infiniteScroll);
+  }
+}

+ 2 - 0
src1/components/hashtag/hashtag.html

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

+ 5 - 0
src1/components/hashtag/hashtag.scss

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

+ 18 - 0
src1/components/hashtag/hashtag.ts

@@ -0,0 +1,18 @@
+import { Component, Input } from "@angular/core";
+import { App } from "ionic-angular";
+import { SearchPage } from "../../pages/search/search";
+
+@Component({
+  selector: "hashtag",
+  templateUrl: "hashtag.html"
+})
+export class HashtagComponent {
+  @Input()
+  hashtag;
+
+  constructor(private appCtrl: App) {}
+
+  search(hashtag) {
+    this.appCtrl.getRootNav().push(SearchPage, { query: hashtag });
+  }
+}

+ 2 - 0
src1/components/mention/mention.html

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

+ 5 - 0
src1/components/mention/mention.scss

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

+ 20 - 0
src1/components/mention/mention.ts

@@ -0,0 +1,20 @@
+import { Component, Input } from "@angular/core";
+import { App } from "ionic-angular";
+import { ProfilePage } from "../../pages/profile/profile";
+
+@Component({
+  selector: "mention",
+  templateUrl: "mention.html"
+})
+export class MentionComponent {
+  @Input()
+  username: string;
+  @Input()
+  userId: string;
+
+  constructor(private appCtrl: App) {}
+
+  showProfile(userId) {
+    this.appCtrl.getRootNav().push(ProfilePage, { userId });
+  }
+}

+ 28 - 0
src1/components/profile-header/profile-header.html

@@ -0,0 +1,28 @@
+<!-- Generated template for the ProfileHeaderComponent component -->
+<div>
+  <div class="profile-banner" [style.background-image]="'url('+ banner +')'" [style.background-color]="'#' + user.profile_background_color">
+    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 300 100" xml:space="preserve" width="100%" class="svg-triangle">
+      <polygon points="0,75  0,100 300,100 300,99" fill="#FFFFFF" />
+    </svg>
+    <img src="{{ user.profile_image_url_https | highResolution }}" alt="{{ user.name }}" class="avatar" (click)="showProfilePicture()">
+    <button ion-button color="primary" *ngIf="!user.following" (click)="follow(user.id_str)" class="follow-button">Follow</button>
+    <button ion-button color="danger" *ngIf="user.following" (click)="unfollow(user.id_str)" class="follow-button">Unfollow</button>
+  </div>
+  <div padding>
+    <p class="profile-stats">
+      {{ user.followers_count | friendlyNumber }} Followers |
+      {{ user.friends_count | friendlyNumber }} Following |
+      {{ user.statuses_count | friendlyNumber }} Tweets
+    </p>
+    <p class="profile-description" *ngIf="user.description">{{ user.description }}</p>
+    <div class="profile-infos">
+      <span class="user-location" *ngIf="user.location">
+        <ion-icon name="pin"></ion-icon>&nbsp;{{ user.location }}
+      </span>
+      <span class="user-website" *ngIf="user.url">
+        <ion-icon name="link"></ion-icon>&nbsp;
+        <span [innerHTML]="user.url | replaceUrls: user.entities.url.urls"></span>
+      </span>
+    </div>
+  </div>
+</div>

+ 25 - 0
src1/components/profile-header/profile-header.scss

@@ -0,0 +1,25 @@
+profile-header {
+  .profile-banner {
+    width: 100%;
+    height: calc(100vw * 0.333333333);
+    background-size: cover;
+    position: relative;
+    .svg-triangle {
+      position: absolute;
+      bottom: -1px;
+    }
+  }
+  .avatar {
+    border-radius: 50%;
+    box-shadow: 0px 0px 4px 0px #777;
+    position: absolute;
+    bottom: -15px;
+    left: 15px;
+    width: 90px;
+  }
+  .follow-button {
+    position: absolute;
+    right: 16px;
+    bottom: -8px;
+  }
+}

+ 45 - 0
src1/components/profile-header/profile-header.ts

@@ -0,0 +1,45 @@
+import { Component, Input } from "@angular/core";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+import { PhotoViewer } from "@ionic-native/photo-viewer";
+
+@Component({
+  selector: "profile-header",
+  templateUrl: "profile-header.html"
+})
+export class ProfileHeaderComponent {
+  @Input()
+  user: any;
+
+  constructor(
+    private twitter: TwitterApiProvider,
+    private photoViewer: PhotoViewer
+  ) {}
+
+  get banner() {
+    if (this.user.profile_banner_url) {
+      return this.user.profile_banner_url + "/1500x500";
+    } else {
+      return this.user.profile_background_image_url_https;
+    }
+  }
+
+  async follow(userId) {
+    await this.twitter.createFriendship(userId);
+    this.user.following = true;
+  }
+
+  async unfollow(userId) {
+    await this.twitter.destroyFriendship(userId);
+    this.user.following = false;
+  }
+
+  showProfilePicture() {
+    const profilePicutreHighResUrl = this.user.profile_image_url_https.replace(
+      "_normal",
+      ""
+    );
+    this.photoViewer.show(profilePicutreHighResUrl, this.user.name, {
+      share: true
+    });
+  }
+}

+ 13 - 0
src1/components/quoted-status/quoted-status.html

@@ -0,0 +1,13 @@
+<!-- Generated template for the QuotedStatusComponent component -->
+<div class="quoted-tweet">
+  <div class="header">
+    <img src="{{ data.user.profile_image_url_https }}" alt="{{ data.user.name }}" class="avatar">
+    <span>{{ data.user.name }}</span>
+    <span class="twitter-handle">@{{ data.user.screen_name }}</span>
+    <span class="timestamp">{{ data.created_at | diffForHumans }}</span>
+  </div>
+  <div class="body">
+    <p>{{ data.full_text }}</p>
+    <img *ngIf="hasPhoto" src="{{ data.entities.media[0]['media_url_https'] }}" alt="Photo" (click)="showPhoto(data.entities.media[0]['media_url_https'])">
+  </div>
+</div>

+ 43 - 0
src1/components/quoted-status/quoted-status.scss

@@ -0,0 +1,43 @@
+quoted-status {
+  .quoted-tweet {
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    padding: 5px;
+    margin: 5px 0;
+  }
+  p {
+    white-space: pre-wrap;
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    flex-direction: row;
+    color: #555;
+    font-size: 12px;
+    margin-bottom: 3px;
+    .avatar {
+      border-radius: 50%;
+      width: 28px;
+      margin: 3px 6px 3px 3px;
+    }
+    .twitter-handle {
+      color: #aaa;
+      font-weight: 250;
+      margin-left: 5px;
+    }
+    .timestamp {
+      margin-left: auto;
+      color: #aaa;
+      font-size: 10px;
+    }
+  }
+}
+
+.private quoted-status {
+  .header {
+    color: #efefef;
+  }
+  .body p {
+    color: #efefef;
+  }
+}

+ 24 - 0
src1/components/quoted-status/quoted-status.ts

@@ -0,0 +1,24 @@
+import { Component, Input } from "@angular/core";
+import { PhotoViewer } from "@ionic-native/photo-viewer";
+
+@Component({
+  selector: "quoted-status",
+  templateUrl: "quoted-status.html"
+})
+export class QuotedStatusComponent {
+  @Input()
+  data: any[];
+
+  constructor(private photoViewer: PhotoViewer) {}
+
+  get hasPhoto() {
+    return (
+      this.data["entities"]["media"] &&
+      this.data["entities"]["media"][0]["type"] == "photo"
+    );
+  }
+
+  showPhoto(url: string) {
+    this.photoViewer.show(url, null, { share: true });
+  }
+}

+ 24 - 0
src1/components/tweet-actions/tweet-actions.html

@@ -0,0 +1,24 @@
+<div class="actions-container">
+  <div class="reply" *ngIf="!data.private_tweet" (click)="replyToStatus(data.id_str)">
+    <ion-icon name="undo"></ion-icon>
+  </div>
+  <div class="retweets" *ngIf="!data.user.protected" (click)="retweetStatus(id)">
+    <ion-icon name="ios-git-compare-outline"></ion-icon>
+    <span>{{ data.retweet_count | friendlyNumber }}</span>
+  </div>
+  <div class="likes" *ngIf="!data.private_tweet">
+    <span class="icon-stack" (click)="toggleLike(id)">
+      <ion-icon class="primary-icon" *ngIf="data.favorited" name="ios-heart" color="danger"></ion-icon>
+      <ion-icon class="primary-icon" *ngIf="!data.favorited" name="ios-heart-outline"></ion-icon>
+      <ion-icon class="secondary-icon" name="logo-twitter"></ion-icon>
+    </span>
+    <span>{{ favoriteCount | friendlyNumber }}</span>
+  </div>
+  <div class="private-likes" *ngIf="!data.private_tweet">
+    <span class="icon-stack" (click)="addPrivateLike(id)">
+      <ion-icon class="primary-icon" name="ios-heart-outline"></ion-icon>
+      <ion-icon class="secondary-icon" name="glasses"></ion-icon>
+    </span>
+    <span>{{ privateFavoriteCount | friendlyNumber }}</span>
+  </div>
+</div>

+ 33 - 0
src1/components/tweet-actions/tweet-actions.scss

@@ -0,0 +1,33 @@
+tweet-actions {
+  .actions-container {
+    display: flex;
+    flex-direction: row;
+    div {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: center;
+      flex-grow: 1;
+      text-align: center;
+      margin-top: 8px;
+      span {
+        margin-left: 5px;
+        font-size: 12px;
+        font-weight: 300;
+        color: #333;
+      }
+    }
+    .icon-stack {
+      position: relative;
+      .secondary-icon {
+        font-size: 0.7em;
+        position: absolute;
+        bottom: 10%;
+        left: 50%;
+        background: #fff;
+        border-radius: 50%;
+        padding: 1px;
+      }
+    }
+  }
+}

+ 93 - 0
src1/components/tweet-actions/tweet-actions.ts

@@ -0,0 +1,93 @@
+import { Component, Input, ChangeDetectorRef } from "@angular/core";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+import { NavController } from "ionic-angular";
+import { WriteTweetPage } from "../../pages/write-tweet/write-tweet";
+import { P2pDatabaseGunProvider } from "../../providers/p2p-database-gun/p2p-database-gun";
+import { Vibration } from "@ionic-native/vibration";
+
+@Component({
+  selector: "tweet-actions",
+  templateUrl: "tweet-actions.html"
+})
+export class TweetActionsComponent {
+  @Input()
+  data: any[];
+  privateFavoriteCount: number = 0;
+
+  constructor(
+    private twitter: TwitterApiProvider,
+    private ref: ChangeDetectorRef,
+    private navCtrl: NavController,
+    private gun: P2pDatabaseGunProvider,
+    private vibration: Vibration
+  ) {}
+
+  ngOnInit() {
+    this.getPrivateLikes(this.id);
+  }
+
+  get favoriteCount() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["favorite_count"];
+    } else {
+      return this.data["favorite_count"];
+    }
+  }
+
+  get id() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["id_str"];
+    } else {
+      return this.data["id_str"];
+    }
+  }
+
+  private async getPrivateLikes(id: string) {
+    const likeEntry = await this.gun.getLikes(this.id);
+    if (likeEntry.likes > 0) {
+      this.privateFavoriteCount = likeEntry.likes;
+      this.ref.detectChanges();
+    }
+  }
+
+  addPrivateLike(id: string) {
+    this.vibration.vibrate([100, 50, 100]);
+    this.gun.addLike(id).then(() => {
+      this.privateFavoriteCount++;
+      this.ref.detectChanges();
+    });
+  }
+
+  toggleLike(id: string) {
+    this.vibration.vibrate([100, 50, 100]);
+    if (this.data["favorited"]) {
+      this.removeLike(id);
+    } else {
+      this.like(id);
+    }
+  }
+
+  private like(id: string): void {
+    this.twitter.likeTweet(id).then(() => {
+      this.data["favorited"] = true;
+      this.data["favorite_count"]++;
+      this.ref.detectChanges();
+    });
+  }
+
+  private removeLike(id: string): void {
+    this.twitter.unlikeTweet(id).then(() => {
+      this.data["favorited"] = false;
+      this.data["favorite_count"]--;
+      this.ref.detectChanges();
+    });
+  }
+
+  retweetStatus(id: string): void {
+    this.navCtrl.push(WriteTweetPage, { tweetId: id });
+  }
+
+  replyToStatus(id: string): void {
+    this.navCtrl.push(WriteTweetPage, { replyToStatus: id });
+  }
+}

+ 15 - 0
src1/components/tweet-body/tweet-body.html

@@ -0,0 +1,15 @@
+<div>
+  <div class="tweet-array">
+    <span class="tweet-array-part" *ngFor="let part of tweetArray">
+      <span *ngIf="part.type =='text'" class="text" [innerHtml]="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>
+    </span>
+  </div>
+
+  <img *ngIf="hasPhoto" src="{{ entities.media[0]['media_url_https'] }}" alt="Photo" class="photo" (click)="showPhoto(entities.media[0]['media_url_https'])">
+  <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>
+  <div *ngIf="!data.quoted_status && data.quoted_status_id" class="removed-tweet">Tweet has been removed...</div>
+</div>

+ 22 - 0
src1/components/tweet-body/tweet-body.scss

@@ -0,0 +1,22 @@
+tweet-body {
+  span.text {
+    white-space: pre-wrap;
+  }
+  .photo {
+    margin-top: 5px;
+    padding: 2px;
+    border-radius: 3px;
+    border: 1px solid #dfdfdf;
+  }
+  video {
+    width: 100%;
+  }
+  .removed-tweet {
+    border: 1px solid #cc1f1a;
+    padding: 5px;
+    margin: 5px 0;
+    color: #cc1f1a;
+    border-radius: 3px;
+    font-style: italic;
+  }
+}

+ 204 - 0
src1/components/tweet-body/tweet-body.ts

@@ -0,0 +1,204 @@
+import { Component, Input } from "@angular/core";
+import twittertext from "twitter-text";
+import { PhotoViewer } from "@ionic-native/photo-viewer";
+
+@Component({
+  selector: "tweet-body",
+  templateUrl: "tweet-body.html"
+})
+export class TweetBodyComponent {
+  @Input()
+  data: any[];
+
+  constructor(private photoViewer: PhotoViewer) {}
+
+  get full_text(): string {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["full_text"];
+    } else {
+      return this.data["full_text"];
+    }
+  }
+
+  get entities() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["entities"];
+    } else {
+      return this.data["entities"];
+    }
+  }
+
+  get extended_entities() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["extended_entities"];
+    } else {
+      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 (
+      !this.data["private_tweet"] &&
+      !this.isGif &&
+      (this.entities["media"] && this.entities["media"][0]["type"] == "photo")
+    );
+  }
+
+  get isGif() {
+    return (
+      !this.data["private_tweet"] &&
+      this.extended_entities &&
+      this.extended_entities["media"] &&
+      this.extended_entities["media"][0]["type"] === "animated_gif"
+    );
+  }
+
+  get status() {
+    // Cut off beginning
+    let status = this.full_text.substring(this.range[0]);
+
+    // Cut off end (URLs)
+    this.urlsToRemoveFromStatus().forEach(
+      url => (status = status.replace(url, ""))
+    );
+
+    return status.trim();
+  }
+
+  get tweetArray() {
+    const extractedEntites = twittertext.extractEntitiesWithIndices(
+      this.status
+    );
+
+    let tweetArray = [];
+    tweetArray = tweetArray.concat(
+      this.getHashtagsForTweetArray(
+        extractedEntites.filter(element => element["hashtag"])
+      )
+    );
+    tweetArray = tweetArray.concat(
+      this.getMentionsForTweetArray(
+        extractedEntites.filter(element => element["screenName"])
+      )
+    );
+    tweetArray = tweetArray.concat(
+      this.getUrlsForTweetArray(
+        extractedEntites.filter(element => element["url"])
+      )
+    );
+    tweetArray = tweetArray.concat(this.getTextParts(tweetArray));
+
+    return tweetArray.sort((a, b) => a["start"] - b["start"]);
+  }
+
+  private urlsToRemoveFromStatus(): string[] {
+    const res = [];
+
+    if (this.data["quoted_status_permalink"]) {
+      res.push(this.data["quoted_status_permalink"]["url"]);
+    }
+
+    if (this.extended_entities) {
+      this.extended_entities["media"].forEach(element => {
+        res.push(element["url"]);
+      });
+    }
+    return res.filter(entry => entry.length);
+  }
+
+  private getHashtagsForTweetArray(hashtags) {
+    const res = [];
+    hashtags.forEach(element => {
+      res.push({
+        start: element.indices[0],
+        stop: element.indices[1],
+        type: "hashtag",
+        text: "#" + element["hashtag"]
+      });
+    });
+    return res;
+  }
+
+  private getMentionsForTweetArray(mentions) {
+    const res = [];
+    mentions.forEach(element => {
+      const apiEntity = this.entities.user_mentions.filter(
+        el =>
+          el.screen_name.toLowerCase() === element["screenName"].toLowerCase()
+      )[0];
+      if (apiEntity) {
+        res.push({
+          start: element.indices[0],
+          stop: element.indices[1],
+          type: "user_mention",
+          text: "@" + element["screenName"],
+          userId: apiEntity["id_str"]
+        });
+      }
+    });
+    return res;
+  }
+
+  private getUrlsForTweetArray(urls) {
+    const res = [];
+    urls.forEach(element => {
+      const apiEntity = this.entities.urls.filter(
+        el => el.url.toLowerCase() === element["url"].toLowerCase()
+      )[0];
+      res.push({
+        start: element.indices[0],
+        stop: element.indices[1],
+        type: "url",
+        url: element["url"],
+        text: apiEntity["display_url"] || element["url"]
+      });
+    });
+    return res;
+  }
+
+  private getTextParts(tweetArray) {
+    const sortedTweetArray = tweetArray.sort((a, b) => a["start"] - b["start"]);
+
+    const textParts = [];
+
+    let prevEnd = 0;
+    for (let i = 0; i < sortedTweetArray.length; i++) {
+      if (sortedTweetArray[i]["start"] !== prevEnd) {
+        const text = this.status.substring(
+          prevEnd,
+          sortedTweetArray[i]["start"]
+        );
+
+        textParts.push({
+          start: prevEnd,
+          stop: sortedTweetArray[i]["start"],
+          type: "text",
+          text: text
+        });
+      }
+      prevEnd = sortedTweetArray[i]["stop"];
+    }
+
+    if (prevEnd != this.status.length) {
+      textParts.push({
+        start: prevEnd,
+        stop: this.status.length,
+        type: "text",
+        text: this.status.substring(prevEnd, this.status.length)
+      });
+    }
+
+    return textParts;
+  }
+
+  showPhoto(url: string) {
+    this.photoViewer.show(url, null, { share: true });
+  }
+}

+ 14 - 0
src1/components/tweet-header/tweet-header.html

@@ -0,0 +1,14 @@
+<!-- Generated template for the TweetHeaderComponent component -->
+<div class="header-container">
+  <img src="{{ user.profile_image_url_https }}" alt="{{ user.name }}" class="avatar" (click)="showProfile(user.id_str)">
+  <div class="username" (click)="showProfile(user.id_str)">
+    <span>
+      {{ user.name }}
+      <ion-icon name="ios-checkmark-circle" *ngIf="user.verified" class="icon-verified"></ion-icon>
+      <ion-icon name="ios-lock-outline" *ngIf="user.protected" class="icon-protected"></ion-icon>
+    </span>
+    <span class="twitter-handle">@{{user.screen_name}}</span>
+  </div>
+  <div class="timestamp">{{ tweetCreatedAt | diffForHumans }}</div>
+  <ion-icon name="ios-arrow-down" class="options" (click)="showActions(user.id_str)"></ion-icon>
+</div>

+ 39 - 0
src1/components/tweet-header/tweet-header.scss

@@ -0,0 +1,39 @@
+tweet-header {
+  .header-container {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    margin-bottom: 4px;
+  }
+  .avatar {
+    border-radius: 50%;
+    width: 32px;
+    height: 32px;
+    box-shadow: 0px 0px 4px 0px #777;
+    margin: 4px 8px 4px 4px;
+  }
+  .username {
+    display: flex;
+    flex-direction: column;
+    font-size: 13px;
+    font-weight: 500;
+    .twitter-handle {
+      color: #aaa;
+      font-size: 10px;
+      font-weight: 400;
+    }
+  }
+  .timestamp {
+    color: #aaa;
+    font-size: 10px;
+    margin-left: auto;
+  }
+  .options {
+    margin-left: 5px;
+    color: #666;
+  }
+  .icon.icon-verified,
+  .icon.icon-protected {
+    font-size: 1em;
+  }
+}

+ 107 - 0
src1/components/tweet-header/tweet-header.ts

@@ -0,0 +1,107 @@
+import { Component, Input } from "@angular/core";
+import { ActionSheetController, App } from "ionic-angular";
+import { ProfilePage } from "../../pages/profile/profile";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+
+@Component({
+  selector: "tweet-header",
+  templateUrl: "tweet-header.html"
+})
+export class TweetHeaderComponent {
+  @Input()
+  user: any[];
+  @Input()
+  tweetCreatedAt: string;
+
+  constructor(
+    private appCtrl: App,
+    public actionSheetCtrl: ActionSheetController,
+    private twitter: TwitterApiProvider
+  ) {}
+
+  showProfile(userId) {
+    this.appCtrl.getRootNav().push(ProfilePage, { userId });
+  }
+
+  showActions(userId) {
+    this.twitter.fetchUser(userId).then(user => {
+      this.actionSheetCtrl
+        .create({
+          title: "@" + this.user["screen_name"],
+          buttons: this.getButtonsForActionSheet(user)
+        })
+        .present();
+    });
+  }
+
+  private getButtonsForActionSheet(user) {
+    const buttons = [];
+    if (user.following) {
+      // Unfollow
+      buttons.push({
+        text: "Unfollow",
+        role: "destructive",
+        handler: () => {
+          this.twitter.destroyFriendship(user.id_str);
+        }
+      });
+    } else {
+      // Follow
+      buttons.push({
+        text: "Follow",
+        role: "destructive",
+        handler: () => {
+          this.twitter.createFriendship(user.id_str);
+        }
+      });
+    }
+
+    if (user.muting) {
+      // unmute
+      buttons.push({
+        text: "Unmute",
+        role: "destructive",
+        handler: () => {
+          this.twitter.unmuteUser(user.id_str);
+        }
+      });
+    } else {
+      // mute
+      buttons.push({
+        text: "Mute",
+        role: "destructive",
+        handler: () => {
+          this.twitter.muteUser(user.id_str);
+        }
+      });
+    }
+
+    if (user.blocking) {
+      // Unblock
+      buttons.push({
+        text: "Unblock",
+        role: "destructive",
+        handler: () => {
+          this.twitter.unblockUser(user.id_str);
+        }
+      });
+    } else {
+      // Block
+      buttons.push({
+        text: "Block",
+        role: "destructive",
+        handler: () => {
+          this.twitter.blockUser(user.id_str);
+        }
+      });
+    }
+
+    // Cancel button
+    buttons.push({
+      text: "Cancel",
+      role: "cancel"
+    });
+
+    return buttons;
+  }
+}

+ 7 - 0
src1/components/tweet/tweet.html

@@ -0,0 +1,7 @@
+<ion-item text-wrap [class.private]="data.private_tweet">
+  <p *ngIf="data.retweeted_status" class="retweet-info">{{ data.user.name }} has retweeted:</p>
+  <p *ngIf="data.in_reply_to_screen_name" class="retweet-info">Reply to @{{ data.in_reply_to_screen_name }}</p>
+  <tweet-header [user]="user" [tweetCreatedAt]="createdAt"></tweet-header>
+  <tweet-body [data]="data"></tweet-body>
+  <tweet-actions [data]="data"></tweet-actions>
+</ion-item>

+ 23 - 0
src1/components/tweet/tweet.scss

@@ -0,0 +1,23 @@
+tweet {
+  .item-md {
+    padding-left: 0;
+  }
+  .item-inner {
+    padding-left: 8px;
+  }
+  .label-md {
+    margin-left: 8px;
+  }
+  .retweet-info {
+    font-weight: 250;
+    font-size: 10px;
+    margin-bottom: 8px;
+  }
+  .private {
+    background: #333;
+    color: #efefef;
+    .retweet-info {
+      color: #bebebe;
+    }
+  }
+}

+ 34 - 0
src1/components/tweet/tweet.ts

@@ -0,0 +1,34 @@
+import { Component, Input } from "@angular/core";
+
+/**
+ * Generated class for the TweetComponent component.
+ *
+ * See https://angular.io/api/core/Component for more info on Angular
+ * Components.
+ */
+@Component({
+  selector: "tweet",
+  templateUrl: "tweet.html"
+})
+export class TweetComponent {
+  @Input()
+  data: any[];
+
+  constructor() {}
+
+  get user() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["user"];
+    } else {
+      return this.data["user"];
+    }
+  }
+
+  get createdAt() {
+    if (this.data["retweeted_status"]) {
+      return this.data["retweeted_status"]["created_at"];
+    } else {
+      return this.data["created_at"];
+    }
+  }
+}

+ 51 - 0
src1/index.html

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+<head>
+  <meta charset="UTF-8">
+  <title>Ionic App</title>
+  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+  <meta name="format-detection" content="telephone=no">
+  <meta name="msapplication-tap-highlight" content="no">
+
+  <link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico">
+  <link rel="manifest" href="manifest.json">
+  <meta name="theme-color" content="#4e8ef7">
+
+  <!-- add to homescreen for ios -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+
+  <!-- cordova.js required for cordova apps (remove if not needed) -->
+  <script src="cordova.js"></script>
+  <script src="assets/scripts/openpgp.js"></script>
+  <script src="assets/scripts/openpgp.worker.js"></script>
+
+  <!-- un-comment this code to enable service worker
+  <script>
+    if ('serviceWorker' in navigator) {
+      navigator.serviceWorker.register('service-worker.js')
+        .then(() => console.log('service worker installed'))
+        .catch(err => console.error('Error', err));
+    }
+  </script>-->
+
+  <link href="build/main.css" rel="stylesheet">
+
+</head>
+<body>
+
+  <!-- Ionic's root component and where the app will load -->
+  <ion-app></ion-app>
+
+  <!-- The polyfills js is generated during the build process -->
+  <script src="build/polyfills.js"></script>
+
+  <!-- The vendor js is generated during the build process
+       It contains all of the dependencies in node_modules -->
+  <script src="build/vendor.js"></script>
+
+  <!-- The main bundle js is generated during the build process -->
+  <script src="build/main.js"></script>
+
+</body>
+</html>

+ 13 - 0
src1/manifest.json

@@ -0,0 +1,13 @@
+{
+  "name": "Ionic",
+  "short_name": "Ionic",
+  "start_url": "index.html",
+  "display": "standalone",
+  "icons": [{
+    "src": "assets/imgs/logo.png",
+    "sizes": "512x512",
+    "type": "image/png"
+  }],
+  "background_color": "#4e8ef7",
+  "theme_color": "#4e8ef7"
+}

+ 11 - 0
src1/pages/about/about.html

@@ -0,0 +1,11 @@
+<ion-header>
+  <ion-navbar>
+    <ion-title>About HybridOSN</ion-title>
+  </ion-navbar>
+</ion-header>
+
+
+<ion-content padding>
+  <p>Hybrid OSN is a hybrid Twitter client for Android. While Twitter can be used as usual, data can also be securely exchanged with other users via a P2P network. Therefore Twitter is not able to analyse, sell or censor this data.</p>
+  <p>Hybrid OSN was developed by Carsten Porth under the supervision of Aidmar Wainakh and Jörg Daubert as part of a master thesis at TU Darmstadt. It is a prototype which was created as proof of concept.</p>
+</ion-content>

+ 13 - 0
src1/pages/about/about.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { AboutPage } from './about';
+
+@NgModule({
+  declarations: [
+    AboutPage,
+  ],
+  imports: [
+    IonicPageModule.forChild(AboutPage),
+  ],
+})
+export class AboutPageModule {}

+ 3 - 0
src1/pages/about/about.scss

@@ -0,0 +1,3 @@
+page-about {
+
+}

+ 11 - 0
src1/pages/about/about.ts

@@ -0,0 +1,11 @@
+import { Component } from "@angular/core";
+import { IonicPage, NavController, NavParams } from "ionic-angular";
+
+@IonicPage()
+@Component({
+  selector: "page-about",
+  templateUrl: "about.html"
+})
+export class AboutPage {
+  constructor(public navCtrl: NavController, public navParams: NavParams) {}
+}

+ 18 - 0
src1/pages/home/home.html

@@ -0,0 +1,18 @@
+<ion-header>
+  <ion-navbar>
+    <button ion-button menuToggle>
+      <ion-icon name="menu"></ion-icon>
+    </button>
+    <ion-title>Home</ion-title>
+  </ion-navbar>
+</ion-header>
+
+<ion-content>
+  <feed [data]="tweets" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)"></feed>
+
+  <ion-fab bottom right>
+    <button ion-fab (click)="writeTweet()">
+      <ion-icon name="add"></ion-icon>
+    </button>
+  </ion-fab>
+</ion-content>

+ 9 - 0
src1/pages/home/home.module.ts

@@ -0,0 +1,9 @@
+import { NgModule } from "@angular/core";
+import { IonicPageModule } from "ionic-angular";
+import { HomePage } from "./home";
+
+@NgModule({
+  declarations: [HomePage],
+  imports: [IonicPageModule.forChild(HomePage)]
+})
+export class HomePageModule {}

+ 0 - 0
src1/pages/home/home.scss


+ 91 - 0
src1/pages/home/home.ts

@@ -0,0 +1,91 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  MenuController,
+  InfiniteScroll,
+  Refresher,
+  LoadingController
+} from "ionic-angular";
+import { WriteTweetPage } from "../write-tweet/write-tweet";
+import { FeedProvider } from "../../providers/feed/feed";
+
+@IonicPage()
+@Component({
+  selector: "page-home",
+  templateUrl: "home.html"
+})
+export class HomePage {
+  menuController: MenuController;
+  tweets;
+
+  constructor(
+    public navCtrl: NavController,
+    private menuCtrl: MenuController,
+    private feed: FeedProvider,
+    private loadingCtrl: LoadingController
+  ) {}
+
+  ionViewDidLoad() {
+    this.menuCtrl.enable(true, "sideNav");
+  }
+
+  ionViewDidEnter() {
+    const loading = this.loadingCtrl.create();
+    loading.present();
+
+    this.feed
+      .loadHomeTimeline()
+      .then(tweets => (this.tweets = tweets))
+      .catch(err => console.error(err))
+      .then(() => loading.dismiss());
+  }
+
+  doRefresh(refresher: Refresher) {
+    this.feed.loadHomeTimeline().then(tweets => {
+      this.tweets = tweets;
+      refresher.complete();
+    });
+  }
+
+  loadMore(infiniteScroll: InfiniteScroll) {
+    this.feed
+      .loadHomeTimeline(this.oldestPublicTweet, this.oldestPrivateTweet)
+      .then(tweets => {
+        this.tweets = this.tweets.concat(tweets);
+        infiniteScroll.complete();
+      });
+  }
+
+  writeTweet() {
+    this.navCtrl.push(WriteTweetPage);
+  }
+
+  get publicTweets() {
+    return this.tweets.filter(tweet => !tweet.private_tweet);
+  }
+
+  get privateTweets() {
+    return this.tweets.filter(tweet => tweet.private_tweet);
+  }
+
+  get oldestPublicTweet() {
+    if (this.publicTweets.length > 0) {
+      return this.publicTweets.reduce((acc, cur) =>
+        acc.id < cur.id ? acc : cur
+      );
+    } else {
+      return undefined;
+    }
+  }
+
+  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 undefined;
+    }
+  }
+}

+ 13 - 0
src1/pages/login/login.html

@@ -0,0 +1,13 @@
+<ion-content>
+  <div class="header"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 300 100" xml:space="preserve" width="100%" class="svg-triangle">
+      <polygon points="0,99  0,100 300,100 300,75" fill="#FFFFFF" /></svg>
+    <div class="logo">
+      <img src="assets/imgs/logo.png" alt="Logo">
+    </div>
+  </div>
+  <div class="content">
+    <h1>Hybrid OSN</h1>
+    <button ion-button outline block (click)="login()">Login</button>
+    <a href="#" (click)="showAbout()">Learn more</a>
+  </div>
+</ion-content>

+ 9 - 0
src1/pages/login/login.module.ts

@@ -0,0 +1,9 @@
+import { NgModule } from "@angular/core";
+import { IonicPageModule } from "ionic-angular";
+import { LoginPage } from "./login";
+
+@NgModule({
+  declarations: [LoginPage],
+  imports: [IonicPageModule.forChild(LoginPage)]
+})
+export class LoginPageModule {}

+ 33 - 0
src1/pages/login/login.scss

@@ -0,0 +1,33 @@
+page-login {
+  .header {
+    background: url("../assets/imgs/background-login-sm.png") repeat #5ec0f9;
+    width: 100%;
+    height: 40vh;
+    position: relative;
+    margin-bottom: 50px;
+    .logo {
+      width: 140px;
+      height: 140px;
+      position: absolute;
+      bottom: -60px;
+      left: calc(50% - 70px);
+      img {
+        border-radius: 50%;
+        border: 5px solid #fff;
+      }
+    }
+    .svg-triangle {
+      position: absolute;
+      bottom: 0;
+    }
+  }
+  .content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 16px;
+    button {
+      margin: 16px 0 32px 0;
+    }
+  }
+}

+ 53 - 0
src1/pages/login/login.ts

@@ -0,0 +1,53 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  MenuController,
+  AlertController,
+  ModalController,
+  Events
+} from "ionic-angular";
+import { AuthProvider } from "../../providers/auth/auth";
+import { AboutPage } from "../about/about";
+import { HomePage } from "../home/home";
+
+@IonicPage()
+@Component({
+  selector: "page-login",
+  templateUrl: "login.html"
+})
+export class LoginPage {
+  constructor(
+    public navCtrl: NavController,
+    public navParams: NavParams,
+    private menuCtrl: MenuController,
+    private alertCtrl: AlertController,
+    private authProvider: AuthProvider,
+    private modalCtrl: ModalController,
+    private events: Events
+  ) {}
+
+  ionViewDidLoad() {
+    this.menuCtrl.enable(false, "sideNav");
+  }
+
+  login() {
+    const alertText = {
+      title: "Login failed",
+      subTitle:
+        "Somthing went wrong while trying to log you in. Please try again.",
+      buttons: ["OK"]
+    };
+
+    this.authProvider
+      .login()
+      .then(() => this.events.publish("user:login"))
+      .then(() => this.navCtrl.setRoot(HomePage))
+      .catch(err => this.alertCtrl.create(alertText).present());
+  }
+
+  showAbout() {
+    this.modalCtrl.create(AboutPage).present();
+  }
+}

+ 15 - 0
src1/pages/profile/profile.html

@@ -0,0 +1,15 @@
+<ion-header>
+  <ion-navbar>
+    <ion-title>
+      {{ user.name }}
+      <ion-icon name="ios-checkmark-circle" *ngIf="user.verified" class="icon-verified"></ion-icon>
+      <ion-icon name="ios-lock-outline" *ngIf="user.protected" class="icon-protected"></ion-icon>
+    </ion-title>
+  </ion-navbar>
+</ion-header>
+
+<ion-content fullscreen (ionScroll)="onScroll($event)">
+  <profile-header [user]="user"></profile-header>
+  <feed [data]="tweets" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)" [enableRefresh]="enableRefresh"
+    [enableInfiniteScroll]="enableInfiniteScroll"></feed>
+</ion-content>

+ 9 - 0
src1/pages/profile/profile.module.ts

@@ -0,0 +1,9 @@
+import { NgModule } from "@angular/core";
+import { IonicPageModule } from "ionic-angular";
+import { ProfilePage } from "./profile";
+
+@NgModule({
+  declarations: [ProfilePage],
+  imports: [IonicPageModule.forChild(ProfilePage)]
+})
+export class ProfilePageModule {}

+ 3 - 0
src1/pages/profile/profile.scss

@@ -0,0 +1,3 @@
+ion-navbar .icon {
+    font-size: 2rem;
+}

+ 128 - 0
src1/pages/profile/profile.ts

@@ -0,0 +1,128 @@
+import { Component, ViewChild } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  InfiniteScroll,
+  Content,
+  LoadingController
+} from "ionic-angular";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+import { FeedProvider } from "../../providers/feed/feed";
+
+@IonicPage()
+@Component({
+  selector: "page-profile",
+  templateUrl: "profile.html"
+})
+export class ProfilePage {
+  user: any = [];
+  tweets: any[];
+  oldestLoadedTweetId;
+  enableRefresh: boolean = true;
+  enableInfiniteScroll: boolean = true;
+
+  @ViewChild(Content)
+  content: Content;
+
+  constructor(
+    public navCtrl: NavController,
+    private loadingCtrl: LoadingController,
+    private navParams: NavParams,
+    private twitter: TwitterApiProvider,
+    private feed: FeedProvider
+  ) {}
+
+  ionViewDidLoad() {
+    // Show loading indicator
+    const loading = this.loadingCtrl.create();
+    loading.present();
+
+    // Read user id
+    const userId = this.navParams.get("userId");
+
+    // Fetch user details from Twitter
+    this.twitter.fetchUser(userId).then(res => (this.user = res));
+
+    // Load user's timeline from Twitter and P2P
+    this.feed.loadUserTimeline(userId).then(res => {
+      if (res.length > 0) {
+        // Store tweets
+        this.tweets = res;
+      } else {
+        this.enableInfiniteScroll = false;
+      }
+      // Hide loading indicator
+      loading.dismiss();
+    });
+  }
+
+  doRefresh(refresher) {
+    this.feed.loadUserTimeline(this.user.id_str).then(res => {
+      if (res.length > 0) {
+        // Replace tweets
+        this.tweets = res;
+      }
+
+      // Hide loading icon
+      refresher.complete();
+    });
+  }
+
+  loadMore(infiniteScroll: InfiniteScroll) {
+    if (this.enableInfiniteScroll) {
+      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);
+          } else {
+            this.enableInfiniteScroll = false;
+          }
+          // Hide loading icon
+          infiniteScroll.complete();
+        });
+    } else {
+      // Hide loading icon
+      infiniteScroll.complete();
+    }
+  }
+
+  get publicTweets() {
+    return this.tweets.filter(tweet => !tweet.private_tweet);
+  }
+
+  get privateTweets() {
+    return this.tweets.filter(tweet => tweet.private_tweet);
+  }
+
+  get oldestPublicTweet() {
+    if (this.publicTweets.length > 0) {
+      return this.publicTweets.reduce(
+        (acc, cur) => (acc.id < cur.id ? acc : cur)
+      );
+    } else {
+      return undefined;
+    }
+  }
+
+  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 undefined;
+    }
+  }
+
+  onScroll(event) {
+    this.enableRefresh = event.scrollTop === 0;
+  }
+}

+ 4 - 0
src1/pages/search-results-tweets-popular/search-results-tweets-popular.html

@@ -0,0 +1,4 @@
+<ion-content no-padding>
+  <feed [data]="tweets.statuses" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)" [enableRefresh]="enableRefresh"
+    [enableInfiniteScroll]="enableInfiniteScroll"></feed>
+</ion-content>

+ 13 - 0
src1/pages/search-results-tweets-popular/search-results-tweets-popular.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { SearchResultsTweetsPopularPage } from './search-results-tweets-popular';
+
+@NgModule({
+  declarations: [
+    SearchResultsTweetsPopularPage,
+  ],
+  imports: [
+    IonicPageModule.forChild(SearchResultsTweetsPopularPage),
+  ],
+})
+export class SearchResultsTweetsPopularPageModule {}

+ 5 - 0
src1/pages/search-results-tweets-popular/search-results-tweets-popular.scss

@@ -0,0 +1,5 @@
+page-search-results-tweets-popular {
+    feed .scroll-content {
+        margin-top: 0 !important;
+    }
+}

+ 80 - 0
src1/pages/search-results-tweets-popular/search-results-tweets-popular.ts

@@ -0,0 +1,80 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  Refresher,
+  InfiniteScroll,
+  Events
+} from "ionic-angular";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+
+@IonicPage()
+@Component({
+  selector: "page-search-results-tweets-popular",
+  templateUrl: "search-results-tweets-popular.html"
+})
+export class SearchResultsTweetsPopularPage {
+  query: string;
+  tweets = [];
+
+  constructor(
+    public navCtrl: NavController,
+    public navParams: NavParams,
+    private twitter: TwitterApiProvider,
+    private events: Events
+  ) {
+    this.query = this.navParams.data;
+
+    this.events.subscribe("query:changed", query => {
+      if (query.length) {
+        this.twitter
+          .searchPopularTweets(query)
+          .then(res => (this.tweets = res));
+        this.query = query;
+      }
+    });
+  }
+
+  async ionViewDidLoad() {
+    if (this.query.length) {
+      this.tweets = await this.twitter.searchPopularTweets(this.query);
+    }
+  }
+
+  doRefresh(refresher: Refresher) {
+    this.twitter.searchPopularTweets(this.query).then(tweets => {
+      this.tweets = tweets;
+      refresher.complete();
+    });
+  }
+
+  loadMore(infiniteScroll: InfiniteScroll) {
+    this.twitter
+      .searchPopularTweets(this.query, this.oldestTweet)
+      .then(tweets => {
+        this.tweets["statuses"] = this.tweets["statuses"].concat(
+          tweets["statuses"]
+        );
+        infiniteScroll.complete();
+      });
+  }
+
+  get oldestTweet() {
+    if (this.tweets.length > 0) {
+      return this.tweets.reduce((acc, cur) => (acc.id < cur.id ? acc : cur))[
+        "id_str"
+      ];
+    } else {
+      return undefined;
+    }
+  }
+
+  get enableRefresh() {
+    return this.query.length > 0;
+  }
+
+  get enableInfiniteScroll() {
+    return this.query.length > 0;
+  }
+}

+ 4 - 0
src1/pages/search-results-tweets-recent/search-results-tweets-recent.html

@@ -0,0 +1,4 @@
+<ion-content no-padding>
+  <feed [data]="tweets.statuses" (onRefresh)="doRefresh($event)" (onLoadMore)="loadMore($event)" [enableRefresh]="enableRefresh"
+    [enableInfiniteScroll]="enableInfiniteScroll"></feed>
+</ion-content>

+ 13 - 0
src1/pages/search-results-tweets-recent/search-results-tweets-recent.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { SearchResultsTweetsRecentPage } from './search-results-tweets-recent';
+
+@NgModule({
+  declarations: [
+    SearchResultsTweetsRecentPage,
+  ],
+  imports: [
+    IonicPageModule.forChild(SearchResultsTweetsRecentPage),
+  ],
+})
+export class SearchResultsTweetsRecentPageModule {}

+ 5 - 0
src1/pages/search-results-tweets-recent/search-results-tweets-recent.scss

@@ -0,0 +1,5 @@
+page-search-results-tweets-recent {
+    feed .scroll-content {
+        margin-top: 0 !important;
+    }
+}

+ 78 - 0
src1/pages/search-results-tweets-recent/search-results-tweets-recent.ts

@@ -0,0 +1,78 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  Refresher,
+  InfiniteScroll,
+  Events
+} from "ionic-angular";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+
+@IonicPage()
+@Component({
+  selector: "page-search-results-tweets-recent",
+  templateUrl: "search-results-tweets-recent.html"
+})
+export class SearchResultsTweetsRecentPage {
+  query: string;
+  tweets = [];
+
+  constructor(
+    public navCtrl: NavController,
+    public navParams: NavParams,
+    private twitter: TwitterApiProvider,
+    private events: Events
+  ) {
+    this.query = this.navParams.data;
+
+    this.events.subscribe("query:changed", query => {
+      if (query.length) {
+        this.twitter.searchRecentTweets(query).then(res => (this.tweets = res));
+        this.query = query;
+      }
+    });
+  }
+
+  async ionViewDidLoad() {
+    if (this.query.length) {
+      this.tweets = await this.twitter.searchRecentTweets(this.query);
+    }
+  }
+
+  doRefresh(refresher: Refresher) {
+    this.twitter.searchRecentTweets(this.query).then(tweets => {
+      this.tweets = tweets;
+      refresher.complete();
+    });
+  }
+
+  loadMore(infiniteScroll: InfiniteScroll) {
+    this.twitter
+      .searchRecentTweets(this.query, this.oldestTweet)
+      .then(tweets => {
+        this.tweets["statuses"] = this.tweets["statuses"].concat(
+          tweets["statuses"]
+        );
+        infiniteScroll.complete();
+      });
+  }
+
+  get oldestTweet() {
+    if (this.tweets.length > 0) {
+      return this.tweets.reduce((acc, cur) => (acc.id < cur.id ? acc : cur))[
+        "id_str"
+      ];
+    } else {
+      return undefined;
+    }
+  }
+
+  get enableRefresh() {
+    return this.query.length > 0;
+  }
+
+  get enableInfiniteScroll() {
+    return this.query.length > 0;
+  }
+}

+ 6 - 0
src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.html

@@ -0,0 +1,6 @@
+<ion-content no-padding>
+  <ion-tabs selectedIndex="0" tabsPlacement="top">
+    <ion-tab [root]="searchResultsRecentTweets" [rootParams]="query" tabTitle="Recent"></ion-tab>
+    <ion-tab [root]="searchResultsPopularTweets" [rootParams]="query" tabTitle="Popular"></ion-tab>
+  </ion-tabs>
+</ion-content>

+ 13 - 0
src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { SearchResultsTweetsTabsPage } from './search-results-tweets-tabs';
+
+@NgModule({
+  declarations: [
+    SearchResultsTweetsTabsPage,
+  ],
+  imports: [
+    IonicPageModule.forChild(SearchResultsTweetsTabsPage),
+  ],
+})
+export class SearchResultsTweetsTabsPageModule {}

+ 2 - 0
src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.scss

@@ -0,0 +1,2 @@
+page-search-results-tweets-tabs {
+}

+ 21 - 0
src1/pages/search-results-tweets-tabs/search-results-tweets-tabs.ts

@@ -0,0 +1,21 @@
+import { Component } from "@angular/core";
+import { IonicPage, NavController, NavParams } from "ionic-angular";
+import { SearchResultsTweetsRecentPage } from "../search-results-tweets-recent/search-results-tweets-recent";
+import { SearchResultsTweetsPopularPage } from "../search-results-tweets-popular/search-results-tweets-popular";
+
+@IonicPage()
+@Component({
+  selector: "page-search-results-tweets-tabs",
+  templateUrl: "search-results-tweets-tabs.html"
+})
+export class SearchResultsTweetsTabsPage {
+  searchResultsRecentTweets = SearchResultsTweetsRecentPage;
+  searchResultsPopularTweets = SearchResultsTweetsPopularPage;
+  query: string;
+
+  constructor(public navCtrl: NavController, public navParams: NavParams) {
+    this.query = this.navParams.data;
+  }
+
+  ionViewDidLoad() {}
+}

+ 24 - 0
src1/pages/search-results-users/search-results-users.html

@@ -0,0 +1,24 @@
+<ion-content no-padding fullscreen>
+  <ion-refresher (ionRefresh)="doRefresh($event)" enabled="query.length">
+    <ion-refresher-content pullingText=" Pull to refresh" refreshingText="Refreshing...">
+    </ion-refresher-content>
+  </ion-refresher>
+
+  <ion-list>
+    <ion-item *ngFor="let user of users" (click)="showProfile(user.id_str)">
+      <ion-avatar item-start>
+        <img src="{{ user.profile_image_url_https }}">
+      </ion-avatar>
+      <h2>
+        {{ user.name }}
+        <ion-icon name="ios-checkmark-circle" *ngIf="user.verified" class="icon-verified"></ion-icon>
+        <ion-icon name="ios-lock-outline" *ngIf="user.protected" class="icon-protected"></ion-icon>
+      </h2>
+      <p>@{{ user.screen_name }}</p>
+    </ion-item>
+  </ion-list>
+
+  <ion-infinite-scroll (ionInfinite)="loadMore($event)" enabled="query.length">
+    <ion-infinite-scroll-content loadingText="Loading more users..."></ion-infinite-scroll-content>
+  </ion-infinite-scroll>
+</ion-content>

+ 13 - 0
src1/pages/search-results-users/search-results-users.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { SearchResultsUsersPage } from './search-results-users';
+
+@NgModule({
+  declarations: [
+    SearchResultsUsersPage,
+  ],
+  imports: [
+    IonicPageModule.forChild(SearchResultsUsersPage),
+  ],
+})
+export class SearchResultsUsersPageModule {}

+ 6 - 0
src1/pages/search-results-users/search-results-users.scss

@@ -0,0 +1,6 @@
+page-search-results-users {
+  .icon-verified,
+  .icon-protected {
+    font-size: 1em;
+  }
+}

+ 67 - 0
src1/pages/search-results-users/search-results-users.ts

@@ -0,0 +1,67 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  Refresher,
+  InfiniteScroll,
+  App,
+  Events
+} from "ionic-angular";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+import { ProfilePage } from "../profile/profile";
+
+@IonicPage()
+@Component({
+  selector: "page-search-results-users",
+  templateUrl: "search-results-users.html"
+})
+export class SearchResultsUsersPage {
+  query: string;
+  nextPage: number = 2;
+  users: any[] = [];
+
+  constructor(
+    public navCtrl: NavController,
+    public navParams: NavParams,
+    private appCtrl: App,
+    private twitter: TwitterApiProvider,
+    private events: Events
+  ) {
+    this.query = this.navParams.data;
+
+    this.events.subscribe("query:changed", query => {
+      if (query.length) {
+        this.twitter.searchUsers(query).then(res => (this.users = res));
+        this.query = query;
+      }
+    });
+  }
+
+  async ionViewDidLoad() {
+    if (this.query.length) {
+      this.users = await this.twitter.searchUsers(this.query);
+    }
+  }
+
+  showProfile(userId) {
+    this.appCtrl.getRootNav().push(ProfilePage, { userId });
+    this.nextPage = 2;
+  }
+
+  doRefresh(refresher: Refresher) {
+    this.twitter.searchUsers(this.query).then(users => {
+      this.users = users;
+      this.nextPage = 2;
+      refresher.complete();
+    });
+  }
+
+  loadMore(infiniteScroll: InfiniteScroll) {
+    this.twitter.searchUsers(this.query, this.nextPage).then(users => {
+      this.users = this.users.concat(users);
+      infiniteScroll.complete();
+      this.nextPage = this.nextPage + 1;
+    });
+  }
+}

+ 13 - 0
src1/pages/search/search.html

@@ -0,0 +1,13 @@
+<ion-header>
+    <ion-navbar><button ion-button menuToggle>
+            <ion-icon name="menu"></ion-icon>
+        </button>
+        <ion-searchbar [(ngModel)]="query" (ionInput)="onInput()" debounce="500"></ion-searchbar>
+    </ion-navbar>
+</ion-header>
+<ion-content no-padding>
+    <ion-tabs selectedIndex="0" tabsLayout="icon-start" tabsPlacement="bottom">
+        <ion-tab [root]="searchResultsTweets" [rootParams]="query" tabTitle="Tweets" tabIcon="logo-twitter"></ion-tab>
+        <ion-tab [root]="searchResultsUsers" [rootParams]="query" tabTitle="Users" tabIcon="person"></ion-tab>
+    </ion-tabs>
+</ion-content>

+ 0 - 0
src1/pages/search/search.scss


+ 26 - 0
src1/pages/search/search.ts

@@ -0,0 +1,26 @@
+import { Component } from "@angular/core";
+import { NavController, NavParams, Events } from "ionic-angular";
+import { SearchResultsUsersPage } from "../search-results-users/search-results-users";
+import { SearchResultsTweetsTabsPage } from "../search-results-tweets-tabs/search-results-tweets-tabs";
+
+@Component({
+  selector: "page-search",
+  templateUrl: "search.html"
+})
+export class SearchPage {
+  searchResultsTweets = SearchResultsTweetsTabsPage;
+  searchResultsUsers = SearchResultsUsersPage;
+  query: string;
+
+  constructor(
+    public navCtrl: NavController,
+    private navParams: NavParams,
+    private events: Events
+  ) {
+    this.query = this.navParams.get("query");
+  }
+
+  onInput() {
+    this.events.publish("query:changed", this.query.trim());
+  }
+}

+ 40 - 0
src1/pages/settings/settings.html

@@ -0,0 +1,40 @@
+<ion-header>
+  <ion-navbar>
+    <button ion-button menuToggle>
+      <ion-icon name="menu"></ion-icon>
+    </button>
+    <ion-title>Settings</ion-title>
+  </ion-navbar>
+</ion-header>
+
+<ion-content padding>
+  <ion-card>
+    <ion-card-header>Keywords for Private Mode</ion-card-header>
+    <ion-card-content>
+      <p>The following hashtags will automatically activate the private mode when liking or writing a tweet.</p>
+      <ion-label color="primary" stacked>Keywords:</ion-label>
+      <ion-textarea [(ngModel)]="keywords"></ion-textarea>
+    </ion-card-content>
+  </ion-card>
+
+  <ion-card>
+    <ion-card-header>Encryption</ion-card-header>
+    <ion-card-content>
+      <p>To protect your privacy, all data send to the P2P network will be encrypted. Therefore you need to enter or
+        generate
+        a pair of keys. If you run the app on multiple devices, please enter everywhere the same pair of keys.</p>
+      <ion-label color="primary" stacked>Enter your email address:</ion-label>
+      <ion-input [(ngModel)]="email" type="email" required></ion-input>
+      <button ion-button block (click)="generateKeys()">Generate keys</button>
+      <ion-label color="primary" stacked>Private Key:</ion-label>
+      <ion-textarea [(ngModel)]="privateKey"></ion-textarea>
+      <ion-label color="primary" stacked>Public Key:</ion-label>
+      <ion-textarea [(ngModel)]="publicKey"></ion-textarea>
+      <p>Under no circumstances share your private key with any other person!</p>
+      <button ion-button block (click)="publishPublicKey()">Publish public key</button>
+      <button ion-button block (click)="exportPrivateKey()">Export private key</button>
+    </ion-card-content>
+  </ion-card>
+
+  <button ion-button block (click)="save()">Save settings</button>
+</ion-content>

+ 0 - 0
src1/pages/settings/settings.scss


+ 151 - 0
src1/pages/settings/settings.ts

@@ -0,0 +1,151 @@
+import { Component } from "@angular/core";
+import {
+  NavController,
+  ToastController,
+  LoadingController,
+  AlertController
+} from "ionic-angular";
+import { Storage } from "@ionic/storage";
+import { CryptoProvider } from "../../providers/crypto/crypto";
+import { SocialSharing } from "@ionic-native/social-sharing";
+import * as openpgp from 'openpgp';
+
+@Component({
+  selector: "page-settings",
+  templateUrl: "settings.html"
+})
+export class SettingsPage {
+  keywords: string;
+  privateKey: string;
+  publicKey: string;
+  email: string;
+  hkp = new openpgp.HKP('https://sks-keyservers.net/');
+
+  constructor(
+    public navCtrl: NavController,
+    public toastCtrl: ToastController,
+    private cryptoUtils: CryptoProvider,
+    private storage: Storage,
+    private loadingCtrl: LoadingController,
+    private sharing: SocialSharing,
+    private alertCtrl: AlertController
+  ) {
+    this.loadValuesFromStorage();
+  }
+
+  async loadValuesFromStorage() {
+    this.privateKey = await this.storage.get("privateKey");
+    this.publicKey = await this.storage.get("publicKey");
+    this.keywords = await this.storage.get("keywords");
+    this.email = await this.storage.get("email");
+  }
+
+  generateKeys() {
+    if (!this.email){
+          console.log("email is not provided or not valid");
+          return;
+    }
+    else{
+      this.storage.set("email", this.email);
+      if (this.publicKey || this.privateKey) {
+        const alert = this.alertCtrl.create({
+          title: "Are you sure?",
+          subTitle:
+            "You already have keys entered. Do you want to overwrite them?",
+          buttons: [
+            {
+              text: "No",
+              role: "cancel"
+            },
+            {
+              text: "Yes",
+              handler: () => {
+                this.startKeyGeneration();
+              }
+            }
+          ]
+        });
+
+        alert.present();
+      } else {
+        this.startKeyGeneration();
+      }
+    }
+  }
+
+  private async startKeyGeneration() {
+    const keys = await this.cryptoUtils.generatePgpKeys(this.email);
+    
+    this.privateKey = keys.privateKeyArmored;
+    this.publicKey = keys.publicKeyArmored;
+    console.log("public key", this.privateKey);
+    console.log("public key", this.publicKey);
+    // this.privateKey = await this.cryptoUtils.extractPrivateKey(keys);
+  }
+
+  save() {
+    this.storage.set("publicKey", this.publicKey);
+    this.storage.set("privateKey", this.privateKey);
+    this.storage.set("keywords", this.keywords ? this.keywords.trim() : "");
+
+    this.showToast("Successfully saved!");
+  }
+
+  async publishPublicKey() {    
+    await this.publishPublicKey2();
+    await this.lookupKeys(this.email);
+  }
+  
+  async publishPublicKey2() {      
+    const loading = this.loadingCtrl.create();
+      loading.present();
+      // console.log("Uploding publish public key", this.publicKey);
+      if(!this.publicKey) return;
+      this.hkp.upload(this.publicKey).then(function() {
+        console.log("Uploding public key");
+      });
+      loading.dismiss();
+      this.showToast("Public key has been published!");
+      //lookup key to verify it has been pubblished
+      // await this.lookupKeys(this.email);
+  }
+
+  public async lookupKeys(email:string){
+
+    var options = {
+        query: email
+    };
+
+    let armoredPubkey = await this.hkp.lookup(options);
+    let pubkey = await openpgp.key.readArmored(armoredPubkey);
+
+     console.log('Found public key:',pubkey);
+     // if(! (email == 'rohit.hosn@gmail.com'))
+     //   this.pk.push(pubkey.publicKeyArmored);
+     // return pubkey;
+  }
+
+  exportPrivateKey() {
+    if (this.privateKey.length) {
+      this.sharing
+        .share(this.privateKey, null, null, null)
+        .then(() => console.log("Private key was exported"))
+        .catch(() =>
+          this.showToast(
+            "Sorry! Something went wrong trying to export the private key :("
+          )
+        );
+    } else {
+      this.showToast("There is nothing to share.");
+    }
+  }
+
+  private showToast(message: string) {
+    const toast = this.toastCtrl.create({
+      message: message,
+      position: "bottom",
+      duration: 3000
+    });
+    toast.present();
+  }
+}

+ 45 - 0
src1/pages/write-tweet/write-tweet.html

@@ -0,0 +1,45 @@
+<ion-header>
+  <ion-navbar>
+    <ion-title>Tweet something</ion-title>
+  </ion-navbar>
+</ion-header>
+
+<ion-content padding>
+  <!-- Show tweet to retweet or quote (if passed to the page) -->
+  <ion-label *ngIf="retweet" color="primary">Retweet</ion-label>
+  <quoted-status *ngIf="retweet" [data]="retweet.data"></quoted-status>
+
+  <!-- Show tweet to reply to -->
+  <ion-label *ngIf="replyTweet" color="primary">Reply to</ion-label>
+  <quoted-status *ngIf="replyTweet" [data]="replyTweet.data"></quoted-status>
+
+  <!-- Form to write a tweet -->
+  <form [formGroup]="tweet" (ngSubmit)="submitTweet()">
+    <ion-item class="padding-0">
+      <ion-label color="primary" stacked>Your tweet</ion-label>
+      <ion-textarea type="text" formControlName="text" maxlength="140" [attr.rows]="4"></ion-textarea>
+    </ion-item>
+
+    <div class="actions">
+      <span class="progress">
+        <svg width="20" height="20" class="progress-circle">
+          <circle class="background-stroke" cx="10" cy="10" r="8"></circle>
+          <circle class="progress-stroke" cx="10" cy="10" r="8" transform="rotate(-90, 10, 10)" [style.strokeDashoffset]="tweetCharProgress"></circle>
+        </svg>
+
+        <span class="progress-stats">{{ (tweet.value.text).length }}/140</span>
+      </span>
+
+      <span class="network-switch">
+        <ion-icon name="logo-twitter"></ion-icon>
+        <ion-toggle checked="false" formControlName="p2p" color="dark"></ion-toggle>
+        <ion-icon name="glasses"></ion-icon>
+      </span>
+
+      <ion-icon name="warning" *ngIf="showTrigger" class="warning" (click)="showTriggerInfo()"></ion-icon>
+
+      <button ion-button type="submit" class="submit-tweet">tweet!</button>
+    </div>
+  </form>
+
+</ion-content>

+ 9 - 0
src1/pages/write-tweet/write-tweet.module.ts

@@ -0,0 +1,9 @@
+import { NgModule } from "@angular/core";
+import { IonicPageModule } from "ionic-angular";
+import { WriteTweetPage } from "./write-tweet";
+
+@NgModule({
+  declarations: [WriteTweetPage],
+  imports: [IonicPageModule.forChild(WriteTweetPage)]
+})
+export class WriteTweetPageModule {}

+ 53 - 0
src1/pages/write-tweet/write-tweet.scss

@@ -0,0 +1,53 @@
+page-write-tweet {
+  .padding-0 {
+    padding: 0;
+  }
+  .actions {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    .progress {
+      display: flex;
+      align-items: center;
+      .progress-stats {
+        margin-left: 5px;
+      }
+      .background-stroke,
+      .progress-stroke {
+        fill: transparent;
+        stroke-width: 2;
+        stroke-dasharray: 50.2654825;
+        transition: stroke-dashoffset 0.5s;
+        -webkit-animation-play-state: running;
+      }
+      .background-stroke {
+        stroke: #ddd;
+      }
+      .progress-stroke {
+        stroke: blue;
+      }
+    }
+    .network-switch {
+      display: flex;
+      align-items: center;
+      margin-left: 16px;
+    }
+  }
+  ion-icon.warning {
+    color: orange;
+    animation: pulse 1.8s infinite;
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.3);
+  }
+  100% {
+    transform: scale(1);
+  }
+}

+ 379 - 0
src1/pages/write-tweet/write-tweet.ts

@@ -0,0 +1,379 @@
+import { Component } from "@angular/core";
+import {
+  IonicPage,
+  NavController,
+  NavParams,
+  LoadingController,
+  AlertController
+} from "ionic-angular";
+import {
+  FormBuilder,
+  Validators,
+  FormGroup,
+  ValidatorFn,
+  AbstractControl
+} from "@angular/forms";
+import { TwitterApiProvider } from "../../providers/twitter-api/twitter-api";
+import { Storage } from "@ionic/storage";
+import { P2pStorageIpfsProvider } from "../../providers/p2p-storage-ipfs/p2p-storage-ipfs";
+import { P2pDatabaseGunProvider } from "../../providers/p2p-database-gun/p2p-database-gun";
+import twittertext from "twitter-text";
+import { CryptoProvider } from "../../providers/crypto/crypto";
+import * as openpgp from 'openpgp';
+
+@IonicPage()
+@Component({
+  selector: "page-write-tweet",
+  templateUrl: "write-tweet.html"
+})
+export class WriteTweetPage {
+  tweet: FormGroup;
+  retweetId: string;
+  replyToStatusId: string;
+  retweet;
+  replyTweet;
+  openpgp;
+  privateKey;
+  publicKey;
+  pk: any[]=[];
+  passp = 'super long and hard to guess secret';
+  hkp = new openpgp.HKP();
+ 
+
+  constructor(
+    public navCtrl: NavController,
+    public navParams: NavParams,
+    private formBuilder: FormBuilder,
+    private twitter: TwitterApiProvider,
+    private loadingCtrl: LoadingController,
+    private storage: Storage,
+    private ipfs: P2pStorageIpfsProvider,
+    private gun: P2pDatabaseGunProvider,
+    private cryptoUtils: CryptoProvider,
+    private alertCtrl: AlertController
+  ) {
+    this.retweetId = this.navParams.get("tweetId");
+    this.replyToStatusId = this.navParams.get("replyToStatus");
+
+    this.tweet = this.formBuilder.group({
+      text: [""],
+      p2p: [false]
+    });
+
+    this.addValidators();
+    // this.encryptDecryptFunction();
+  }
+
+
+//    public async encryptDecryptFunction () {
+//      await openpgp.initWorker({path:'assets/scripts/openpgp.worker.js'});
+//     let a =  await this.generateKeys();
+//     console.log('a is:',a.publicKeyArmored);
+//     let b  =  await this.generateKeys();
+//     console.log('b is:',b.publicKeyArmored);
+//     let c  =  await this.generateKeys();
+//     this.privateKey =c.privateKeyArmored;
+//     this.publicKey = c.publicKeyArmored;
+
+//     this.pk.push(a.publicKeyArmored);
+//     this.pk.push(b.publicKeyArmored);
+// //     this.pk = [`----BEGIN PGP PUBLIC KEY BLOCK-----
+// // Version: OpenPGP.js v4.7.1
+// // Comment: https://openpgpjs.org
+
+// // xjMEXfAn1xYJKwYBBAHaRw8BAQdAAMVNOABw8MBtrtYR8KC3tSro3wITyApT
+// // TVjKVCppD+DNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
+// // BQJd8CfXBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRD+efBRXzuMsfA7
+// // AQCEgoToFzv2hT9BREdiQp531/AHSyoZWmWvSZSvmga40gD8C+zwbCySnkhQ
+// // pb4L0DCKtSDa7pLg2g0OcxJlbSZWHQ3OOARd8CfXEgorBgEEAZdVAQUBAQdA
+// // p4mVY17dPWf6VCBqW10Ybk5JgUO6FK0OsETWw3gG2zcDAQgHwmEEGBYIAAkF
+// // Al3wJ9cCGwwACgkQ/nnwUV87jLFHbAD9GyoL7dcTDGQoqtrhKozdgnzfugTb
+// // er0bwU15WNMjefkA/jEqK9YUNcRrFKIuac9PVibGgutL8ak7ukysw6iTcCsM
+// // =fmhE
+// // -----END PGP PUBLIC KEY BLOCK-----`,
+// // `-----BEGIN PGP PUBLIC KEY BLOCK-----
+// // Version: OpenPGP.js v4.7.1
+// // Comment: https://openpgpjs.org
+
+// // xjMEXfAn1hYJKwYBBAHaRw8BAQdAsF1ivpd0HU8ogj02LDv6BTOxNMWGZaEc
+// // OyZBwqoYJPrNG0pvbiBTbWl0aCA8am9uQGV4YW1wbGUuY29tPsJ3BBAWCgAf
+// // BQJd8CfWBgsJBwgDAgQVCAoCAxYCAQIZAQIbAwIeAQAKCRDEruv77flRJ32B
+// // AP93GIBcUW2okROoZZhdPVeqjRD72Ft64imXpdZ0jx4ohgEA5Kv9vs2kV73q
+// // k6fcdf7qD/i5gMExU0+vV05c9VxBYwfOOARd8CfWEgorBgEEAZdVAQUBAQdA
+// // 1J7E03ZopUnsIeNzeiZvba6qxhhUbpmBZ1aN1HhWUlEDAQgHwmEEGBYIAAkF
+// // Al3wJ9YCGwwACgkQxK7r++35USdTqQD/ZEg8X5tMx75nQe4mGlyiRjmmtWLw
+// // n9bslTdjBIszs/EA/R1WIm6ji4Ru1dJWc3ISisz78xTM2H8U7fnP8yjFcWcD
+// // =hgnW
+// // -----END PGP PUBLIC KEY BLOCK-----`];
+//     console.log('array of pub keys is :',this.pk);
+//      this.pk = this.pk.map(async (key) => {
+//       return (await openpgp.key.readArmored(key)).keys[0]
+//     });
+
+//     console.log('priv key: ',this.privateKey,'this.pubkey',this.pk);
+//     let encrypted;
+//     const privKeyObj = (await openpgp.key.readArmored(this.privateKey)).keys[0];
+//     console.log('privKeyObj',privKeyObj);
+//     const bla = await privKeyObj.decrypt(this.passp);
+
+//     // const options = {
+//     //     message: openpgp.message.fromText('Hello, World!'),       // input as Message object
+//     //     publicKeys: (await openpgp.key.readArmored(this.publicKey)).keys, // for encryption
+//     //     privateKeys: [privKeyObj]                                 // for signing (optional)
+//     // }
+
+//     const options = {
+//         message: openpgp.message.fromText('Hello, World!'),       // input as Message object
+//         publicKeys: await Promise.all(this.pk), // for encryption
+//         privateKeys: [privKeyObj]                                 // for signing (optional)
+//     }
+
+//     const ciphertext = await openpgp.encrypt(options);
+//       encrypted = ciphertext.data; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
+//         console.log('encrypted is:',encrypted);
+   
+//    let aprivKeyObj = (await openpgp.key.readArmored(a.privateKeyArmored)).keys[0];
+//    await aprivKeyObj.decrypt(this.passp);
+
+//     const options2 = {
+//         message: await openpgp.message.readArmored(encrypted),    // parse armored message
+//         privateKeys: [aprivKeyObj]                                 // for decryption
+//     }
+//     console.log('options2 is: ',options2);
+//     let plaintext = await openpgp.decrypt(options2);
+//     console.log('decrypted text is:',plaintext,plaintext.data);
+//     return plaintext.data // 'Hello, World!'
+
+//   }
+
+  // public async generateKeys(){
+  //   let options = {
+  //     userIds: [{ name:'Jon Smith', email:'jon@example.com' }], // multiple user IDs
+  //     curve: "ed25519",                                         // ECC curve name
+  //     passphrase: this.passp        // protects the private key
+  //   };
+   
+  //    let a = await openpgp.generateKey(options);
+  //    return a;
+  //     // console.log('resolved a = ',a);
+  //     //     this.privateKey =a.privateKeyArmored;
+  //     //     this.publicKey = a.publicKeyArmored;
+  //     //     this.encryptDecryptFunction();
+  // }
+
+
+  private async addValidators() {
+    const triggerWords = await this.storage.get("keywords");
+    const validators = [
+      Validators.maxLength(140),
+      this.containsTriggerWord(triggerWords)
+    ];
+    this.tweet.controls["text"].setValidators(validators);
+  }
+
+  private containsTriggerWord(triggerWords: string): ValidatorFn {
+    return (control: AbstractControl): { [key: string]: any } | null => {
+      if (triggerWords) {
+        const regexList = triggerWords
+          .toLowerCase()
+          .split(", ")
+          .join("|");
+        const regex = new RegExp(regexList);
+        const containsTriggerWord = regex.test(control.value.toLowerCase());
+        return containsTriggerWord
+          ? { containsTriggerWord: { value: control.value } }
+          : null;
+      } else {
+        return null;
+      }
+    };
+  }
+
+  async ionViewDidLoad() {
+    if (this.retweetId) {
+      this.retweet = await this.twitter.fetchTweet(this.retweetId);
+    }
+    if (this.replyToStatusId) {
+      this.replyTweet = await this.twitter.fetchTweet(this.replyToStatusId);
+    }
+  }
+
+  get tweetCharProgress() {
+    const progress = 1 - this.tweet.value["text"].length / 140;
+    const radius = 8;
+    const circumference = Math.PI * radius * 2;
+    return progress * circumference;
+  }
+
+  get showTrigger(): boolean {
+    return (
+      this.tweet &&
+      this.tweet.controls &&
+      this.tweet.controls.text &&
+      this.tweet.controls.text.errors &&
+      this.tweet.controls.text.errors["containsTriggerWord"] &&
+      !this.tweet.controls.p2p.value
+    );
+  }
+
+  showTriggerInfo() {
+    this.alertCtrl
+      .create({
+        title: "Watch Out!",
+        message:
+          "Your tweet contains words you have previously defined to only share securely via P2P. Currently P2P mode is not selected.",
+        buttons: ["OK"]
+      })
+      .present();
+  }
+
+  async submitTweet() {
+    console.log('Submitting tweet')
+    const loading = this.loadingCtrl.create();
+    loading.present();
+
+    if (this.tweet.value.p2p) {
+      loading.setContent("Validate keys...");
+      if (
+        (await this.cryptoUtils.isPrivateKeySet()) &&
+        (await this.cryptoUtils.isPublicKeyPublished())
+      ) {
+        loading.setContent("Publish private tweet...");
+        await this.tweetPrivate();
+        } else {
+        loading.dismiss();
+        const alert = this.alertCtrl.create({
+          title: "Oooops...",
+          message:
+            "Please verify that you have set a private and public key in the settings and that your latest public key was published."
+        });
+        alert.present();
+        return;
+      }
+    } else {
+      loading.setContent("Publish on Twitter...");
+      await this.twitter.tweet(
+        this.tweet.value["text"],
+        this.retweet,
+        this.replyToStatusId
+      );
+    }
+
+    loading.dismiss();
+    this.navCtrl.pop();
+  }
+
+   public async lookupKeys(email:string){
+     var options = {
+        query: email
+    };
+
+    let armoredPubkey = await this.hkp.lookup(options);
+    console.log('armord pubkey',armoredPubkey);
+
+    let pubkey = (await openpgp.key.readArmored(armoredPubkey));
+    // console.log('array of opubkes returened from server',pubkey);
+
+     pubkey = (await openpgp.key.readArmored(armoredPubkey)).keys[0];
+    // console.log('latest  pubkey is:',pubkey);
+     // console.log('Found public key:',pubkey);
+    
+    this.pk.push(pubkey);
+
+  }
+
+  private async tweetPrivate() {
+    const tweet = await this.buildPrivateTweet();
+    console.log('tweet is:',tweet.full_text);
+    const privateKey = await this.storage.get("privateKey");
+    //fetch followers and their public keys
+    //assuming the email id of rohit.shiva.gowda
+    await this.lookupKeys('rohit.hosn@gmail.com');
+
+
+     console.log('array of pub keys is :', this.pk);
+    this.pk = this.pk.map(async (key) => {
+      console.log('key is:',key);
+      return (await openpgp.key.readArmored(key)).keys[0]
+    });
+
+    console.log("after mapping", this.pk);
+
+    const options = {
+      message: openpgp.message.fromText(tweet), // input as Message object
+      publicKeys: await Promise.all(this.pk), // for encryption
+      // privateKeys: [privKeyObj] // for signing (optional)
+    }
+
+    const ciphertext = await openpgp.encrypt(options);
+
+    const encryptedTweet = ciphertext.data;
+    console.log('encrypted tweet is:',encryptedTweet);
+
+    // const encryptedTweet = this.cryptoUtils.encrypt(
+    //   JSON.stringify(tweet),
+    //   privateKey
+    // );
+    const res = await this.ipfs.storeTweet(encryptedTweet);
+
+    this.gun.storeLastTweetHashForUser(
+      tweet.user_id,
+      res["Hash"],
+      tweet.created_at
+    );
+
+    this.gun.publishHashtags(tweet.entities.hashtags);
+  }
+
+  private async buildPrivateTweet() {
+    const status = this.tweet.value["text"].trim();
+    const entities = await this.getEntities(status);
+
+    return {
+      full_text: status,
+      user_id: await this.storage.get("userId"),
+      created_at: Date.now(),
+      private_tweet: true,
+      in_reply_to_status_id: this.replyToStatusId,
+      quoted_status_id: this.retweetId,
+      display_text_range: [0, status.length],
+      entities: entities
+    };
+  }
+
+  private async getEntities(status: string) {
+    return {
+      hashtags: twittertext.extractHashtagsWithIndices(status),
+      urls: twittertext.extractUrlsWithIndices(status),
+      user_mentions: await this.getMentions(status)
+    };
+  }
+
+  private async getMentions(status: string) {
+    // extract mentions
+    const entities = twittertext.extractMentionsWithIndices(status);
+
+    // add user_id
+    const entitiesWithPromises = entities.map(async mention => {
+      try {
+        const user = await this.twitter.fetchUserFromScreenName(
+          mention.screenName
+        );
+        mention["id_str"] = user[0]["id_str"];
+        mention["screen_name"] = mention.screenName;
+        delete mention.screenName;
+      } catch (err) {
+        console.error(
+          "There is no user signed up to twitter with username: " +
+            mention.screenName
+        );
+      }
+      return mention;
+    });
+
+    // filter for valid users and return
+    return (await Promise.all(entitiesWithPromises)).filter(el =>
+      el.hasOwnProperty("id_str")
+    );
+  }
+}

+ 18 - 0
src1/pipes/diff-for-humans/diff-for-humans.ts

@@ -0,0 +1,18 @@
+import { Pipe, PipeTransform } from "@angular/core";
+import TimeAgo from "javascript-time-ago";
+import en from "javascript-time-ago/locale/en";
+
+TimeAgo.locale(en);
+const timeAgo = new TimeAgo("en-US");
+
+@Pipe({
+  name: "diffForHumans"
+})
+export class DiffForHumansPipe implements PipeTransform {
+  /**
+   * Takes a timestamp and makes the diff readable for humans.
+   */
+  transform(value: string, ...args) {
+    return timeAgo.format(new Date(value), "twitter");
+  }
+}

+ 19 - 0
src1/pipes/friendly-number/friendly-number.ts

@@ -0,0 +1,19 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+@Pipe({
+  name: "friendlyNumber"
+})
+export class FriendlyNumberPipe implements PipeTransform {
+  /**
+   * Takes a number and formats it using binary prefixes
+   */
+  transform(value: number, ...args) {
+    if (value > 1000000) {
+      return (value / 1000000).toFixed(1) + "M";
+    } else if (value > 1000) {
+      return (value / 1000).toFixed(1) + "k";
+    } else {
+      return value;
+    }
+  }
+}

+ 15 - 0
src1/pipes/high-resolution/high-resolution.ts

@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+@Pipe({
+  name: "highResolution"
+})
+export class HighResolutionPipe implements PipeTransform {
+  /**
+   * Takes a profile img URL string and removes "normal" from the URL to receive a high resolution image
+   */
+  transform(value: string, ...args) {
+    if (value) {
+      return value.replace("_normal", "_400x400");
+    }
+  }
+}

+ 24 - 0
src1/pipes/pipes.module.ts

@@ -0,0 +1,24 @@
+import { NgModule } from "@angular/core";
+import { FriendlyNumberPipe } from "./friendly-number/friendly-number";
+import { DiffForHumansPipe } from "./diff-for-humans/diff-for-humans";
+import { ReplaceUrlsPipe } from "./replace-urls/replace-urls";
+import { ReplaceHashtagsPipe } from "./replace-hashtags/replace-hashtags";
+import { HighResolutionPipe } from './high-resolution/high-resolution';
+@NgModule({
+  declarations: [
+    FriendlyNumberPipe,
+    DiffForHumansPipe,
+    ReplaceUrlsPipe,
+    ReplaceHashtagsPipe,
+    HighResolutionPipe
+  ],
+  imports: [],
+  exports: [
+    FriendlyNumberPipe,
+    DiffForHumansPipe,
+    ReplaceUrlsPipe,
+    ReplaceHashtagsPipe,
+    HighResolutionPipe
+  ]
+})
+export class PipesModule {}

+ 20 - 0
src1/pipes/replace-hashtags/replace-hashtags.ts

@@ -0,0 +1,20 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+@Pipe({
+  name: "replaceHashtags"
+})
+export class ReplaceHashtagsPipe implements PipeTransform {
+  /**
+   * Takes a string and highlights the hashtags.
+   */
+  transform(value: string, ...args) {
+    for (let hashtag of args[0]) {
+      value = value.replace(
+        "#" + hashtag.text,
+        '<span class="hashtag">#' + hashtag.text + "</span>"
+      );
+    }
+
+    return value;
+  }
+}

Some files were not shown because too many files changed in this diff