Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connection manager replacement #573

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { SetHeadersInterceptor } from './interceptors/set-headers.interceptor';
import { BalanceWithSymbolComponent } from './components/balance-with-symbol/balance-with-symbol.component';
import { AddressIdenticonComponent } from './components/address-identicon/address-identicon.component';
import { PaymentIdentifierInputComponent } from './components/payment-identifier-input/payment-identifier-input.component';
import { QuickConnectDialogComponent } from './components/quick-connect-dialog/quick-connect-dialog.component';

const appRoutes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
Expand Down Expand Up @@ -120,6 +121,7 @@ export function ConfigLoader(raidenConfig: RaidenConfig) {
BalanceWithSymbolComponent,
AddressIdenticonComponent,
PaymentIdentifierInputComponent,
QuickConnectDialogComponent,
],
imports: [
RouterModule.forRoot(appRoutes),
Expand All @@ -144,7 +146,7 @@ export function ConfigLoader(raidenConfig: RaidenConfig) {
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorHandlingInterceptor,
deps: [NotificationService, RaidenService],
deps: [NotificationService, RaidenService, RaidenConfig],
multi: true,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
min-width: 0;

&:disabled {
color: $text-grey;
color: $dark-grey;
background: none;
border: 1px solid $dark-grey;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<app-raiden-dialog
titleText="Quick Connect"
acceptText="Connect"
[acceptDisabled]="form.invalid"
[formGroup]="form"
(accept)="accept()"
(cancel)="cancel()"
>
<app-token-network-selector
*ngIf="initiatedWithoutToken"
formControlName="token"
[showOnChainBalance]="true"
>
</app-token-network-selector>
<app-token-input
placeholder="Total Deposit"
infoText="You will get suggested partners to connect to. Quick Connect will open channels with these suggestions. The total deposit is split among the channels. You can modify the deposit for each partner. By setting a deposit to 0 no channel will be created."
formControlName="totalAmount"
[onChainInput]="true"
>
</app-token-input>
<div
*ngIf="loading || pfsError || suggestions.length > 0"
class="content"
[@stretchVertically]="'in'"
>
<div *ngIf="!loading && !pfsError; else spinner">
Top suggestions: {{ suggestions }}
</div>
<ng-template #spinner>
<div class="content__full" fxLayoutAlign="center center">
<mat-progress-spinner
*ngIf="loading; else error"
diameter="60"
mode="indeterminate"
color="primary"
></mat-progress-spinner>
</div>
</ng-template>
<ng-template #error>
<div class="content__full error" fxLayoutAlign="center center">
Could not fetch any suggestions
</div>
</ng-template>
</div>
</app-raiden-dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import '../../../sass/colors';

.content {
height: 182px;

&__full {
width: 100%;
height: 100%;
}
}

.error {
font-size: 13px;
line-height: 15px;
letter-spacing: 0.35px;
color: $red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { QuickConnectDialogComponent } from './quick-connect-dialog.component';

describe('QuickConnectDialogComponent', () => {
let component: QuickConnectDialogComponent;
let fixture: ComponentFixture<QuickConnectDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [QuickConnectDialogComponent],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(QuickConnectDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Animations } from 'app/animations/animations';
import { ConnectionChoice, SuggestedConnection } from 'app/models/connection';
import { UserToken } from 'app/models/usertoken';
import { ChannelPollingService } from 'app/services/channel-polling.service';
import { RaidenService } from 'app/services/raiden.service';
import { TokenPollingService } from 'app/services/token-polling.service';
import { combineLatest, Observable, Subject, zip } from 'rxjs';
import {
catchError,
filter,
finalize,
first,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators';
import { TokenInputComponent } from '../token-input/token-input.component';

export interface QuickConnectDialogPayload {
readonly token: UserToken;
}

export interface QuickConnectDialogResult {
readonly token: UserToken;
readonly connectionChoices: ConnectionChoice[];
}

@Component({
selector: 'app-quick-connect-dialog',
templateUrl: './quick-connect-dialog.component.html',
styleUrls: ['./quick-connect-dialog.component.scss'],
animations: Animations.stretchInOut,
})
export class QuickConnectDialogComponent implements OnInit, OnDestroy {
@ViewChild(TokenInputComponent, { static: true })
private tokenInput: TokenInputComponent;

form: FormGroup;
initiatedWithoutToken = false;
suggestions: SuggestedConnection[] = [];
loading = false;
pfsError = false;

private ngUnsubscribe = new Subject();

constructor(
@Inject(MAT_DIALOG_DATA) data: QuickConnectDialogPayload,
private dialogRef: MatDialogRef<QuickConnectDialogComponent>,
private fb: FormBuilder,
private tokenPollingService: TokenPollingService,
private raidenService: RaidenService,
private http: HttpClient,
private channelPollingService: ChannelPollingService
) {
this.initiatedWithoutToken = !data.token;
this.form = this.fb.group({
token: [data.token, Validators.required],
totalAmount: ['', Validators.required],
});
}

ngOnInit(): void {
this.subscribeToSuggestions();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: private methods with internal, explicit subscriptions aren't usually the recommended way to do that in Angular. Maybe you could just initialize private cold observables, possibly shared, and then use | async on template, which does handle subscriptions automatically. Declarative is usually better than imperative.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes totally sense! I like it your way better. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't make it because there were too many changes needed in the tests. Not worth the effort.

this.subscribeToTokenUpdates();
this.form.controls.token.updateValueAndValidity();
}

ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}

accept() {
const payload: QuickConnectDialogResult = {
token: this.form.value.token,
connectionChoices: undefined, // TODO
};
this.dialogRef.close(payload);
}

cancel() {
this.dialogRef.close();
}

private subscribeToSuggestions() {
const pathfindingServiceUrl$ = this.raidenService
.getSettings()
.pipe(map((settings) => settings.pathfinding_service_address));
const tokenValueChange$: Observable<UserToken> = this.form.controls
.token.valueChanges;
const tokenNetworkAddress$ = tokenValueChange$.pipe(
filter((token) => !!token),
tap(() => {
this.loading = true;
this.resetError();
}),
switchMap((token) =>
this.raidenService.getTokenNetworkAddress(token.address)
)
);

const suggestions$ = combineLatest([
pathfindingServiceUrl$,
tokenNetworkAddress$,
]).pipe(
switchMap(([pathfindingServiceUrl, tokenNetworkAddress]) =>
this.http.get<SuggestedConnection[]>(
`${pathfindingServiceUrl}/api/v1/${tokenNetworkAddress}/suggest_partner`
)
)
);
const allChannels$ = this.channelPollingService.channels$.pipe(first());
const existingChannels$ = combineLatest([
tokenValueChange$,
allChannels$,
]).pipe(
map(([token, channels]) =>
channels.filter(
(channel) =>
channel.state !== 'settled' &&
channel.token_address === token.address
)
)
);

zip(suggestions$, existingChannels$)
.pipe(
map(([suggestions, channels]) =>
suggestions.filter(
(suggestion) =>
!channels.find(
(channel) =>
channel.partner_address ===
suggestion.address
)
)
),
catchError((error, caught) => {
this.showError();
return caught;
}),
tap(() => (this.loading = false)),
takeUntil(this.ngUnsubscribe)
)
.subscribe((suggestions) => {
if (suggestions.length === 0) {
this.showError();
}
this.suggestions = suggestions;
});
}

private subscribeToTokenUpdates() {
this.form.controls.token.valueChanges
.pipe(
tap((token) => (this.tokenInput.selectedToken = token)),
switchMap((token) =>
this.tokenPollingService.getTokenUpdates(
token?.address ?? ''
)
),
takeUntil(this.ngUnsubscribe)
)
.subscribe((updatedToken) => {
this.tokenInput.maxAmount = updatedToken?.balance;
});
}

private showError() {
this.pfsError = true;
this.form.controls.totalAmount.disable();
}

private resetError() {
this.pfsError = false;
this.form.controls.totalAmount.enable();
}
}
4 changes: 0 additions & 4 deletions src/app/components/raiden-dialog/raiden-dialog.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@

&--black {
color: $white;

&:disabled {
color: $dark-grey;
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/app/components/token-input/token-input.component.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="amount" fxLayout="row" fxLayoutAlign="start center">
<div
class="input"
[class.input--disabled]="disabled"
fxLayout="row"
fxLayoutAlign="center center"
fxFlex="0 1 {{ width }}px"
Expand All @@ -10,6 +11,7 @@
aria-label="Amount input"
class="input__field"
type="text"
[disabled]="disabled"
(input)="onChange()"
(focus)="onTouched()"
#input
Expand Down Expand Up @@ -94,7 +96,10 @@
</div>
</ng-template>
<ng-template #max_amount>
<div *ngIf="maxAmount && selectedToken" class="info-box__text">
<div
*ngIf="maxAmount && selectedToken && !disabled"
class="info-box__text"
>
<app-balance-with-symbol
[balance]="maxAmount"
[token]="selectedToken"
Expand All @@ -107,7 +112,7 @@
</div>

<div
*ngIf="maxAmount && selectedToken"
*ngIf="maxAmount && selectedToken && !disabled"
fxFlex="0 1 80px"
fxLayoutAlign="center"
>
Expand Down
6 changes: 6 additions & 0 deletions src/app/components/token-input/token-input.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
height: 32px;
color: $black;

&--disabled {
color: $dark-grey;
background: none;
border: 1px solid $dark-grey;
}

&__field {
width: 100%;
min-width: 0;
Expand Down
5 changes: 5 additions & 0 deletions src/app/components/token-input/token-input.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class TokenInputComponent implements ControlValueAccessor, Validator {
amount: BigNumber;
errors: ValidationErrors = { empty: true };
touched = false;
disabled = false;

private _selectedToken: UserToken;
private _maxAmount: BigNumber;
Expand Down Expand Up @@ -107,6 +108,10 @@ export class TokenInputComponent implements ControlValueAccessor, Validator {
return this.errors;
}

setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}

onChange() {
this.setAmount();
}
Expand Down
Loading