Add address link previews
This commit is contained in:
		
							parent
							
								
									d1ad9efe64
								
							
						
					
					
						commit
						5854931430
					
				@ -6,6 +6,7 @@ import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
 | 
			
		||||
import { BlockPreviewComponent } from './components/block/block-preview.component';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { AddressPreviewComponent } from './components/address/address-preview.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
@ -69,7 +70,10 @@ let routes: Routes = [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'address/:id',
 | 
			
		||||
            children: [],
 | 
			
		||||
            component: AddressComponent
 | 
			
		||||
            component: AddressComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              ogImage: true
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'tx',
 | 
			
		||||
@ -175,7 +179,10 @@ let routes: Routes = [
 | 
			
		||||
          {
 | 
			
		||||
            path: 'address/:id',
 | 
			
		||||
            children: [],
 | 
			
		||||
            component: AddressComponent
 | 
			
		||||
            component: AddressComponent,
 | 
			
		||||
            data: {
 | 
			
		||||
              ogImage: true
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'tx',
 | 
			
		||||
@ -278,7 +285,10 @@ let routes: Routes = [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressComponent
 | 
			
		||||
        component: AddressComponent,
 | 
			
		||||
        data: {
 | 
			
		||||
          ogImage: true
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tx',
 | 
			
		||||
@ -342,6 +352,21 @@ let routes: Routes = [
 | 
			
		||||
        path: 'signet/block/:id',
 | 
			
		||||
        component: BlockPreviewComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressPreviewComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'testnet/address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressPreviewComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'signet/address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressPreviewComponent
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@ -415,7 +440,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
            {
 | 
			
		||||
              path: 'address/:id',
 | 
			
		||||
              children: [],
 | 
			
		||||
              component: AddressComponent
 | 
			
		||||
              component: AddressComponent,
 | 
			
		||||
              data: {
 | 
			
		||||
                ogImage: true
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: 'tx',
 | 
			
		||||
@ -522,7 +550,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
        {
 | 
			
		||||
          path: 'address/:id',
 | 
			
		||||
          children: [],
 | 
			
		||||
          component: AddressComponent
 | 
			
		||||
          component: AddressComponent,
 | 
			
		||||
          data: {
 | 
			
		||||
            ogImage: true
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'tx',
 | 
			
		||||
@ -595,6 +626,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
 | 
			
		||||
          path: 'testnet/block/:id',
 | 
			
		||||
          component: BlockPreviewComponent
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'address/:id',
 | 
			
		||||
          children: [],
 | 
			
		||||
          component: AddressPreviewComponent
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: 'testnet/address/:id',
 | 
			
		||||
          children: [],
 | 
			
		||||
          component: AddressPreviewComponent
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,55 @@
 | 
			
		||||
<div class="box preview-box" *ngIf="address && !error">
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-md">
 | 
			
		||||
      <div class="title-address">
 | 
			
		||||
        <h1 i18n="shared.address">Address</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      <a [routerLink]="['/address/' | relativeUrl, addressString]" class="address-link" >
 | 
			
		||||
        <span class="truncated-address">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span>
 | 
			
		||||
      </a>
 | 
			
		||||
      <table class="table table-borderless table-striped">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr *ngIf="addressInfo && addressInfo.unconfidential">
 | 
			
		||||
            <td i18n="address.unconfidential">Unconfidential</td>
 | 
			
		||||
            <td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
 | 
			
		||||
              <span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
 | 
			
		||||
              <span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
 | 
			
		||||
            </a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <ng-template [ngIf]="!address.electrum">
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="address.total-received">Total received</td>
 | 
			
		||||
              <td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received" [noFiat]="true"></app-amount></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td i18n="address.total-sent">Total sent</td>
 | 
			
		||||
              <td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </ng-template>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="address.balance">Balance</td>
 | 
			
		||||
            <td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received - sent" [noFiat]="true"></app-amount></td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="address.transactions">Transactions</td>
 | 
			
		||||
            <td>{{ txCount | number }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="address.unspent_txos">Unspent TXOs</td>
 | 
			
		||||
            <td>{{ totalUnspent | number }}</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="w-100 d-block d-md-none"></div>
 | 
			
		||||
    <div class="col-md qrcode-col">
 | 
			
		||||
      <div class="qr-wrapper">
 | 
			
		||||
        <app-qrcode [data]="address.address" [size]="370"></app-qrcode>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template #confidentialTd>
 | 
			
		||||
  <td i18n="shared.confidential">Confidential</td>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,46 @@
 | 
			
		||||
h1 {
 | 
			
		||||
  font-size: 42px;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qr-wrapper {
 | 
			
		||||
  background-color: #FFF;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  padding-bottom: 5px;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qrcode-col {
 | 
			
		||||
  width: 420px;
 | 
			
		||||
  min-width: 420px;
 | 
			
		||||
  flex-grow: 0;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table {
 | 
			
		||||
  font-size: 24px;
 | 
			
		||||
 | 
			
		||||
  ::ng-deep .symbol {
 | 
			
		||||
    font-size: 18px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.address-link {
 | 
			
		||||
  font-size: 20px;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  align-items: baseline;
 | 
			
		||||
  .truncated-address {
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    max-width: calc(505px - 4em);
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
  }
 | 
			
		||||
  .last-four {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								frontend/src/app/components/address/address-preview.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								frontend/src/app/components/address/address-preview.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
 | 
			
		||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
 | 
			
		||||
import { AudioService } from 'src/app/services/audio.service';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { of, merge, Subscription, Observable } from 'rxjs';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { AddressInformation } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-address-preview',
 | 
			
		||||
  templateUrl: './address-preview.component.html',
 | 
			
		||||
  styleUrls: ['./address-preview.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class AddressPreviewComponent implements OnInit, OnDestroy {
 | 
			
		||||
  network = '';
 | 
			
		||||
 | 
			
		||||
  address: Address;
 | 
			
		||||
  addressString: string;
 | 
			
		||||
  isLoadingAddress = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
  mainSubscription: Subscription;
 | 
			
		||||
  addressLoadingStatus$: Observable<number>;
 | 
			
		||||
  addressInfo: null | AddressInformation = null;
 | 
			
		||||
 | 
			
		||||
  totalConfirmedTxCount = 0;
 | 
			
		||||
  loadedConfirmedTxCount = 0;
 | 
			
		||||
  txCount = 0;
 | 
			
		||||
  received = 0;
 | 
			
		||||
  sent = 0;
 | 
			
		||||
  totalUnspent = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private openGraphService: OpenGraphService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.openGraphService.setPreviewLoading();
 | 
			
		||||
    this.stateService.networkChanged$.subscribe((network) => this.network = network);
 | 
			
		||||
 | 
			
		||||
    this.addressLoadingStatus$ = this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap(() => this.stateService.loadingIndicators$),
 | 
			
		||||
        map((indicators) => indicators['address-' + this.addressString] !== undefined ? indicators['address-' + this.addressString] : 0)
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.mainSubscription = this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.error = undefined;
 | 
			
		||||
          this.isLoadingAddress = true;
 | 
			
		||||
          this.loadedConfirmedTxCount = 0;
 | 
			
		||||
          this.address = null;
 | 
			
		||||
          this.addressInfo = null;
 | 
			
		||||
          this.addressString = params.get('id') || '';
 | 
			
		||||
          if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
 | 
			
		||||
            this.addressString = this.addressString.toLowerCase();
 | 
			
		||||
          }
 | 
			
		||||
          this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
 | 
			
		||||
 | 
			
		||||
          return this.electrsApiService.getAddress$(this.addressString)
 | 
			
		||||
            .pipe(
 | 
			
		||||
              catchError((err) => {
 | 
			
		||||
                this.isLoadingAddress = false;
 | 
			
		||||
                this.error = err;
 | 
			
		||||
                console.log(err);
 | 
			
		||||
                return of(null);
 | 
			
		||||
              })
 | 
			
		||||
            );
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      .pipe(
 | 
			
		||||
        filter((address) => !!address),
 | 
			
		||||
        tap((address: Address) => {
 | 
			
		||||
          if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
 | 
			
		||||
            this.apiService.validateAddress$(address.address)
 | 
			
		||||
              .subscribe((addressInfo) => {
 | 
			
		||||
                this.addressInfo = addressInfo;
 | 
			
		||||
              });
 | 
			
		||||
          }
 | 
			
		||||
          this.address = address;
 | 
			
		||||
          this.updateChainStats();
 | 
			
		||||
          this.isLoadingAddress = false;
 | 
			
		||||
          this.openGraphService.setPreviewReady();
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      .subscribe(() => {},
 | 
			
		||||
        (error) => {
 | 
			
		||||
          console.log(error);
 | 
			
		||||
          this.error = error;
 | 
			
		||||
          this.isLoadingAddress = false;
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateChainStats() {
 | 
			
		||||
    this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
 | 
			
		||||
    this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
 | 
			
		||||
    this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
 | 
			
		||||
    this.totalConfirmedTxCount = this.address.chain_stats.tx_count;
 | 
			
		||||
    this.totalUnspent = this.address.chain_stats.funded_txo_count - this.address.chain_stats.spent_txo_count;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.mainSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,3 @@
 | 
			
		||||
.box {
 | 
			
		||||
  padding: 2rem 3rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-title {
 | 
			
		||||
  margin-bottom: 0.75em;
 | 
			
		||||
  font-size: 42px;
 | 
			
		||||
 | 
			
		||||
@ -58,4 +58,14 @@ export class OpenGraphService {
 | 
			
		||||
    this.metaService.updateTag({ property: 'og:image:width', content: '1000' });
 | 
			
		||||
    this.metaService.updateTag({ property: 'og:image:height', content: '500' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// signal that the unfurler should wait for a 'ready' signal before taking a screenshot
 | 
			
		||||
  setPreviewLoading() {
 | 
			
		||||
    this.metaService.updateTag({ property: 'og:loading', content: 'loading'});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // signal to the unfurler that the page is ready for a screenshot
 | 
			
		||||
  setPreviewReady() {
 | 
			
		||||
    this.metaService.updateTag({ property: 'og:ready', content: 'ready'});
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ import { BlockAuditComponent } from '../components/block-audit/block-audit.compo
 | 
			
		||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
 | 
			
		||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
 | 
			
		||||
import { AddressComponent } from '../components/address/address.component';
 | 
			
		||||
import { AddressPreviewComponent } from '../components/address/address-preview.component';
 | 
			
		||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
 | 
			
		||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
 | 
			
		||||
import { FooterComponent } from '../components/footer/footer.component';
 | 
			
		||||
@ -124,6 +125,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
    AddressPreviewComponent,
 | 
			
		||||
    SearchFormComponent,
 | 
			
		||||
    TimeSpanComponent,
 | 
			
		||||
    AddressLabelsComponent,
 | 
			
		||||
@ -225,6 +227,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
			
		||||
    BlockOverviewTooltipComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
    AddressPreviewComponent,
 | 
			
		||||
    SearchFormComponent,
 | 
			
		||||
    TimeSpanComponent,
 | 
			
		||||
    AddressLabelsComponent,
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,11 @@ body {
 | 
			
		||||
  box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview-box {
 | 
			
		||||
  min-height: 512px;
 | 
			
		||||
  padding: 2rem 3rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767.98px) {
 | 
			
		||||
  .box {
 | 
			
		||||
    padding: 0.75rem;
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ class Server {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async clusterTask({ page, data: { url, action } }) {
 | 
			
		||||
    await page.goto(url, { waitUntil: "domcontentloaded" });
 | 
			
		||||
    await page.goto(url, { waitUntil: "networkidle0" });
 | 
			
		||||
    switch (action) {
 | 
			
		||||
      case 'screenshot': {
 | 
			
		||||
        await page.evaluate(async () => {
 | 
			
		||||
@ -73,11 +73,21 @@ class Server {
 | 
			
		||||
            }),
 | 
			
		||||
          ]);
 | 
			
		||||
        });
 | 
			
		||||
        const waitForReady = await page.$('meta[property="og:loading"]');
 | 
			
		||||
        const alreadyReady = await page.$('meta[property="og:ready"]');
 | 
			
		||||
        if (waitForReady != null && alreadyReady == null) {
 | 
			
		||||
          try {
 | 
			
		||||
            await page.waitForSelector('meta[property="og:ready]"', { timeout: 10000 });
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            // probably timed out
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return page.screenshot();
 | 
			
		||||
      } break;
 | 
			
		||||
      default: {
 | 
			
		||||
        try {
 | 
			
		||||
          await page.waitForSelector('meta[property="og:title"', { timeout: 5000 })
 | 
			
		||||
          await page.waitForSelector('meta[property="og:title"]', { timeout: 10000 })
 | 
			
		||||
          const tag = await page.$('meta[property="og:title"]');
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          // probably timed out
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user