From ec0abb5e24475f9b1a3e54d6d2a0d2fa37fb3339 Mon Sep 17 00:00:00 2001 From: ogdbd3h5qze42igcv8wcrqk3 Date: Sun, 19 Mar 2023 18:25:16 +0100 Subject: [PATCH] [frontend] Add initial mastodon frontend --- .../board/platforms/platforms.component.html | 1 + .../mastodon-bot-card.component.html | 47 ++++++++ .../mastodon-bot-card.component.scss | 0 .../mastodon-bot-card.component.spec.ts | 23 ++++ .../mastodon-bot-card.component.ts | 27 +++++ .../mastodon-bot-info-dialog.component.html | 54 ++++++++++ .../mastodon-bot-info-dialog.component.scss | 0 .../mastodon-bot-info-dialog.component.ts | 12 +++ .../mastodon-dialog.component.html | 46 ++++++++ .../mastodon-dialog.component.scss | 26 +++++ .../mastodon-dialog.component.spec.ts | 23 ++++ .../mastodon-dialog.component.ts | 79 ++++++++++++++ .../mastodon-settings.component.html | 69 ++++++++++++ .../mastodon-settings.component.scss | 23 ++++ .../mastodon-settings.component.spec.ts | 23 ++++ .../mastodon-settings.component.ts | 102 ++++++++++++++++++ .../platforms-info-page.component.html | 1 + .../src/app/platforms/platforms.module.ts | 9 ++ frontend/src/assets/mastodon.png | Bin 0 -> 15086 bytes 19 files changed, 565 insertions(+) create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.html create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.scss create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.spec.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.html create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.scss create mode 100644 frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.html create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.scss create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.spec.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.html create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.scss create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.spec.ts create mode 100644 frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.ts create mode 100644 frontend/src/assets/mastodon.png diff --git a/frontend/src/app/dashboard/board/platforms/platforms.component.html b/frontend/src/app/dashboard/board/platforms/platforms.component.html index dec4623..d4c4987 100644 --- a/frontend/src/app/dashboard/board/platforms/platforms.component.html +++ b/frontend/src/app/dashboard/board/platforms/platforms.component.html @@ -39,4 +39,5 @@ + diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.html b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.html new file mode 100644 index 0000000..a4ab1a2 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.html @@ -0,0 +1,47 @@ +
+ + + +
+ + mastodon + + +
+ + + + + @{{ mastodon.username }} + + + + + +
+ + + Unfortunately your hood admin has not configured mastodon as platform + yet. + + +
+ warning + + + +
+ \ No newline at end of file diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.scss b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.spec.ts b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.spec.ts new file mode 100644 index 0000000..fa631eb --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MastodonBotCardComponent } from './mastodon-bot-card.component'; + +describe('MastodonBotCardComponent', () => { + let component: MastodonBotCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MastodonBotCardComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MastodonBotCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.ts b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.ts new file mode 100644 index 0000000..a00bbe0 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-card.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { MastodonService } from 'src/app/core/api'; +import { MastodonBotInfoDialogComponent } from './mastodon-bot-info-dialog/mastodon-bot-info-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'app-mastodon-bot-card', + templateUrl: './mastodon-bot-card.component.html', + styleUrls: ['./mastodon-bot-card.component.scss'], +}) +export class MastodonBotCardComponent implements OnInit { + @Input() hoodId; + mastodons$; + + constructor( + private mastodonService: MastodonService, + private dialog: MatDialog + ) {} + + ngOnInit(): void { + this.mastodons$ = this.mastodonService.getMastodonsPublic(this.hoodId); + } + + onInfoClick() { + this.dialog.open(MastodonBotInfoDialogComponent); + } +} diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.html b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.html new file mode 100644 index 0000000..55ea167 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.html @@ -0,0 +1,54 @@ + +
+

How to communicate with the hood via Telegram?

+ + + + How to subscribe to the hood via Telegram? + +
    +
  1. + Click on the telegram bot name that is shown in the telegram card. +
  2. +
  3. + Start messaging the telegram bot that the link leads to by writing a + message containing only the word /start. Done! +
  4. +
+
+ + + How to send a broadcast message to the hood? + +

+ Write a direct message to the bot. This message will be broadcasted to + the other platforms. +

+
+ + + How to receive messages? + +

+ If you subscribed to the bot, you will automatically receive the + messages of your hood from the bot. +

+
+ + + How to stop receiving messages? + +

+ Write a message with content /stop to the bot. You + should receive a message from the bot which confirms that the + unsubscription was successful. +

+
+
+
+
+ \ No newline at end of file diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.scss b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.ts b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.ts new file mode 100644 index 0000000..0a303ec --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-mastodon-bot-info-dialog', + templateUrl: './mastodon-bot-info-dialog.component.html', + styleUrls: ['./mastodon-bot-info-dialog.component.scss'] +}) +export class MastodonBotInfoDialogComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.html b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.html new file mode 100644 index 0000000..f18716c --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.html @@ -0,0 +1,46 @@ +

Create new inbox

+ + +
+ + Mastodon Instance URL + + Mastodon Instance URL is required + + + Mastodon E-Mail + + Mastodon E-Mail is required + + + Mastodon Password + + Mastodon Password is required + +
+
+ + + + + diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.scss b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.scss new file mode 100644 index 0000000..dbb2947 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.scss @@ -0,0 +1,26 @@ +.input { + display: grid; + grid-template-rows: 1fr 1fr 1fr; + width: 100%; + } + + form { + margin-top: 10px; + height: 100%; + } + + textarea { + height: 120px; + } + + .example-image { + margin-left: 10%; + margin-right: 10%; + width: 80%; + @media screen and (max-width: 600px) { + width: 100%; + margin-left: 0%; + margin-right: 0%; + } + } + \ No newline at end of file diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.spec.ts b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.spec.ts new file mode 100644 index 0000000..16f7cde --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MastodonDialogComponent } from './mastodon-dialog.component'; + +describe('MastodonDialogComponent', () => { + let component: MastodonDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MastodonDialogComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MastodonDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.ts b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.ts new file mode 100644 index 0000000..162162e --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component.ts @@ -0,0 +1,79 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { Validators, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MastodonService } from 'src/app/core/api'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { first } from 'rxjs/operators'; + +@Component({ + selector: 'app-mastodon-dialog', + templateUrl: './mastodon-dialog.component.html', + styleUrls: ['./mastodon-dialog.component.scss'], +}) +export class MastodonDialogComponent implements OnInit { + form: UntypedFormGroup; + + constructor( + public dialogRef: MatDialogRef, + private formBuilder: UntypedFormBuilder, + private mastodonService: MastodonService, + private snackBar: MatSnackBar, + @Inject(MAT_DIALOG_DATA) public data + ) {} + + ngOnInit(): void { + this.form = this.formBuilder.group({ + email: ['', Validators.required], + password: ['', Validators.required], + instance_url: ['', Validators.required], + }); + + if (this.data.mastodonId) { + this.mastodonService + .getMastodon(this.data.mastodonId, this.data.hoodId) + .subscribe((data) => { + this.form.controls.email.setValue(data.email); + this.form.controls.password.setValue(data.password); + this.form.controls.instance_url.setValue(data.instance_url); + }); + } + } + + onCancel() { + this.dialogRef.close(); + } + + success() { + this.dialogRef.close(); + } + + error() { + this.snackBar.open('Invalid API Key. Try again!', 'Close', { + duration: 2000, + }); + } + + onSubmit() { + if (this.form.invalid) { + return; + } + + const response = { + email: this.form.controls.email.value, + instance_url: this.form.controls.instance_url.value, + password: this.form.controls.password.value + } + + this.mastodonService + .createMastodon(this.data.hoodId, response) + .pipe(first()) + .subscribe( + () => { + this.success(); + }, + () => { + this.error(); + } + ); +} +} diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.html b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.html new file mode 100644 index 0000000..15b5ca1 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.html @@ -0,0 +1,69 @@ + + +
+ + Mastodon + + +
+ + + + + + + + +
+ @{{ mastodon.username }} + + +
+ + + + + + +
+
+ warning + + + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.scss b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.scss new file mode 100644 index 0000000..5265644 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.scss @@ -0,0 +1,23 @@ +.entry { + display: grid; + grid-template-columns: 4fr 40px 20px; + width: 100%; + align-items: center; + } + + .platform-title { + display: grid; + grid-template-columns: 1fr 40px; + width: 100%; + align-items: center; + } + + .platform-heading { + align-self: flex-end; + } + + .mastodon { + background-image: url("../../../../assets/mastodon.png"); + background-size: cover; + } + \ No newline at end of file diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.spec.ts b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.spec.ts new file mode 100644 index 0000000..d03c29d --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MastodonSettingsComponent } from './mastodon-settings.component'; + +describe('MastodonSettingsComponent', () => { + let component: MastodonSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MastodonSettingsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MastodonSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.ts b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.ts new file mode 100644 index 0000000..ffbd679 --- /dev/null +++ b/frontend/src/app/platforms/mastodon/mastodon-settings/mastodon-settings.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { MastodonService } from 'src/app/core/api'; +import { Observable } from 'rxjs'; +import { MastodonBotInfoDialogComponent } from '../mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { MastodonDialogComponent } from './mastodon-dialog/mastodon-dialog.component'; +import { YesNoDialogComponent } from 'src/app/shared/yes-no-dialog/yes-no-dialog.component'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-mastodon-settings', + templateUrl: './mastodon-settings.component.html', + styleUrls: ['./mastodon-settings.component.scss'], +}) +export class MastodonSettingsComponent implements OnInit { + @Input() hoodId; + mastodons$: Observable>; + + constructor( + private mastodonService: MastodonService, + public dialog: MatDialog, + private snackBar: MatSnackBar + ) {} + + ngOnInit(): void { + this.reload(); + } + + private reload() { + this.mastodons$ = this.mastodonService.getMastodons(this.hoodId); + } + + onInfoClick() { + this.dialog.open(MastodonBotInfoDialogComponent); + } + + onDelete(mastodonId) { + const dialogRef = this.dialog.open(YesNoDialogComponent, { + data: { + title: 'Warning', + content: + 'This will also delete the list of subscribers of the mastodon bot.', + }, + }); + + dialogRef.afterClosed().subscribe((response) => { + if (response) { + this.mastodonService + .deleteMastodon(mastodonId, this.hoodId) + .subscribe(() => { + this.reload(); + }); + } + }); + } + + onCreate() { + const dialogRef = this.dialog.open(MastodonDialogComponent, { + data: { hoodId: this.hoodId }, + }); + + dialogRef.afterClosed().subscribe(() => { + this.reload(); + }); + } + + onEdit(mastodonId) { + const dialogRef = this.dialog.open(MastodonDialogComponent, { + data: { hoodId: this.hoodId, mastodonId }, + }); + + dialogRef.afterClosed().subscribe(() => { + this.reload(); + }); + } + + onChange(mastodon) { + if (mastodon.enabled === 0) { + this.mastodonService.startMastodon(mastodon.id, this.hoodId).subscribe( + () => {}, + (error) => { + this.snackBar.open('Could not start. Check your settings.', 'Close', { + duration: 2000, + }); + } + ); + } else if (mastodon.enabled === 1) { + this.mastodonService.stopMastodon(mastodon.id, this.hoodId).subscribe( + () => {}, + (error) => { + this.snackBar.open('Could not stop. Check your settings.', 'Close', { + duration: 2000, + }); + } + ); + } + // TODO yeah i know this is bad, implement disabling/enabling + setTimeout(() => { + this.reload(); + }, 100); + } +} diff --git a/frontend/src/app/platforms/platforms-info-page/platforms-info-page.component.html b/frontend/src/app/platforms/platforms-info-page/platforms-info-page.component.html index b7d8ed4..3d3b646 100644 --- a/frontend/src/app/platforms/platforms-info-page/platforms-info-page.component.html +++ b/frontend/src/app/platforms/platforms-info-page/platforms-info-page.component.html @@ -3,4 +3,5 @@ + diff --git a/frontend/src/app/platforms/platforms.module.ts b/frontend/src/app/platforms/platforms.module.ts index e84b289..b9bb530 100644 --- a/frontend/src/app/platforms/platforms.module.ts +++ b/frontend/src/app/platforms/platforms.module.ts @@ -20,6 +20,10 @@ import { TelegramBotInfoDialogComponent } from './telegram/telegram-bot-card/tel import { TwitterBotInfoDialogComponent } from './twitter/twitter-bot-card/twitter-bot-info-dialog/twitter-bot-info-dialog.component'; import { EmailConfirmationComponent } from './email/email-confirmation/email-confirmation.component'; import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsubscribe.component'; +import { MastodonBotCardComponent } from './mastodon/mastodon-bot-card/mastodon-bot-card.component'; +import { MastodonSettingsComponent } from './mastodon/mastodon-settings/mastodon-settings.component'; +import { MastodonDialogComponent } from './mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component'; +import { MastodonBotInfoDialogComponent } from './mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component'; @NgModule({ declarations: [ @@ -42,10 +46,15 @@ import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsub TwitterBotInfoDialogComponent, EmailConfirmationComponent, EmailUnsubscribeComponent, + MastodonBotCardComponent, + MastodonSettingsComponent, + MastodonDialogComponent, + MastodonBotInfoDialogComponent ], imports: [CommonModule, SharedModule], exports: [ TelegramSettingsComponent, + MastodonSettingsComponent, TwitterSettingsComponent, EmailSettingsComponent, PlatformsInfoPageComponent, diff --git a/frontend/src/assets/mastodon.png b/frontend/src/assets/mastodon.png new file mode 100644 index 0000000000000000000000000000000000000000..b09a98bb9b0649cb67305b6663bd56b3cfb17222 GIT binary patch literal 15086 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