feat: Address chunking issue #6289

This commit is contained in:
rodribp 2026-02-14 18:10:40 -06:00
parent 16907fc207
commit 29fcf23dd3
No known key found for this signature in database
GPG key ID: CC349739DA92EE20
10 changed files with 209 additions and 9 deletions

View file

@ -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>

View file

@ -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();
}
}
}

View 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';
}
}

View file

@ -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>
}

View file

@ -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();
}
}
}

View file

@ -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) {

View file

@ -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>

View file

@ -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();
}
}
}

View 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);
}
}

View file

@ -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 {