From a6c0b9c962a4fad56ca7f1285d177690bbbc483c Mon Sep 17 00:00:00 2001
From: ogdbd3h5qze42igcv8wcrqk3 <ogdbd3h5qze42igcv8wcrqk3@systemli.org>
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 @@
   <app-email-settings [hoodId]="hoodId"></app-email-settings>
   <app-twitter-settings [hoodId]="hoodId"></app-twitter-settings>
   <app-telegram-settings [hoodId]="hoodId"></app-telegram-settings>
+  <app-mastodon-settings [hoodId]="hoodId"></app-mastodon-settings>
 </div>
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 @@
+<div *ngIf="mastodons$ | loading | async as mastodons">
+    <ng-template [ngIf]="mastodons.value">
+      <mat-card appearance="outlined">
+        <mat-card-header>
+          <div mat-card-avatar class="mastodon"></div>
+          <mat-card-title class="platform-title">
+            mastodon
+            <button mat-icon-button aria-label="How to use">
+              <mat-icon
+                matTooltip="How to send and receive hood broadcast messages with mastodon"
+                class="info-button"
+                (click)="onInfoClick()"
+                >info</mat-icon
+              >
+            </button>
+          </mat-card-title>
+        </mat-card-header>
+        <mat-card-content *ngIf="mastodons.value.length !== 0; else nomastodon">
+          <mat-selection-list [multiple]="false" class="list">
+            <a
+              *ngFor="let mastodon of mastodons.value"
+              href="https://{{mastodon.instance}}/@{{ mastodon.username }}"
+              routerLinkActive="router-link-active"
+            >
+              <mat-list-option>
+                @{{ mastodon.username }}
+                <mat-divider></mat-divider>
+              </mat-list-option>
+            </a>
+          </mat-selection-list>
+        </mat-card-content>
+      </mat-card>
+      <ng-template #nomastodon>
+        <mat-card-content>
+          Unfortunately your hood admin has not configured mastodon as platform
+          yet.
+        </mat-card-content>
+      </ng-template>
+    </ng-template>
+    <ng-template [ngIf]="mastodons.error"
+      ><mat-icon class="warning">warning</mat-icon></ng-template
+    >
+    <ng-template [ngIf]="mastodons.loading">
+      <mat-spinner [diameter]="45" class="spinner"></mat-spinner>
+    </ng-template>
+  </div>
+  
\ 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<MastodonBotCardComponent>;
+
+  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 @@
+<mat-dialog-content>
+    <div class="container">
+      <h2>How to communicate with the hood via Telegram?</h2>
+      <mat-accordion>
+        <mat-expansion-panel>
+          <mat-expansion-panel-header>
+            <mat-panel-title
+              >How to subscribe to the hood via Telegram?</mat-panel-title
+            >
+          </mat-expansion-panel-header>
+          <ol>
+            <li>
+              Click on the telegram bot name that is shown in the telegram card.
+            </li>
+            <li>
+              Start messaging the telegram bot that the link leads to by writing a
+              message containing only the word <strong>/start</strong>. Done!
+            </li>
+          </ol>
+        </mat-expansion-panel>
+        <mat-expansion-panel>
+          <mat-expansion-panel-header>
+            <mat-panel-title
+              >How to send a broadcast message to the hood?</mat-panel-title
+            >
+          </mat-expansion-panel-header>
+          <p>
+            Write a direct message to the bot. This message will be broadcasted to
+            the other platforms.
+          </p>
+        </mat-expansion-panel>
+        <mat-expansion-panel>
+          <mat-expansion-panel-header>
+            <mat-panel-title>How to receive messages?</mat-panel-title>
+          </mat-expansion-panel-header>
+          <p>
+            If you subscribed to the bot, you will automatically receive the
+            messages of your hood from the bot.
+          </p>
+        </mat-expansion-panel>
+        <mat-expansion-panel>
+          <mat-expansion-panel-header>
+            <mat-panel-title>How to stop receiving messages?</mat-panel-title>
+          </mat-expansion-panel-header>
+          <p>
+            Write a message with content <strong>/stop</strong> to the bot. You
+            should receive a message from the bot which confirms that the
+            unsubscription was successful.
+          </p>
+        </mat-expansion-panel>
+      </mat-accordion>
+    </div>
+  </mat-dialog-content>
+  
\ 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 @@
+<h2 mat-dialog-title>Create new inbox</h2>
+
+<mat-dialog-content>
+  <form class="input" [formGroup]="form">
+    <mat-form-field>
+      <mat-label>Mastodon Instance URL</mat-label>
+      <input matInput formControlName="instance_url" />
+      <mat-error
+        *ngIf="
+          form.controls.instance_url.errors &&
+          form.controls.instance_url.errors.required
+        "
+        >Mastodon Instance URL is required</mat-error
+      >
+    </mat-form-field>
+    <mat-form-field>
+      <mat-label>Mastodon E-Mail</mat-label>
+      <input matInput formControlName="email" />
+      <mat-error
+        *ngIf="
+          form.controls.email.errors &&
+          form.controls.email.errors.required
+        "
+        >Mastodon E-Mail is required</mat-error
+      >
+    </mat-form-field>
+    <mat-form-field>
+      <mat-label>Mastodon Password</mat-label>
+      <input matInput formControlName="password" />
+      <mat-error
+        *ngIf="
+          form.controls.password.errors &&
+          form.controls.password.errors.required
+        "
+        >Mastodon Password is required</mat-error
+      >
+    </mat-form-field>
+  </form>
+</mat-dialog-content>
+
+<mat-dialog-actions align="end">
+  <button mat-button (click)="onCancel()">Cancel</button>
+  <button mat-button (click)="onSubmit()" cdkFocusInitial>
+    Add Mastodon bot
+  </button>
+</mat-dialog-actions>
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<MastodonDialogComponent>;
+
+  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<MastodonDialogComponent>,
+    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 @@
+<mat-card appearance="outlined">
+    <mat-card-header>
+      <div mat-card-avatar class="mastodon"></div>
+      <mat-card-title class="platform-title">
+        Mastodon
+        <button mat-icon-button aria-label="How to use">
+          <mat-icon
+            matTooltip="How to add an mastodon bot to your hood"
+            class="info-button"
+            (click)="onInfoClick()"
+            >info</mat-icon
+          >
+        </button>
+      </mat-card-title>
+    </mat-card-header>
+    <mat-card-content>
+      <mat-list *ngIf="mastodons$ | loading | async as mastodons">
+        <ng-template [ngIf]="mastodons.value">
+          <mat-list-item *ngIf="mastodons.value.length === 0">
+            <button class="add-button" mat-button (click)="onCreate()">
+              <div class="in-add-button">
+                <mat-icon>add</mat-icon>
+                <span> Add a platform connection!</span>
+              </div>
+            </button>
+            <mat-divider></mat-divider>
+          </mat-list-item>
+          <mat-list-item *ngFor="let mastodon of mastodons.value">
+            <div class="entry">
+              @{{ mastodon.username }}
+              <mat-slide-toggle
+                [checked]="mastodon.enabled === 1"
+                (change)="onChange(mastodon)"
+              ></mat-slide-toggle>
+              <button
+                mat-icon-button
+                [matMenuTriggerFor]="menu"
+                aria-label="Example icon-button with a menu"
+              >
+                <mat-icon>more_vert</mat-icon>
+              </button>
+            </div>
+            <mat-divider></mat-divider>
+            <mat-menu #menu="matMenu">
+              <button mat-menu-item (click)="onEdit(mastodon.id)">
+                <mat-icon>edit</mat-icon>
+                <span>Edit</span>
+              </button>
+              <button mat-menu-item (click)="onDelete(mastodon.id)">
+                <mat-icon>delete</mat-icon>
+                <span>Delete</span>
+              </button>
+              <button mat-menu-item (click)="onCreate()">
+                <mat-icon>add</mat-icon>
+                <span>Add another</span>
+              </button>
+            </mat-menu>
+          </mat-list-item>
+        </ng-template>
+        <ng-template [ngIf]="mastodons.error"
+          ><mat-icon class="warning">warning</mat-icon></ng-template
+        >
+        <ng-template [ngIf]="mastodons.loading">
+          <mat-spinner [diameter]="45" class="spinner"></mat-spinner>
+        </ng-template>
+      </mat-list>
+    </mat-card-content>
+  </mat-card>
+  
\ 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<MastodonSettingsComponent>;
+
+  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<Array<any>>;
+
+  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 @@
   <app-twitter-bot-card [hoodId]="hoodId"></app-twitter-bot-card>
   <app-telegram-bot-card [hoodId]="hoodId"></app-telegram-bot-card>
   <app-email-bot-card [hoodId]="hoodId"></app-email-bot-card>
+  <app-mastodon-bot-card [hoodId]="hoodId"></app-mastodon-bot-card>
 </div>
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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
J&YPUie*xt08p;3w

literal 0
HcmV?d00001