Merge pull request #4666 from mempool/mononaut/persistent-filters
Make dashboard filters persistent, add disjunctive filter mode
This commit is contained in:
		
						commit
						d4db628c3b
					
				| @ -1,4 +1,4 @@ | |||||||
| <div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200"> | <div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.any-mode]="filterMode === 'or'" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200"> | ||||||
|   <a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions"> |   <a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions"> | ||||||
|     <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span> |     <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span> | ||||||
|     <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> |     <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon> | ||||||
| @ -14,6 +14,15 @@ | |||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> |   <div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> | ||||||
|  |     <h5>Match</h5> | ||||||
|  |     <div class="btn-group btn-group-toggle"> | ||||||
|  |       <label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'"> | ||||||
|  |         <input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All | ||||||
|  |       </label> | ||||||
|  |       <label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'"> | ||||||
|  |         <input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any | ||||||
|  |       </label> | ||||||
|  |     </div> | ||||||
|     <ng-container *ngFor="let group of filterGroups;"> |     <ng-container *ngFor="let group of filterGroups;"> | ||||||
|       <h5>{{ group.label }}</h5> |       <h5>{{ group.label }}</h5> | ||||||
|       <div class="filter-group"> |       <div class="filter-group"> | ||||||
|  | |||||||
| @ -77,6 +77,49 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   &.any-mode { | ||||||
|  |     .filter-tag { | ||||||
|  |       border: solid 1px #1a9436; | ||||||
|  |       &.selected { | ||||||
|  |         background-color: #1a9436; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .btn-group { | ||||||
|  |     font-size: 0.9em; | ||||||
|  |     margin-right: 0.25em; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .mode-toggle { | ||||||
|  |     padding: 0.2em 0.5em; | ||||||
|  |     pointer-events: all; | ||||||
|  |     line-height: 1.5; | ||||||
|  |     background: #181b2daf; | ||||||
|  | 
 | ||||||
|  |     &:first-child { | ||||||
|  |       border-top-left-radius: 0.2rem; | ||||||
|  |       border-bottom-left-radius: 0.2rem; | ||||||
|  |     } | ||||||
|  |     &:last-child { | ||||||
|  |       border-top-right-radius: 0.2rem; | ||||||
|  |       border-bottom-right-radius: 0.2rem; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &.blue { | ||||||
|  |       border: solid 1px #105fb0; | ||||||
|  |       &.active { | ||||||
|  |         background: #105fb0; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     &.green { | ||||||
|  |       border: solid 1px #1a9436; | ||||||
|  |       &.active { | ||||||
|  |         background: #1a9436; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   :host-context(.block-overview-graph:hover) &, &:hover, &:active { |   :host-context(.block-overview-graph:hover) &, &:hover, &:active { | ||||||
|     .menu-toggle { |     .menu-toggle { | ||||||
|       opacity: 0.5; |       opacity: 0.5; | ||||||
| @ -132,6 +175,11 @@ | |||||||
|     .filter-tag { |     .filter-tag { | ||||||
|       font-size: 0.7em; |       font-size: 0.7em; | ||||||
|     } |     } | ||||||
|  |     .mode-toggle { | ||||||
|  |       font-size: 0.7em; | ||||||
|  |       margin-bottom: 5px; | ||||||
|  |       margin-top: 2px; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &.tiny { |   &.tiny { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; | import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; | ||||||
| import { FilterGroups, TransactionFilters } from '../../shared/filters.utils'; | import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| @ -12,7 +12,7 @@ import { Subscription } from 'rxjs'; | |||||||
| export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | ||||||
|   @Input() cssWidth: number = 800; |   @Input() cssWidth: number = 800; | ||||||
|   @Input() excludeFilters: string[] = []; |   @Input() excludeFilters: string[] = []; | ||||||
|   @Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter(); |   @Output() onFilterChanged: EventEmitter<ActiveFilter | null> = new EventEmitter(); | ||||||
| 
 | 
 | ||||||
|   filterSubscription: Subscription; |   filterSubscription: Subscription; | ||||||
| 
 | 
 | ||||||
| @ -21,6 +21,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|   disabledFilters: { [key: string]: boolean } = {}; |   disabledFilters: { [key: string]: boolean } = {}; | ||||||
|   activeFilters: string[] = []; |   activeFilters: string[] = []; | ||||||
|   filterFlags: { [key: string]: boolean } = {}; |   filterFlags: { [key: string]: boolean } = {}; | ||||||
|  |   filterMode: FilterMode = 'and'; | ||||||
|   menuOpen: boolean = false; |   menuOpen: boolean = false; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
| @ -29,15 +30,16 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.filterSubscription = this.stateService.activeGoggles$.subscribe((activeFilters: string[]) => { |     this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { | ||||||
|  |       this.filterMode = active.mode; | ||||||
|       for (const key of Object.keys(this.filterFlags)) { |       for (const key of Object.keys(this.filterFlags)) { | ||||||
|         this.filterFlags[key] = false; |         this.filterFlags[key] = false; | ||||||
|       } |       } | ||||||
|       for (const key of activeFilters) { |       for (const key of active.filters) { | ||||||
|         this.filterFlags[key] = !this.disabledFilters[key]; |         this.filterFlags[key] = !this.disabledFilters[key]; | ||||||
|       } |       } | ||||||
|       this.activeFilters = [...activeFilters.filter(key => !this.disabledFilters[key])]; |       this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])]; | ||||||
|       this.onFilterChanged.emit(this.getBooleanFlags()); |       this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -53,6 +55,12 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   setFilterMode(mode): void { | ||||||
|  |     this.filterMode = mode; | ||||||
|  |     this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); | ||||||
|  |     this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   toggleFilter(key): void { |   toggleFilter(key): void { | ||||||
|     const filter = this.filters[key]; |     const filter = this.filters[key]; | ||||||
|     this.filterFlags[key] = !this.filterFlags[key]; |     this.filterFlags[key] = !this.filterFlags[key]; | ||||||
| @ -73,8 +81,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|       this.activeFilters = this.activeFilters.filter(f => f != key); |       this.activeFilters = this.activeFilters.filter(f => f != key); | ||||||
|     } |     } | ||||||
|     const booleanFlags = this.getBooleanFlags(); |     const booleanFlags = this.getBooleanFlags(); | ||||||
|     this.onFilterChanged.emit(booleanFlags); |     this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); | ||||||
|     this.stateService.activeGoggles$.next([...this.activeFilters]); |     this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   getBooleanFlags(): bigint | null { |   getBooleanFlags(): bigint | null { | ||||||
| @ -90,7 +98,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { | |||||||
|   @HostListener('document:click', ['$event']) |   @HostListener('document:click', ['$event']) | ||||||
|   onClick(event): boolean { |   onClick(event): boolean { | ||||||
|     // click away from menu
 |     // click away from menu
 | ||||||
|     if (!event.target.closest('button')) { |     if (!event.target.closest('button') && !event.target.closest('label')) { | ||||||
|       this.menuOpen = false; |       this.menuOpen = false; | ||||||
|     } |     } | ||||||
|     return true; |     return true; | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import { Price } from '../../services/price.service'; | |||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; | import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; | ||||||
|  | import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; | ||||||
| 
 | 
 | ||||||
| const unmatchedOpacity = 0.2; | const unmatchedOpacity = 0.2; | ||||||
| const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); | const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); | ||||||
| @ -42,7 +43,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|   @Input() showFilters: boolean = false; |   @Input() showFilters: boolean = false; | ||||||
|   @Input() excludeFilters: string[] = []; |   @Input() excludeFilters: string[] = []; | ||||||
|   @Input() filterFlags: bigint | null = null; |   @Input() filterFlags: bigint | null = null; | ||||||
|   @Input() filterMode: 'and' | 'or' = 'and'; |   @Input() filterMode: FilterMode = 'and'; | ||||||
|   @Input() blockConversion: Price; |   @Input() blockConversion: Price; | ||||||
|   @Input() overrideColors: ((tx: TxView) => Color) | null = null; |   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||||
|   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); |   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); | ||||||
| @ -119,10 +120,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setFilterFlags(flags?: bigint | null): void { |   setFilterFlags(goggle?: ActiveFilter): void { | ||||||
|     this.activeFilterFlags = this.filterFlags || flags || null; |     this.filterMode = goggle?.mode || this.filterMode; | ||||||
|  |     this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags; | ||||||
|     if (this.scene) { |     if (this.scene) { | ||||||
|       if (this.activeFilterFlags != null) { |       if (this.activeFilterFlags != null && this.filtersAvailable) { | ||||||
|         this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); |         this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); | ||||||
|       } else { |       } else { | ||||||
|         this.scene.setColorFunction(this.overrideColors); |         this.scene.setColorFunction(this.overrideColors); | ||||||
| @ -157,7 +159,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
| 
 | 
 | ||||||
|   // initialize the scene without any entry transition
 |   // initialize the scene without any entry transition
 | ||||||
|   setup(transactions: TransactionStripped[]): void { |   setup(transactions: TransactionStripped[]): void { | ||||||
|     this.filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); |     const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); | ||||||
|  |     if (filtersAvailable !== this.filtersAvailable) { | ||||||
|  |       this.setFilterFlags(); | ||||||
|  |     } | ||||||
|  |     this.filtersAvailable = filtersAvailable; | ||||||
|     if (this.scene) { |     if (this.scene) { | ||||||
|       this.scene.setup(transactions); |       this.scene.setup(transactions); | ||||||
|       this.readyNextFrame = true; |       this.readyNextFrame = true; | ||||||
| @ -523,8 +529,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { |   getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { | ||||||
|  |     console.log('getting filter color function: ', flags, this.filterMode); | ||||||
|     return (tx: TxView) => { |     return (tx: TxView) => { | ||||||
|       if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (tx.bigintFlags & flags) > 0n)) { |       if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { | ||||||
|         return defaultColorFunction(tx); |         return defaultColorFunction(tx); | ||||||
|       } else { |       } else { | ||||||
|         return defaultColorFunction( |         return defaultColorFunction( | ||||||
|  | |||||||
| @ -10,6 +10,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi | |||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
| import { Color } from '../block-overview-graph/sprite-types'; | import { Color } from '../block-overview-graph/sprite-types'; | ||||||
| import TxView from '../block-overview-graph/tx-view'; | import TxView from '../block-overview-graph/tx-view'; | ||||||
|  | import { FilterMode } from '../../shared/filters.utils'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-mempool-block-overview', |   selector: 'app-mempool-block-overview', | ||||||
| @ -22,7 +23,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang | |||||||
|   @Input() showFilters: boolean = false; |   @Input() showFilters: boolean = false; | ||||||
|   @Input() overrideColors: ((tx: TxView) => Color) | null = null; |   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||||
|   @Input() filterFlags: bigint | undefined = undefined; |   @Input() filterFlags: bigint | undefined = undefined; | ||||||
|   @Input() filterMode: 'and' | 'or' = 'and'; |   @Input() filterMode: FilterMode = 'and'; | ||||||
|   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); |   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); | ||||||
| 
 | 
 | ||||||
|   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; |   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ | |||||||
|             <div class="quick-filter"> |             <div class="quick-filter"> | ||||||
|               <div class="btn-group btn-group-toggle"> |               <div class="btn-group btn-group-toggle"> | ||||||
|                 <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle"> |                 <label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex"  *ngFor="let filter of goggleCycle"> | ||||||
|                   <input type="radio" [value]="'3m'" fragment="3m" (click)="goggleIndex = filter.index" [attr.data-cy]="'3m'"> {{ filter.name }} |                   <input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }} | ||||||
|                 </label> |                 </label> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
| @ -34,8 +34,8 @@ | |||||||
|               <app-mempool-block-overview |               <app-mempool-block-overview | ||||||
|                 [index]="0" |                 [index]="0" | ||||||
|                 [resolution]="goggleResolution" |                 [resolution]="goggleResolution" | ||||||
|                 [filterFlags]="goggleCycle[goggleIndex].flag" |                 [filterFlags]="goggleFlags" | ||||||
|                 filterMode="or" |                 [filterMode]="goggleMode" | ||||||
|               ></app-mempool-block-overview> |               ></app-mempool-block-overview> | ||||||
|             </div> |             </div> | ||||||
|           </ng-template> |           </ng-template> | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ import { ApiService } from '../services/api.service'; | |||||||
| import { StateService } from '../services/state.service'; | import { StateService } from '../services/state.service'; | ||||||
| import { WebsocketService } from '../services/websocket.service'; | import { WebsocketService } from '../services/websocket.service'; | ||||||
| import { SeoService } from '../services/seo.service'; | import { SeoService } from '../services/seo.service'; | ||||||
|  | import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils'; | ||||||
| 
 | 
 | ||||||
| interface MempoolBlocksData { | interface MempoolBlocksData { | ||||||
|   blocks: number; |   blocks: number; | ||||||
| @ -55,6 +56,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|   currentReserves$: Observable<CurrentPegs>; |   currentReserves$: Observable<CurrentPegs>; | ||||||
|   fullHistory$: Observable<any>; |   fullHistory$: Observable<any>; | ||||||
|   isLoad: boolean = true; |   isLoad: boolean = true; | ||||||
|  |   filterSubscription: Subscription; | ||||||
|   mempoolInfoSubscription: Subscription; |   mempoolInfoSubscription: Subscription; | ||||||
|   currencySubscription: Subscription; |   currencySubscription: Subscription; | ||||||
|   currency: string; |   currency: string; | ||||||
| @ -65,13 +67,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|   private lastReservesBlockUpdate: number = 0; |   private lastReservesBlockUpdate: number = 0; | ||||||
| 
 | 
 | ||||||
|   goggleResolution = 82; |   goggleResolution = 82; | ||||||
|   goggleCycle = [ |   goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [ | ||||||
|     { index: 0, name: 'All' }, |     { index: 0, name: 'All', mode: 'and', filters: [] }, | ||||||
|     { index: 1, name: 'Consolidations', flag: 0b00000010_00000000_00000000_00000000_00000000n }, |     { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] }, | ||||||
|     { index: 2, name: 'Coinjoin', flag: 0b00000001_00000000_00000000_00000000_00000000n }, |     { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] }, | ||||||
|     { index: 3, name: '💩', flag: 0b00000100_00000000_00000000_00000000n | 0b00000010_00000000_00000000_00000000n | 0b00000001_00000000_00000000_00000000n }, |     { index: 3, name: '💩', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] }, | ||||||
|   ]; |   ]; | ||||||
|   goggleIndex = 0; // Math.floor(Math.random() * this.goggleCycle.length);
 |   goggleFlags = 0n; | ||||||
|  |   goggleMode: FilterMode = 'and'; | ||||||
|  |   goggleIndex = 0; | ||||||
| 
 | 
 | ||||||
|   private destroy$ = new Subject(); |   private destroy$ = new Subject(); | ||||||
| 
 | 
 | ||||||
| @ -87,6 +91,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
|  |     this.filterSubscription.unsubscribe(); | ||||||
|     this.mempoolInfoSubscription.unsubscribe(); |     this.mempoolInfoSubscription.unsubscribe(); | ||||||
|     this.currencySubscription.unsubscribe(); |     this.currencySubscription.unsubscribe(); | ||||||
|     this.websocketService.stopTrackRbfSummary(); |     this.websocketService.stopTrackRbfSummary(); | ||||||
| @ -107,6 +112,30 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|         map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) |         map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100) | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|  |     this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { | ||||||
|  |       const activeFilters = active.filters.sort().join(','); | ||||||
|  |       for (const goggle of this.goggleCycle) { | ||||||
|  |         if (goggle.mode === active.mode) { | ||||||
|  |           const goggleFilters = goggle.filters.sort().join(','); | ||||||
|  |           if (goggleFilters === activeFilters) { | ||||||
|  |             this.goggleIndex = goggle.index; | ||||||
|  |             this.goggleFlags = toFlags(goggle.filters); | ||||||
|  |             this.goggleMode = goggle.mode; | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       this.goggleCycle.push({ | ||||||
|  |         index: this.goggleCycle.length, | ||||||
|  |         name: 'Custom', | ||||||
|  |         mode: active.mode, | ||||||
|  |         filters: active.filters, | ||||||
|  |       }); | ||||||
|  |       this.goggleIndex = this.goggleCycle.length - 1; | ||||||
|  |       this.goggleFlags = toFlags(active.filters); | ||||||
|  |       this.goggleMode = active.mode; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     this.mempoolInfoData$ = combineLatest([ |     this.mempoolInfoData$ = combineLatest([ | ||||||
|       this.stateService.mempoolInfo$, |       this.stateService.mempoolInfo$, | ||||||
|       this.stateService.vbytesPerSecond$ |       this.stateService.vbytesPerSecond$ | ||||||
| @ -375,6 +404,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { | |||||||
|     return Array.from({ length: num }, (_, i) => i + 1); |     return Array.from({ length: num }, (_, i) => i + 1); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   setFilter(index): void { | ||||||
|  |     const selected = this.goggleCycle[index]; | ||||||
|  |     this.stateService.activeGoggles$.next(selected); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @HostListener('window:resize', ['$event']) |   @HostListener('window:resize', ['$event']) | ||||||
|   onResize(): void { |   onResize(): void { | ||||||
|     if (window.innerWidth >= 992) { |     if (window.innerWidth >= 992) { | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import { filter, map, scan, shareReplay } from 'rxjs/operators'; | |||||||
| import { StorageService } from './storage.service'; | import { StorageService } from './storage.service'; | ||||||
| import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; | import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; | ||||||
| import { ApiService } from './api.service'; | import { ApiService } from './api.service'; | ||||||
|  | import { ActiveFilter } from '../shared/filters.utils'; | ||||||
| 
 | 
 | ||||||
| export interface MarkBlockState { | export interface MarkBlockState { | ||||||
|   blockHeight?: number; |   blockHeight?: number; | ||||||
| @ -150,7 +151,7 @@ export class StateService { | |||||||
|   searchFocus$: Subject<boolean> = new Subject<boolean>(); |   searchFocus$: Subject<boolean> = new Subject<boolean>(); | ||||||
|   menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false); |   menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false); | ||||||
| 
 | 
 | ||||||
|   activeGoggles$: BehaviorSubject<string[]> = new BehaviorSubject([]); |   activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] }); | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     @Inject(PLATFORM_ID) private platformId: any, |     @Inject(PLATFORM_ID) private platformId: any, | ||||||
|  | |||||||
| @ -7,6 +7,13 @@ export interface Filter { | |||||||
|   important?: boolean, |   important?: boolean, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export type FilterMode = 'and' | 'or'; | ||||||
|  | 
 | ||||||
|  | export interface ActiveFilter { | ||||||
|  |   mode: FilterMode, | ||||||
|  |   filters: string[], | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // binary flags for transaction classification
 | // binary flags for transaction classification
 | ||||||
| export const TransactionFlags = { | export const TransactionFlags = { | ||||||
|   // features
 |   // features
 | ||||||
| @ -43,6 +50,14 @@ export const TransactionFlags = { | |||||||
|   sighash_acp:    0b00010000_00000000_00000000_00000000_00000000_00000000n, |   sighash_acp:    0b00010000_00000000_00000000_00000000_00000000_00000000n, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export function toFlags(filters: string[]): bigint { | ||||||
|  |   let flag = 0n; | ||||||
|  |   for (const filter of filters) { | ||||||
|  |     flag |= TransactionFlags[filter]; | ||||||
|  |   } | ||||||
|  |   return flag; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const TransactionFilters: { [key: string]: Filter } = { | export const TransactionFilters: { [key: string]: Filter } = { | ||||||
|     /* features */ |     /* features */ | ||||||
|     rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, |     rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true }, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user