mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
feat: Address chunking issue #6289
This commit is contained in:
parent
16907fc207
commit
29fcf23dd3
10 changed files with 209 additions and 9 deletions
|
|
@ -0,0 +1,13 @@
|
|||
<div [formGroup]="form" class="text-small text-center">
|
||||
<select
|
||||
formControlName="mode"
|
||||
class="custom-select custom-select-sm form-control-secondary form-control mx-auto"
|
||||
style="width: 140px;"
|
||||
id="addressFormatSelector"
|
||||
>
|
||||
<option value="off" i18n="address-formatting.default">Default adresses</option>
|
||||
<option value="color" i18n="address-formatting.color">Colored</option>
|
||||
<option value="spacing" i18n="address-formatting.color-spacing">+ Spacing</option>
|
||||
<option value="copy" i18n="address-formatting.color-spacing-copy">+ Clipboard</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { AddressFormattingService, FormattingMode } from '@app/services/address-formatting.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-formatting-selector',
|
||||
templateUrl: './address-formatting-selector.component.html',
|
||||
styles: [],
|
||||
standalone: false,
|
||||
})
|
||||
export class AddressFormattingSelectorComponent implements OnInit, OnDestroy {
|
||||
form: FormGroup;
|
||||
private formattingStateSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private formattingService: AddressFormattingService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
mode: [this.formattingService.mode]
|
||||
});
|
||||
|
||||
this.formattingStateSubscription = this.form.get('mode').valueChanges.subscribe((mode: FormattingMode) => {
|
||||
this.formattingService.setMode(mode);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.formattingStateSubscription) {
|
||||
this.formattingStateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
frontend/src/app/services/address-formatting.service.ts
Normal file
46
frontend/src/app/services/address-formatting.service.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
export type FormattingMode = 'off' | 'color' | 'spacing' | 'copy';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AddressFormattingService {
|
||||
private readonly STORAGE_KEY = 'address-formatting-mode';
|
||||
|
||||
private modeSubject = new BehaviorSubject<FormattingMode>(this.getInitialState());
|
||||
mode$ = this.modeSubject.asObservable();
|
||||
|
||||
constructor(private storageService: StorageService) {}
|
||||
|
||||
get mode(): FormattingMode {
|
||||
return this.modeSubject.value;
|
||||
}
|
||||
|
||||
setMode(mode: FormattingMode) {
|
||||
this.modeSubject.next(mode);
|
||||
this.storageService.setValue(this.STORAGE_KEY, mode);
|
||||
}
|
||||
|
||||
get useColors(): boolean {
|
||||
return this.mode !== 'off';
|
||||
}
|
||||
|
||||
get useSpacing(): boolean {
|
||||
return this.mode === 'spacing' || this.mode === 'copy';
|
||||
}
|
||||
|
||||
get showCopyButton(): boolean {
|
||||
return this.mode === 'copy';
|
||||
}
|
||||
|
||||
private getInitialState(): FormattingMode {
|
||||
const saved = this.storageService.getValue(this.STORAGE_KEY);
|
||||
if (saved === 'color' || saved === 'spacing' || saved === 'copy') {
|
||||
return saved as FormattingMode;
|
||||
}
|
||||
return 'off';
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,10 @@
|
|||
</span>
|
||||
</div>
|
||||
} @else {
|
||||
<a class="address" [routerLink]="['/address/' | relativeUrl, address]" title="{{ address }}">
|
||||
<app-truncate [text]="address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<a class="address" [routerLink]="['/address/' | relativeUrl, address]" title="{{ address }}" [style.user-select]="formattingService.useSpacing ? 'none' : 'auto'" style="user-select: none; min-width: 0;">
|
||||
<app-truncate [text]="address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<app-clipboard *ngIf="formattingService.showCopyButton" [text]="address" size="normal"></app-clipboard>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||
import { AddressMatch, AddressTypeInfo } from '@app/shared/address-utils';
|
||||
import { AddressFormattingService } from '@app/services/address-formatting.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-text',
|
||||
|
|
@ -11,7 +13,7 @@ export class AddressTextComponent {
|
|||
@Input() address: string;
|
||||
@Input() info: AddressTypeInfo | null;
|
||||
@Input() similarity: { score: number, match: AddressMatch, group: number } | null;
|
||||
|
||||
private formattingStateSubscription: Subscription;
|
||||
min = Math.min;
|
||||
|
||||
groupColors: string[] = [
|
||||
|
|
@ -20,4 +22,21 @@ export class AddressTextComponent {
|
|||
'var(--info)',
|
||||
'white',
|
||||
];
|
||||
|
||||
constructor(
|
||||
public formattingService: AddressFormattingService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.formattingStateSubscription = this.formattingService.mode$.subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.formattingStateSubscription) {
|
||||
this.formattingStateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@
|
|||
<app-theme-selector></app-theme-selector>
|
||||
</div>
|
||||
}
|
||||
<div class="selector d-none" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'">
|
||||
<app-address-formatting-selector></app-address-formatting-selector>
|
||||
</div>
|
||||
<a *ngIf="stateService.isMempoolSpaceBuild" class="btn btn-purple sponsor d-none justify-content-center" [ngClass]="isServicesPage ? 'd-lg-flex' : 'd-md-flex'" [routerLink]="['/login']">
|
||||
<span *ngIf="user" i18n="shared.my-account" class="nowrap">My Account</span>
|
||||
<span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
|
||||
|
|
@ -49,6 +52,7 @@
|
|||
<div class="selector d-flex justify-content-center ml-auto mr-auto mt-0" [ngClass]="isServicesPage ? 'd-lg-none' : 'd-md-none'">
|
||||
<app-amount-selector class="add-margin"></app-amount-selector>
|
||||
<app-theme-selector class="add-margin"></app-theme-selector>
|
||||
<app-address-formatting-selector class="add-margin"></app-address-formatting-selector>
|
||||
</div>
|
||||
}
|
||||
@if (!enterpriseInfo?.footer_img) {
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@
|
|||
</span>
|
||||
|
||||
<ng-template #ltrTruncated>
|
||||
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
|
||||
<span class="first" [innerHTML]="text.slice(0,-lastChars) | addressChunk : 0 : text"></span>
|
||||
<span class="last-four" [innerHTML]="text.slice(-lastChars) | addressChunk : (text.length - lastChars) : text"></span>
|
||||
<div class="hidden-content">{{ text }}</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #rtlTruncated>
|
||||
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
|
||||
<span class="first" [innerHTML]="text.slice(lastChars) | addressChunk : lastChars : text"></span>
|
||||
<span class="last-four" [innerHTML]="text.slice(0, lastChars) | addressChunk : 0 : text"></span>
|
||||
<div class="hidden-content">{{ text }}</div>
|
||||
</ng-template>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||
import { AddressFormattingService } from '@app/services/address-formatting.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
@Component({
|
||||
selector: 'app-truncate',
|
||||
templateUrl: './truncate.component.html',
|
||||
|
|
@ -18,12 +19,27 @@ export class TruncateComponent {
|
|||
@Input() textAlign: 'start' | 'end' = 'start';
|
||||
@Input() disabled: boolean = false;
|
||||
rtl: boolean;
|
||||
private formattingStateSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
public formattingService: AddressFormattingService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.rtl = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.formattingStateSubscription = this.formattingService.mode$.subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.formattingStateSubscription) {
|
||||
this.formattingStateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
frontend/src/app/shared/pipes/address-chunk.pipe.ts
Normal file
53
frontend/src/app/shared/pipes/address-chunk.pipe.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { AddressFormattingService } from '@app/services/address-formatting.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'addressChunk',
|
||||
standalone: false,
|
||||
pure: false
|
||||
})
|
||||
export class AddressChunkPipe implements PipeTransform {
|
||||
|
||||
private colors = ['#05ddff', '#6c92f9'];
|
||||
|
||||
constructor(
|
||||
private sanitizer: DomSanitizer,
|
||||
private formattingService: AddressFormattingService
|
||||
) {}
|
||||
|
||||
transform(chunk: string | null | undefined, offset: number = 0, fullString?: string): SafeHtml {
|
||||
if (!this.formattingService.useColors) return chunk || '';
|
||||
if (!chunk) return '';
|
||||
|
||||
const context = fullString || chunk;
|
||||
const isPureHex = /^[0-9a-fA-F]{64,66}$/.test(context);
|
||||
|
||||
if (isPureHex) return chunk;
|
||||
|
||||
let html = '';
|
||||
let i = 0;
|
||||
|
||||
const useSpacing = this.formattingService.useSpacing;
|
||||
|
||||
while (i < chunk.length) {
|
||||
const realIndex = i + offset;
|
||||
const positionInBlock = realIndex % 4;
|
||||
const charsLeftInColorGroup = 4 - positionInBlock;
|
||||
const lengthToTake = Math.min(charsLeftInColorGroup, chunk.length - i);
|
||||
const segment = chunk.substr(i, lengthToTake);
|
||||
const colorIndex = Math.floor(realIndex / 4) % 2;
|
||||
const color = this.colors[colorIndex];
|
||||
|
||||
const isLastSegment = (i + lengthToTake) >= chunk.length;
|
||||
|
||||
const margin = (useSpacing && !isLastSegment) ? ' margin-right: 3px;' : '';
|
||||
const style = `color: ${color};${margin}`;
|
||||
|
||||
html += `<span style="${style}">${segment}</span>`;
|
||||
i += lengthToTake;
|
||||
}
|
||||
|
||||
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||
}
|
||||
}
|
||||
|
|
@ -133,6 +133,9 @@ import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-inv
|
|||
import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives';
|
||||
import { GithubLogin } from '@components/github-login.component/github-login.component';
|
||||
|
||||
import { AddressChunkPipe } from '@app/shared/pipes/address-chunk.pipe';
|
||||
import { AddressFormattingSelectorComponent } from '@components/address-formatting-selector/address-formatting-selector.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ClipboardComponent,
|
||||
|
|
@ -256,6 +259,8 @@ import { GithubLogin } from '@components/github-login.component/github-login.com
|
|||
TwitterLogin,
|
||||
GithubLogin,
|
||||
BitcoinInvoiceComponent,
|
||||
AddressChunkPipe,
|
||||
AddressFormattingSelectorComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -405,6 +410,9 @@ import { GithubLogin } from '@components/github-login.component/github-login.com
|
|||
|
||||
OnlyVsizeDirective,
|
||||
OnlyWeightDirective,
|
||||
|
||||
AddressChunkPipe,
|
||||
AddressFormattingSelectorComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue