diff --git a/src/app/endorsements/endorsements.module.ts b/src/app/endorsements/endorsements.module.ts index 88b83fb..30942f5 100644 --- a/src/app/endorsements/endorsements.module.ts +++ b/src/app/endorsements/endorsements.module.ts @@ -1,11 +1,12 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; import {RouterModule, Routes} from "@angular/router"; -import { MyEndorsementsComponent } from './pages/my-endorsements/my-endorsements.component'; -import { EndorseInstanceComponent } from './pages/endorse-instance/endorse-instance.component'; +import {MyEndorsementsComponent} from './pages/my-endorsements/my-endorsements.component'; +import {EndorseInstanceComponent} from './pages/endorse-instance/endorse-instance.component'; import {ReactiveFormsModule} from "@angular/forms"; import {Guards} from "../guards/guards"; import {SharedModule} from "../shared/shared.module"; +import {EditEndorsementReasonsComponent} from './pages/edit-endorsement-reasons/edit-endorsement-reasons.component'; const routes: Routes = [ { @@ -17,13 +18,19 @@ const routes: Routes = [ path: 'endorse', component: EndorseInstanceComponent, canActivate: [Guards.isLoggedIn()], - } + }, + { + path: 'my/edit/:instance', + component: EditEndorsementReasonsComponent, + canActivate: [Guards.isLoggedIn()], + }, ]; @NgModule({ declarations: [ MyEndorsementsComponent, - EndorseInstanceComponent + EndorseInstanceComponent, + EditEndorsementReasonsComponent ], imports: [ CommonModule, diff --git a/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.html b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.html new file mode 100644 index 0000000..22f1b76 --- /dev/null +++ b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.html @@ -0,0 +1,25 @@ + + + +
+
+
+

Endorsing means that you approve of an instance. For more information visit the Glossary.

+

You can do this only if your instance is guaranteed by some other instance.

+
+
+ + +
+
+ + +
+ +
+
+
+
+
diff --git a/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.scss b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.scss new file mode 100644 index 0000000..5f5d12d --- /dev/null +++ b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.scss @@ -0,0 +1,3 @@ +:host { + min-width: 100%; +} diff --git a/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.ts b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.ts new file mode 100644 index 0000000..befe8bd --- /dev/null +++ b/src/app/endorsements/pages/edit-endorsement-reasons/edit-endorsement-reasons.component.ts @@ -0,0 +1,104 @@ +import {Component, OnInit} from '@angular/core'; +import {FormControl, FormGroup, Validators} from "@angular/forms"; +import {TitleService} from "../../../services/title.service"; +import {MessageService} from "../../../services/message.service"; +import {FediseerApiService} from "../../../services/fediseer-api.service"; +import {ActivatedRoute, Router} from "@angular/router"; +import {AuthenticationManagerService} from "../../../services/authentication-manager.service"; +import {ApiResponseHelperService} from "../../../services/api-response-helper.service"; +import {toPromise} from "../../../types/resolvable"; +import {map} from "rxjs"; +import {NormalizedInstanceDetailResponse} from "../../../response/normalized-instance-detail.response"; + +@Component({ + selector: 'app-edit-endorsement-reasons', + templateUrl: './edit-endorsement-reasons.component.html', + styleUrls: ['./edit-endorsement-reasons.component.scss'] +}) +export class EditEndorsementReasonsComponent implements OnInit { + public form = new FormGroup({ + instance: new FormControl({value: '', disabled: true}, [Validators.required]), + reasons: new FormControl([]), + }); + public loading: boolean = true; + public availableReasons: string[] = []; + + constructor( + private readonly titleService: TitleService, + private readonly messageService: MessageService, + private readonly api: FediseerApiService, + private readonly router: Router, + private readonly activatedRoute: ActivatedRoute, + private readonly authManager: AuthenticationManagerService, + private readonly apiResponseHelper: ApiResponseHelperService, + ) { + } + + public async ngOnInit(): Promise { + this.titleService.title = 'Update endorsement reasons'; + + this.activatedRoute.params.subscribe(async params => { + const targetInstance = params['instance'] as string; + let availableReasons = await toPromise(this.api.usedEndorsementReasons); + if (availableReasons === null) { + this.messageService.createWarning(`Couldn't get list of reasons that were used previously, autocompletion won't work.`); + availableReasons = []; + } + this.availableReasons = availableReasons; + + const existing = await toPromise( + this.api.getEndorsementsByInstance([this.authManager.currentInstanceSnapshot.name]).pipe( + map(response => { + if (this.apiResponseHelper.handleErrors([response])) { + return null; + } + + const instance = response.successResponse!.instances.filter( + instance => instance.domain === targetInstance, + ); + if (!instance.length) { + this.messageService.createError(`Couldn't find this instance amongst your endorsements. Are you sure you've endorsed it?`); + return null; + } + + return instance[0]; + }), + ), + ); + + if (existing === null) { + this.loading = false; + return; + } + + this.form.patchValue({ + instance: existing.domain, + reasons: NormalizedInstanceDetailResponse.fromInstanceDetail(existing).unmergedEndorsementReasons, + }); + this.loading = false; + }); + } + + public async updateReasons(): Promise { + if (!this.form.valid) { + this.messageService.createError("The form is not valid, please make sure all fields are filled correctly."); + return; + } + + this.loading = true; + this.api.updateEndorsement( + this.form.controls.instance.value!, + this.form.controls.reasons.value ? this.form.controls.reasons.value!.join(',') : null, + ).subscribe(response => { + if (this.apiResponseHelper.handleErrors([response])) { + this.loading = false; + return; + } + + this.loading = false; + this.router.navigateByUrl('/endorsements/my').then(() => { + this.messageService.createSuccess(`${this.form.controls.instance.value} was successfully updated!`); + }); + }); + } +} diff --git a/src/app/endorsements/pages/endorse-instance/endorse-instance.component.html b/src/app/endorsements/pages/endorse-instance/endorse-instance.component.html index c7e3e59..636a641 100644 --- a/src/app/endorsements/pages/endorse-instance/endorse-instance.component.html +++ b/src/app/endorsements/pages/endorse-instance/endorse-instance.component.html @@ -11,6 +11,12 @@ +
+ + +
diff --git a/src/app/endorsements/pages/endorse-instance/endorse-instance.component.ts b/src/app/endorsements/pages/endorse-instance/endorse-instance.component.ts index cf42950..4cb4e61 100644 --- a/src/app/endorsements/pages/endorse-instance/endorse-instance.component.ts +++ b/src/app/endorsements/pages/endorse-instance/endorse-instance.component.ts @@ -4,6 +4,8 @@ import {FormControl, FormGroup, Validators} from "@angular/forms"; import {MessageService} from "../../../services/message.service"; import {FediseerApiService} from "../../../services/fediseer-api.service"; import {Router} from "@angular/router"; +import {ApiResponseHelperService} from "../../../services/api-response-helper.service"; +import {toPromise} from "../../../types/resolvable"; @Component({ selector: 'app-endorse-instance', @@ -13,8 +15,10 @@ import {Router} from "@angular/router"; export class EndorseInstanceComponent implements OnInit { public form = new FormGroup({ instance: new FormControl('', [Validators.required]), + reasons: new FormControl([]), }); - public loading: boolean = false; + public loading: boolean = true; + public availableReasons: string[] = []; constructor( private readonly titleService: TitleService, @@ -25,6 +29,15 @@ export class EndorseInstanceComponent implements OnInit { } public async ngOnInit(): Promise { this.titleService.title = 'Endorse an instance'; + + const reasons = await toPromise(this.api.usedEndorsementReasons); + if (reasons === null) { + this.messageService.createWarning('Getting list of reasons failed, there will not be any autocompletion.'); + } else { + this.availableReasons = reasons; + } + + this.loading = false; } public async doEndorse(): Promise { @@ -34,7 +47,10 @@ export class EndorseInstanceComponent implements OnInit { } this.loading = true; - this.api.endorseInstance(this.form.controls.instance.value!).subscribe(response => { + this.api.endorseInstance( + this.form.controls.instance.value!, + this.form.controls.reasons.value ? this.form.controls.reasons.value!.join(',') : null, + ).subscribe(response => { if (!response.success) { this.messageService.createError(`There was an api error: ${response.errorResponse!.message}`); this.loading = false; diff --git a/src/app/endorsements/pages/my-endorsements/my-endorsements.component.html b/src/app/endorsements/pages/my-endorsements/my-endorsements.component.html index 6daa998..1ce82b1 100644 --- a/src/app/endorsements/pages/my-endorsements/my-endorsements.component.html +++ b/src/app/endorsements/pages/my-endorsements/my-endorsements.component.html @@ -12,14 +12,21 @@

Endorsements that {{instance.name}} received {{ "Instance" }} + Reasons - This instance didn't receive any endorsements yet. + This instance didn't receive any endorsements yet. {{endorsed.domain}} + +
    +
  • {{reason}}
  • +
+ N/A + @@ -35,8 +42,8 @@

Endorsements that {{instance.name}} has given Instance - Endorsements - Cancel + Reasons + Actions @@ -51,8 +58,15 @@

Endorsements that {{instance.name}} has given {{endorsed.domain}} - {{endorsed.endorsements}} +
    +
  • {{reason}}
  • +
+ N/A + + + Edit +   diff --git a/src/app/endorsements/pages/my-endorsements/my-endorsements.component.ts b/src/app/endorsements/pages/my-endorsements/my-endorsements.component.ts index 238cf89..dfb8a09 100644 --- a/src/app/endorsements/pages/my-endorsements/my-endorsements.component.ts +++ b/src/app/endorsements/pages/my-endorsements/my-endorsements.component.ts @@ -8,6 +8,7 @@ import {Observable} from "rxjs"; import {Instance} from "../../../user/instance"; import {toObservable, toPromise} from "../../../types/resolvable"; import {ApiResponseHelperService} from "../../../services/api-response-helper.service"; +import {NormalizedInstanceDetailResponse} from "../../../response/normalized-instance-detail.response"; @Component({ selector: 'app-my-endorsements', @@ -15,8 +16,8 @@ import {ApiResponseHelperService} from "../../../services/api-response-helper.se styleUrls: ['./my-endorsements.component.scss'] }) export class MyEndorsementsComponent implements OnInit { - public endorsementsForMyInstance: InstanceDetailResponse[] = []; - public endorsementsByMyInstance: InstanceDetailResponse[] = []; + public endorsementsForMyInstance: NormalizedInstanceDetailResponse[] = []; + public endorsementsByMyInstance: NormalizedInstanceDetailResponse[] = []; public instance: Observable = this.authManager.currentInstance; public guaranteed: boolean = false; public loading: boolean = true; @@ -44,8 +45,12 @@ export class MyEndorsementsComponent implements OnInit { return; } - this.endorsementsForMyInstance = responses[0].successResponse!.instances; - this.endorsementsByMyInstance = responses[1].successResponse!.instances; + this.endorsementsForMyInstance = responses[0].successResponse!.instances.map( + instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance), + ); + this.endorsementsByMyInstance = responses[1].successResponse!.instances.map( + instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance), + ); this.guaranteed = responses[2].successResponse!.guarantor !== undefined; this.loading = false; } diff --git a/src/app/instances/pages/instance-detail/instance-detail.component.html b/src/app/instances/pages/instance-detail/instance-detail.component.html index 35f7538..f8087c5 100644 --- a/src/app/instances/pages/instance-detail/instance-detail.component.html +++ b/src/app/instances/pages/instance-detail/instance-detail.component.html @@ -173,11 +173,18 @@

Endorsements received ({{endor Instance + Reasons {{instance.domain}} + +
    +
  • {{reason}}
  • +
+ N/A + @@ -199,11 +206,18 @@

Endorsements given ({{endorsement Instance + Reasons {{instance.domain}} + +
    +
  • {{reason}}
  • +
+ N/A + diff --git a/src/app/instances/pages/instance-detail/instance-detail.component.ts b/src/app/instances/pages/instance-detail/instance-detail.component.ts index d1da5c9..0436411 100644 --- a/src/app/instances/pages/instance-detail/instance-detail.component.ts +++ b/src/app/instances/pages/instance-detail/instance-detail.component.ts @@ -19,8 +19,8 @@ export class InstanceDetailComponent implements OnInit { public censuresGiven: NormalizedInstanceDetailResponse[] | null = null; public hesitationsReceived: NormalizedInstanceDetailResponse[] | null = null; public hesitationsGiven: NormalizedInstanceDetailResponse[] | null = null; - public endorsementsReceived: InstanceDetailResponse[] | null = null; - public endorsementsGiven: InstanceDetailResponse[] | null = null; + public endorsementsReceived: NormalizedInstanceDetailResponse[] | null = null; + public endorsementsGiven: NormalizedInstanceDetailResponse[] | null = null; public guaranteesGiven: InstanceDetailResponse[] | null = null; public detail: InstanceDetailResponse | null = null; @@ -70,8 +70,12 @@ export class InstanceDetailComponent implements OnInit { this.censuresGiven = responses[1].successResponse?.instances.map( instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance), ) ?? null; - this.endorsementsReceived = responses[2].successResponse?.instances ?? null; - this.endorsementsGiven = responses[3].successResponse?.instances ?? null; + this.endorsementsReceived = responses[2].successResponse?.instances.map( + instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance), + ) ?? null; + this.endorsementsGiven = responses[3].successResponse?.instances.map( + instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance), + ) ?? null; this.guaranteesGiven = responses[4].successResponse?.instances ?? null; this.detail = responses[5].successResponse ?? null; this.myInstance = !this.authManager.currentInstanceSnapshot.anonymous && this.detail?.domain === this.authManager.currentInstanceSnapshot.name; diff --git a/src/app/response/instance-detail.response.ts b/src/app/response/instance-detail.response.ts index 13074fc..e367657 100644 --- a/src/app/response/instance-detail.response.ts +++ b/src/app/response/instance-detail.response.ts @@ -16,4 +16,5 @@ export interface InstanceDetailResponse { censure_evidence?: string[]; hesitation_reasons?: string[]; hesitation_evidence?: string[]; + endorsement_reasons?: string[] | null; } diff --git a/src/app/response/normalized-instance-detail.response.ts b/src/app/response/normalized-instance-detail.response.ts index 9eac014..2fd998d 100644 --- a/src/app/response/normalized-instance-detail.response.ts +++ b/src/app/response/normalized-instance-detail.response.ts @@ -17,6 +17,10 @@ export class NormalizedInstanceDetailResponse { public hesitationReasons: string[], public unmergedHesitationReasons: string[], public hesitationsEvidence: string, + public endorsementReasons: string[], + public unmergedEndorsementReasons: string[], + public sysadmins: int | null, + public moderators: int | null, public guarantor?: string | null, ) { } @@ -26,6 +30,8 @@ export class NormalizedInstanceDetailResponse { let unmergedCensureReasons: string[] = []; let hesitationReasons: string[] = []; let unmergedHesitationReasons: string[] = [] + let endorsementReasons: string[] = []; + let unmergedEndorsementReasons: string[] = []; if (detail.censure_reasons) { [censureReasons, unmergedCensureReasons] = this.getReasons(detail.censure_reasons); @@ -33,6 +39,9 @@ export class NormalizedInstanceDetailResponse { if (detail.hesitation_reasons) { [hesitationReasons, unmergedHesitationReasons] = this.getReasons(detail.hesitation_reasons); } + if (detail.endorsement_reasons) { + [endorsementReasons, unmergedEndorsementReasons] = this.getReasons(detail.endorsement_reasons); + } return new NormalizedInstanceDetailResponse( detail.id, @@ -49,6 +58,10 @@ export class NormalizedInstanceDetailResponse { hesitationReasons, unmergedHesitationReasons, detail.hesitation_evidence?.join(', ') ?? '', + endorsementReasons, + unmergedEndorsementReasons, + detail.sysadmins, + detail.moderators, detail.guarantor, ); } diff --git a/src/app/services/fediseer-api.service.ts b/src/app/services/fediseer-api.service.ts index 9a91571..253d90d 100644 --- a/src/app/services/fediseer-api.service.ts +++ b/src/app/services/fediseer-api.service.ts @@ -75,8 +75,20 @@ export class FediseerApiService { return this.sendRequest(HttpMethod.Get, `approvals/${instanceString}`); } - public endorseInstance(instance: string): Observable> { - return this.sendRequest(HttpMethod.Put, `endorsements/${instance}`); + public endorseInstance(instance: string, reason: string | null = null): Observable> { + const body: {reason?: string} = {}; + if (reason) { + body.reason = reason; + } + return this.sendRequest(HttpMethod.Put, `endorsements/${instance}`, body); + } + + public updateEndorsement(instance: string, reason: string | null): Observable> { + const body: {[key: string]: string} = {}; + if (reason) { + body['reason'] = reason; + } + return this.sendRequest(HttpMethod.Patch, `endorsements/${instance}`, body); } public cancelEndorsement(instance: string): Observable> { @@ -219,6 +231,39 @@ export class FediseerApiService { ); } + public get usedEndorsementReasons(): Observable { + const cacheItem = this.runtimeCache.getItem(`used_endorsement_reasons`); + if (cacheItem.isHit) { + return of(cacheItem.value!); + } + + return this.getEndorsementsByInstance([this.authManager.currentInstanceSnapshot.name]).pipe( + map (response => { + if (!response.success) { + return null; + } + + const instances = response.successResponse!.instances + .map(instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance)); + + let reasons: string[] = []; + + for (const instance of instances) { + reasons = [...reasons, ...instance.unmergedEndorsementReasons]; + } + + return [...new Set(reasons)]; + }), + tap (reasons => { + if (reasons === null) { + return; + } + cacheItem.value = reasons; + this.runtimeCache.save(cacheItem); + }) + ); + } + public updateInstanceData(instance: string, data: EditableInstanceData): Observable> { const body = {...data}; if (body.sysadmins === null) {