Skip to content

Commit

Permalink
#163 Add controller for archives
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski committed Dec 27, 2024
1 parent 66112e1 commit e9ece58
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 6 deletions.
7 changes: 7 additions & 0 deletions src/app/models/archive-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum ArchiveStatus {
New = 'new',
Processing = 'processing',
Ready = 'ready',
Expired = 'expired',
Error = 'error'
}
15 changes: 15 additions & 0 deletions src/app/models/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ArchiveStatus } from "./archive-status";
import { User } from "./user";

export class Archive {
public id = '';
public user?: User;
public requestDate?: Date;
public startDate?: Date;
public endDate?: Date;
public fileName?: string;
public status?: ArchiveStatus;
public errorMessage?: string;
public createdAt?: Date;
public updatedAt?: Date;
}
5 changes: 4 additions & 1 deletion src/app/models/event-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,8 @@ export enum EventType {
UserAliasesDelete = 'userAliasesDelete',

ActorRead = 'actorRead',
HealthRead = 'healthRead'
HealthRead = 'healthRead',

ArchivesList = 'archivesList',
ArchivesCreate = 'archivesCreate'
}
100 changes: 97 additions & 3 deletions src/app/pages/account/account.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ <h3>Fields</h3>
<!-- Alias Column -->
<ng-container matColumnDef="alias">
<th mat-header-cell *matHeaderCellDef> Alias </th>
<td mat-cell *matCellDef="let element" class="domain">
<td mat-cell *matCellDef="let element">
{{ element.alias }}
</td>
</ng-container>
Expand All @@ -285,8 +285,8 @@ <h3>Fields</h3>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-header-row *matHeaderRowDef="aliasDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: aliasDisplayedColumns;"></tr>
</table>
}
</div>
Expand All @@ -297,6 +297,100 @@ <h3>Fields</h3>
</mat-card-content>
</mat-card>

<mat-card class="margin-bottom-20" appearance="outlined">
<mat-card-header>
<mat-card-title>
Export
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="flex-row flex-responsive flex-stretch gap-16 margin-bottom-20">
<div>
You can request an archive containing your posts and uploaded media. The exported data will be provided
in the ActivityPub format, ensuring compatibility with any compliant software. Archives may be requested
once every 30 days.
</div>
<div class="text-right">
@if (showRequestArchiveButton()) {
<button type="button" mat-raised-button color="primary" aria-label="Request archive" (click)="onRequestArchive()">Request archive</button>
}
</div>
</div>

@if (archives && archives.length > 0) {
<table mat-table [dataSource]="archives" @fadeIn>

<!-- RequestDate Column -->
<ng-container matColumnDef="requestDate">
<th mat-header-cell *matHeaderCellDef> Request date </th>
<td mat-cell *matCellDef="let element">
{{ element.requestDate | date: 'short' }}
</td>
</ng-container>

<!-- Request StartDate Column -->
<ng-container matColumnDef="startDate">
<th mat-header-cell *matHeaderCellDef> Start processing date </th>
<td mat-cell *matCellDef="let element">
@if (element.startDate) {
{{ element.startDate | date: 'short' }}
}
</td>
</ng-container>

<!-- Request EndDate Column -->
<ng-container matColumnDef="endDate">
<th mat-header-cell *matHeaderCellDef> End processing date </th>
<td mat-cell *matCellDef="let element">
@if (element.endDate) {
{{ element.endDate | date: 'short' }}
}
</td>
</ng-container>

<!-- Status Column -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> Status </th>
<td mat-cell *matCellDef="let element">
<mat-chip-set aria-label="Status">
@switch (element.status) {
@case (archiveStatus.New) {
<mat-chip [disableRipple]="true">New</mat-chip>
}
@case (archiveStatus.Processing) {
<mat-chip [disableRipple]="true">Processing...</mat-chip>
}
@case (archiveStatus.Ready) {
<mat-chip [disableRipple]="true">Ready</mat-chip>
}
@case (archiveStatus.Expired) {
<mat-chip [disableRipple]="true">Expired</mat-chip>
}
@case (archiveStatus.Error) {
<mat-chip [disableRipple]="true">Error</mat-chip>
}
}
</mat-chip-set>
</td>
</ng-container>

<!-- Request EndDate Column -->
<ng-container matColumnDef="download">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let element">
@if (element.status === archiveStatus.Ready) {
<a href="#">Download your archive</a>
}
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="archivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: archivesDisplayedColumns;"></tr>
</table>
}
</mat-card-content>
</mat-card>

<mat-card class="margin-bottom-20" appearance="outlined">
<mat-card-header>
<mat-card-title>
Expand Down
77 changes: 75 additions & 2 deletions src/app/pages/account/account.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import { CreateAliasDialog } from 'src/app/dialogs/create-alias-dialog/create-al
import { UserAlias } from 'src/app/models/user-alias';
import { UserAliasesService } from 'src/app/services/http/user-aliases.service';
import { ConfirmationDialog } from 'src/app/dialogs/confirmation-dialog/confirmation.dialog';
import { ArchivesService } from 'src/app/services/http/archives.service';
import { Archive } from 'src/app/models/archive';
import { ArchiveStatus } from 'src/app/models/archive-status';

@Component({
selector: 'app-account',
Expand All @@ -34,15 +37,22 @@ import { ConfirmationDialog } from 'src/app/dialogs/confirmation-dialog/confirma
animations: fadeInAnimation
})
export class AccountPage extends ResponsiveComponent implements OnInit {
readonly archiveStatus = ArchiveStatus;

userName = '';
verification = '';
user: User = new User();
isReady = false;
twoFactorTokenEnabled = false;

readonly displayedColumns: string[] = ['alias', 'actions'];
readonly aliasDisplayedColumns: string[] = ['alias', 'actions'];

archivesDisplayedColumns: string[] = [];
private readonly archivesDisplayedColumnsFull: string[] = ['requestDate', 'startDate', 'endDate', 'status', 'download'];
private readonly archivesDisplayedColumnsMinimum: string[] = ['requestDate', 'download'];

userAliases: UserAlias[] = [];
archives: Archive[] = [];

selectedAvatarFile: any = null;
avatarSrc?: string;
Expand All @@ -59,6 +69,7 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
private userAliasesService: UserAliasesService,
private messageService: MessagesService,
private windowService: WindowService,
private archivesService: ArchivesService,
private router: Router,
public dialog: MatDialog,
private clipboard: Clipboard,
Expand All @@ -79,6 +90,7 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
this.userName = userFromToken?.userName;
await this.loadUserData();
await this.loadUserAliases();
await this.loadArchives();
} else {
this.messageService.showError('Cannot download user settings.');
}
Expand All @@ -94,6 +106,22 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
}
}

protected override onHandsetPortrait(): void {
this.archivesDisplayedColumns = this.archivesDisplayedColumnsMinimum;
}

protected override onHandsetLandscape(): void {
this.archivesDisplayedColumns = this.archivesDisplayedColumnsMinimum;
}

protected override onTablet(): void {
this.archivesDisplayedColumns = this.archivesDisplayedColumnsMinimum;
}

protected override onBrowser(): void {
this.archivesDisplayedColumns = this.archivesDisplayedColumnsFull;
}

async onSubmit(): Promise<void> {
try {
if (this.user.userName != null) {
Expand Down Expand Up @@ -280,8 +308,9 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
if (result?.confirmed) {
try {
await this.userAliasesService.delete(userAlias.id);
this.messageService.showSuccess('Account alias has been deleted.');
await this.loadUserAliases();

this.messageService.showSuccess('Account alias has been deleted.');
} catch (error) {
console.error(error);
this.messageService.showServerError(error);
Expand All @@ -290,6 +319,46 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
});
}

async onRequestArchive(): Promise<void> {
try {
await this.archivesService.create();
await this.loadArchives();

this.messageService.showSuccess('Archive has been requested.');
} catch (error) {
console.error(error);
this.messageService.showServerError(error);
}
}

showRequestArchiveButton(): boolean {
if (this.archives?.length === 0) {
return true;
}

if (this.archives.some(x => x.status === ArchiveStatus.New || x.status === ArchiveStatus.Processing)) {
return false;
}

const readyArchives = this.archives.filter(x => x.status === ArchiveStatus.Ready);
if (readyArchives?.length > 0) {
const readyArchive = readyArchives[0];
if (readyArchive && readyArchive.requestDate) {
const requestDate = new Date(readyArchive.requestDate);
requestDate.setMonth(requestDate.getMonth() + 1);

console.log(requestDate);

const currentDate = new Date();
if (requestDate > currentDate) {
return false;
}
}
}

return true;
}

private async loadUserData(): Promise<void> {
this.user = await this.usersService.profile(this.userName);
this.avatarSrc = this.user.avatarUrl ?? 'assets/avatar-placeholder.svg';
Expand All @@ -299,4 +368,8 @@ export class AccountPage extends ResponsiveComponent implements OnInit {
private async loadUserAliases(): Promise<void> {
this.userAliases = await this.userAliasesService.get();
}

private async loadArchives(): Promise<void> {
this.archives = await this.archivesService.get();
}
}
23 changes: 23 additions & 0 deletions src/app/services/http/archives.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { WindowService } from '../common/window.service';
import { Archive } from 'src/app/models/archive';

@Injectable({
providedIn: 'root'
})
export class ArchivesService {
constructor(private httpClient: HttpClient, private windowService: WindowService) {
}

public async get(): Promise<Archive[]> {
const event$ = this.httpClient.get<Archive[]>(this.windowService.apiUrl() + '/api/v1/archives');
return await firstValueFrom(event$);
}

public async create(): Promise<Archive> {
const event$ = this.httpClient.post<Archive>(this.windowService.apiUrl() + '/api/v1/archives', null);
return await firstValueFrom(event$);
}
}

0 comments on commit e9ece58

Please sign in to comment.