Skip to content

Commit

Permalink
Feat: Add tags support (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage committed Oct 1, 2023
1 parent b2b6bbc commit aad2c2e
Show file tree
Hide file tree
Showing 15 changed files with 177 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,11 @@ export class GuaranteeInstanceComponent implements OnInit {
this.cachedApi.getGuaranteesByInstance(currentInstance, {clear: true}),
this.cachedApi.getCensuresByInstances([currentInstance], {clear: true}),
this.cachedApi.getHesitationsByInstances([currentInstance], {clear: true}),
this.cachedApi.getWhitelistedInstances({clear: true}),
]).subscribe(() => {
this.loading = false;
this.router.navigateByUrl('/guarantees/my').then(() => {
this.messageService.createSuccess(`${this.form.controls.instance.value} was successfully guaranteed!`);
this.cachedApi.getWhitelistedInstances({clear: true}).subscribe();
this.cachedApi.clearWhitelistCache();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class MyGuaranteesComponent implements OnInit {
this.instancesGuaranteedByMe = this.instancesGuaranteedByMe.filter(
guaranteedInstance => guaranteedInstance.domain !== instance,
);
this.cachedApi.getWhitelistedInstances({clear: true}).subscribe();
this.cachedApi.clearWhitelistCache();
this.cachedApi.getGuaranteesByInstance(this.instance.name).subscribe();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ <h3 class="card-title" id="instance-details">Instance details</h3>
<label for="inputModerators">Amount of moderators</label>
<input class="form-control" type="number" min="0" step="1" id="inputModerators" formControlName="moderators" />
</div>
<div class="form-group">
<label for="inputTags">Tags</label>
<app-tooltip text="Self-reported list of tags, can be anything you feel represents your instance." />
<select formControlName="tags" id="inputTags" tom-select multiple [create]="true" [maxItems]="null">
<option *ngFor="let option of availableTags" [value]="option">{{option}}</option>
</select>
</div>

<ng-template #visibilitySelect let-formControlName="formControlName" let-type="type">
<div class="form-group">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Component, OnInit} from '@angular/core';
import {TitleService} from "../../../services/title.service";
import {FediseerApiService} from "../../../services/fediseer-api.service";
import {ApiResponse, FediseerApiService} from "../../../services/fediseer-api.service";
import {toPromise} from "../../../types/resolvable";
import {ApiResponseHelperService} from "../../../services/api-response-helper.service";
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {InstanceDetailResponse} from "../../../response/instance-detail.response";
import {MessageService} from "../../../services/message.service";
import {ListVisibility} from "../../../types/list-visibility";
import {CachedFediseerApiService} from "../../../services/cached-fediseer-api.service";

@Component({
selector: 'app-edit-own-instance',
Expand All @@ -16,6 +17,8 @@ import {ListVisibility} from "../../../types/list-visibility";
export class EditOwnInstanceComponent implements OnInit {
protected readonly ListVisibility = ListVisibility;

private originalTags: string[] = [];

public loading: boolean = true;
public detail: InstanceDetailResponse | null = null;
public form = new FormGroup({
Expand All @@ -24,11 +27,14 @@ export class EditOwnInstanceComponent implements OnInit {
censuresVisibility: new FormControl<ListVisibility>(ListVisibility.Open, [Validators.required]),
hesitationsVisibility: new FormControl<ListVisibility>(ListVisibility.Open, [Validators.required]),
endorsementsVisibility: new FormControl<ListVisibility>(ListVisibility.Open, [Validators.required]),
tags: new FormControl<string[]>([]),
});
public availableTags: string[] = [];

constructor(
private readonly titleService: TitleService,
private readonly api: FediseerApiService,
private readonly cachedApi: CachedFediseerApiService,
private readonly apiResponseHelper: ApiResponseHelperService,
private readonly messageService: MessageService,
) {
Expand All @@ -49,23 +55,45 @@ export class EditOwnInstanceComponent implements OnInit {
censuresVisibility: this.detail.visibility_censures!,
endorsementsVisibility: this.detail.visibility_endorsements!,
hesitationsVisibility: this.detail.visibility_hesitations!,
tags: this.detail.tags,
});
this.titleService.title = `Editing ${this.detail.domain}`;
this.originalTags = this.detail.tags;
// this.availableTags = this.detail.tags;
this.availableTags = await toPromise(this.cachedApi.getAvailableTags());
this.loading = false;
}

public async updateInstance(): Promise<void> {
const response = await toPromise(this.api.updateInstanceData(this.detail!.domain, {
moderators: this.form.controls.moderators.value,
sysadmins: this.form.controls.sysadmins.value,
visibility_endorsements: this.form.controls.endorsementsVisibility.value!,
visibility_censures: this.form.controls.censuresVisibility.value!,
visibility_hesitations: this.form.controls.hesitationsVisibility.value!,
}));
if (this.apiResponseHelper.handleErrors([response])) {
const addedTags = this.form.controls.tags.value!.filter(tag => !this.originalTags.includes(tag));
const removedTags = this.originalTags.filter(tag => !this.form.controls.tags.value!.includes(tag));

const requests: Promise<ApiResponse<any>>[] = [
toPromise(this.api.updateInstanceData(this.detail!.domain, {
moderators: this.form.controls.moderators.value,
sysadmins: this.form.controls.sysadmins.value,
visibility_endorsements: this.form.controls.endorsementsVisibility.value!,
visibility_censures: this.form.controls.censuresVisibility.value!,
visibility_hesitations: this.form.controls.hesitationsVisibility.value!,
})),
];
if (addedTags.length) {
requests.push(toPromise(this.api.tagInstance(addedTags)));
}
if (removedTags.length) {
requests.push(toPromise(this.api.removeInstanceTags(removedTags)));
}

const responses = await Promise.all(requests);

if (this.apiResponseHelper.handleErrors(responses)) {
return;
}

this.cachedApi.clearWhitelistCache();
this.cachedApi.getAvailableTags({clear: true}).subscribe();
this.cachedApi.getCurrentInstanceInfo(null, {clear: true}).subscribe();

this.messageService.createSuccess('The data were successfully updated!')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ <h3 class="card-title" id="instance-details">Instance details</h3>
<app-instance-status [status]="detail.state" />
</td>
</tr>
<tr>
<td>
<strong>Tags</strong>
<app-tooltip text="Self-reported list of tags" />
</td>
<td>
<code *ngIf="!detail.tags.length">N/A</code>
<ng-container *ngIf="detail.tags.length">
<a routerLink="/instances/whitelisted" [queryParams]="{tags: tag}" class="btn btn-sm btn-outline-primary" *ngFor="let tag of detail.tags">
{{tag}}
</a>
</ng-container>
</td>
</tr>
<tr>
<td><strong>Open registrations?</strong></td>
<td>
Expand Down Expand Up @@ -145,14 +159,6 @@ <h3 class="card-title" id="instance-details">Instance details</h3>
<ng-container *ngIf="detail.visibility_hesitations === ListVisibility.Private">{{'app.list_visibility.private' | transloco}}</ng-container>
</td>
</tr>
<tr>
<td>
<strong>Instance status</strong>
</td>
<td>
<app-instance-status [status]="detail.state" />
</td>
</tr>
</tbody>
</table>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:host {
min-width: 100%;
}

.btn-sm {
margin-right: 5px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {NormalizedInstanceDetailResponse} from "../../../response/normalized-ins
import {CachedFediseerApiService} from "../../../services/cached-fediseer-api.service";
import {ListVisibility} from "../../../types/list-visibility";
import {InstanceMoveEvent} from "../../../shared/components/instance-move-to-list/instance-move-to-list.component";
import {InstanceFlag} from "../../../types/instance-flag";

@Component({
selector: 'app-instance-detail',
Expand Down Expand Up @@ -122,4 +123,6 @@ export class InstanceDetailComponent implements OnInit {
public async onInstanceMoved(event: InstanceMoveEvent) {

}

protected readonly InstanceFlag = InstanceFlag;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</th>
<th>Endorsements</th>
<th>Sysadmins / moderators</th>
<th>Tags</th>
<th>Instance status</th>
<th *ngIf="!currentInstance.anonymous">Endorsed by me?</th>
</tr>
Expand Down Expand Up @@ -50,6 +51,14 @@
<ng-container *ngIf="instance.moderators !== null">{{instance.moderators}}</ng-container>
</ng-template>
</td>
<td>
<code *ngIf="!instance.tags.length">N/A</code>
<ng-container *ngIf="instance.tags.length">
<a routerLink="/instances/whitelisted" [queryParams]="{tags: tag}" class="btn btn-sm btn-outline-primary" *ngFor="let tag of instance.tags">
{{tag}}
</a>
</ng-container>
</td>
<td>
<app-instance-status [status]="instance.state" />
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:host {
min-width: 100%;
}

.btn-sm {
margin-right: 5px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Instance} from "../../../user/instance";
import {SuccessResponse} from "../../../response/success.response";
import {ApiResponseHelperService} from "../../../services/api-response-helper.service";
import {CachedFediseerApiService} from "../../../services/cached-fediseer-api.service";
import {WhitelistFilter} from "../../../types/whitelist-filter";

@Component({
selector: 'app-whitelisted-instances',
Expand Down Expand Up @@ -44,38 +45,44 @@ export class WhitelistedInstancesComponent implements OnInit {
public async ngOnInit(): Promise<void> {
this.titleService.title = 'Whitelisted instances';

if (!this.currentInstance.anonymous) {
const response = await toPromise(this.cachedApi.getEndorsementsByInstances([this.currentInstance.name]));
this.activatedRoute.queryParams.subscribe(async queryParams => {
this.loading = true;
this.currentPage = queryParams['page'] ? Number(queryParams['page']) : 1;

const filters: WhitelistFilter = {};
if (queryParams['tags']) {
filters.tags = queryParams['tags'].split(',');
}

if (!this.currentInstance.anonymous) {
const response = await toPromise(this.cachedApi.getEndorsementsByInstances([this.currentInstance.name]));
if (this.apiResponseHelper.handleErrors([response])) {
this.loading = false;
return;
}
this.endorsedByMe = response.successResponse!.instances.map(instance => instance.domain);
}

const response = await toPromise(this.cachedApi.getWhitelistedInstances(filters));
if (this.apiResponseHelper.handleErrors([response])) {
this.loading = false;
return;
}
this.endorsedByMe = response.successResponse!.instances.map(instance => instance.domain);
}
this.allInstances = response.successResponse!.instances.sort((a, b) => {
if (a.endorsements === b.endorsements) {
return 0;
}

const response = await toPromise(this.cachedApi.getWhitelistedInstances());
if (this.apiResponseHelper.handleErrors([response])) {
this.loading = false;
return;
}
this.allInstances = response.successResponse!.instances.sort((a, b) => {
if (a.endorsements === b.endorsements) {
return 0;
return a.endorsements > b.endorsements ? -1 : 1;
});
this.titleService.title = `Whitelisted instances (${this.allInstances.length})`;
this.maxPage = Math.ceil(this.allInstances.length / this.perPage);
for (let i = 1; i <= this.maxPage; ++i) {
this.pages.push(i);
}

return a.endorsements > b.endorsements ? -1 : 1;
});
this.titleService.title = `Whitelisted instances (${this.allInstances.length})`;
this.maxPage = Math.ceil(this.allInstances.length / this.perPage);
for (let i = 1; i <= this.maxPage; ++i) {
this.pages.push(i);
}

this.loading = false;

this.activatedRoute.queryParams.subscribe(query => {
this.currentPage = query['page'] ? Number(query['page']) : 1;
this.instances = this.allInstances.slice((this.currentPage - 1) * this.perPage, this.currentPage * this.perPage);
this.loading = false;
});
}

Expand Down
1 change: 1 addition & 0 deletions src/app/response/instance-detail.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export interface InstanceDetailResponse {
visibility_hesitations?: ListVisibility;
state: InstanceStatus;
flags: {flag: InstanceFlag, comment: string}[];
tags: string[];
}
45 changes: 41 additions & 4 deletions src/app/services/cached-fediseer-api.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Injectable} from '@angular/core';
import {ApiResponse, FediseerApiService} from "./fediseer-api.service";
import {RuntimeCacheService} from "./cache/runtime-cache.service";
import {Observable, of, tap} from "rxjs";
import {map, Observable, of, tap} from "rxjs";
import {InstanceDetailResponse} from "../response/instance-detail.response";
import {int} from "../types/number";
import {PermanentCacheService} from "./cache/permanent-cache.service";
import {Cache, CacheItem} from "./cache/cache";
import {InstanceListResponse} from "../response/instance-list.response";
import {WhitelistFilter} from "../types/whitelist-filter";

export enum CacheType {
Runtime,
Expand Down Expand Up @@ -61,21 +62,26 @@ export class CachedFediseerApiService {
);
}

public getWhitelistedInstances(cacheConfig: CacheConfiguration = {}): Observable<ApiResponse<InstanceListResponse<InstanceDetailResponse>>> {
public getWhitelistedInstances(whitelistFilter: WhitelistFilter = {}, cacheConfig: CacheConfiguration = {}): Observable<ApiResponse<InstanceListResponse<InstanceDetailResponse>>> {
cacheConfig.type ??= CacheType.Permanent;
cacheConfig.ttl ??= 120;

const cacheKey = `api.whitelist${cacheConfig.ttl}`;
const cacheKey = `api.whitelist${cacheConfig.ttl}.${JSON.stringify(whitelistFilter)}`;
const item = this.getCacheItem<InstanceListResponse<InstanceDetailResponse>>(cacheKey, cacheConfig)!;
if (item.isHit && !cacheConfig.clear) {
return this.getSuccessResponse(item);
}

return this.api.getWhitelistedInstances().pipe(
return this.api.getWhitelistedInstances(whitelistFilter).pipe(
tap(this.storeResponse(item, cacheConfig)),
);
}

public clearWhitelistCache(): void {
this.runtimeCache.clearByPrefix('api.whitelist');
this.permanentCache.clearByPrefix('api.whitelist');
}

public getHesitationsByInstances(instances: string[], cacheConfig: CacheConfiguration = {}): Observable<ApiResponse<InstanceListResponse<InstanceDetailResponse>>> {
cacheConfig.type ??= CacheType.Permanent;
cacheConfig.ttl ??= 60;
Expand Down Expand Up @@ -174,6 +180,37 @@ export class CachedFediseerApiService {
);
}

public getAvailableTags(cacheConfig: CacheConfiguration = {}): Observable<string[]> {
cacheConfig.type ??= CacheType.Permanent;
cacheConfig.ttl ??= 300;

const cacheKey = `api.tags${cacheConfig.ttl}`;

const item = this.getCacheItem<string[] | null>(cacheKey, cacheConfig)!;

if (item.isHit && !cacheConfig.clear) {
return of(item.value!);
}

return this.getWhitelistedInstances()
.pipe(
map (response => {
if (!response.success) {
return [];
}

return response.successResponse!.instances.flatMap(instance => instance.tags);
}),
tap (result => {
item.value = result;
if (cacheConfig.ttl! >= 0) {
item.expiresAt = new Date(new Date().getTime() + (cacheConfig.ttl! * 1_000));
}
this.saveCacheItem(item, cacheConfig);
}),
)
}

public clearCache(): void {
this.runtimeCache.clear();
this.permanentCache.clear();
Expand Down
Loading

0 comments on commit aad2c2e

Please sign in to comment.