Compare commits

...

2 commits

13 changed files with 455 additions and 19 deletions

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { StandUp } from './standup'; import { StandUp } from './standup';
import { SupaService } from './supa.service'; import { SupaService } from './supa.service';
@ -10,11 +10,17 @@ export class StandupService {
// standupMap: Map<number, StandUp> = new Map(); // standupMap: Map<number, StandUp> = new Map();
standupMap = {}; standupMap = {};
standups: BehaviorSubject<any> = new BehaviorSubject([]); standups: BehaviorSubject<any> = new BehaviorSubject([]);
isListening: boolean = false;
constructor( constructor(
private supa: SupaService, private supa: SupaService,
) { } ) { }
/**
* Get StandUps from local store.
* Requests data if store is emtpy.
* @returns Observable<StandUp[]>
*/
getStandUps(): Observable<StandUp[]> { getStandUps(): Observable<StandUp[]> {
if (Object.values(this.standupMap).length === 0) { if (Object.values(this.standupMap).length === 0) {
console.log('getStandUps - REFRESH') console.log('getStandUps - REFRESH')
@ -25,31 +31,133 @@ export class StandupService {
} }
} }
/**
* Listen to realtime events from standup db.
*/
subscribeToStandups() {
if (!this.isListening) {
this.isListening = true;
this.supa.client.from<StandUp>('standup').on('*', payload => {
console.log('subscribeToStandups - REALTIME EVENT', payload);
if ((payload.eventType === 'INSERT') || (payload.eventType === 'UPDATE')) {
this.standupMap[payload.new.id] = payload.new;
} else {
delete this.standupMap[payload.old.id];
}
this.next();
}).subscribe();
}
}
/**
* Requests up to date data from API.
* Returns the local copy.
* @returns Observable<StandUp>
*/
refreshStandUps(): Observable<StandUp[]> { refreshStandUps(): Observable<StandUp[]> {
this.supa.client.from<StandUp>('standup').select('id, name, description') this.subscribeToStandups();
this.supa.client.from<StandUp>('standup').select()
.then(standups => this.updateStore(standups.body)) .then(standups => this.updateStore(standups.body))
.catch(error => console.log('Error: ', error)); .catch(error => console.log('Error: ', error));
return this.standups.asObservable(); return this.standups.asObservable();
} }
/**
* Update the local store with provided standups
* @param standups
*/
updateStore(standups: StandUp[]) { updateStore(standups: StandUp[]) {
standups.forEach(e => { standups.forEach(e => {
// this.standupMap.set(e.id, e); // this.standupMap.set(e.id, e);
this.standupMap[e.id] = e; this.standupMap[e.id] = e;
}); });
this.standups.next(Object.values(this.standupMap)); this.next();
} }
updateOne(standup: StandUp) { /**
* Emits local store.
*/
next = () => this.standups.next(Object.values(this.standupMap));
/**
* Retrieve standup from local store.
* @param id
*/
getOne(id: number) {
if (this.standupMap[id]) {
return of(this.standupMap[id]);
} else {
const subject: Subject<StandUp> = new Subject();
this.supa.client.from<StandUp>('standup').select('id, name, description')
.filter(<never>'id', 'eq', id)
.then(data => {
this.updateStore([data.body[0]]);
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
} }
deleteOne(standup: StandUp) { /**
*
* @param standup
*/
updateOne(standup: StandUp): Observable<StandUp> {
const subject: Subject<StandUp> = new Subject();
this.supa.client.from<StandUp>('standup').update(standup)
.match({ id: standup.id })
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
} }
addOne(standup: StandUp) {
/**
* Removes one standup from db.
* @param standup
*/
deleteOne(standup: StandUp): Observable<StandUp> {
const subject: Subject<StandUp> = new Subject();
this.supa.client.from<StandUp>('standup').delete()
.match({ id: standup.id })
.then(data => {
subject.next(standup);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
/**
* Creates a standup on the db.
* @param standup
*/
addOne(standup: StandUp): Observable<StandUp> {
const subject: Subject<StandUp> = new Subject();
this.supa.client.from<StandUp>('standup').insert(standup)
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
} }
} }

View file

@ -2,4 +2,9 @@ export class StandUp {
id: number; id: number;
name: string; name: string;
description: string; description: string;
constructor(name: string, description: string) {
this.name = name;
this.description = description;
}
} }

View file

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { StoryService } from './story.service';
describe('StoryService', () => {
let service: StoryService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(StoryService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View file

@ -0,0 +1,181 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { Story } from './story';
import { SupaService } from './supa.service';
@Injectable({
providedIn: 'root'
})
export class StoryService {
// storyMap: Map<number, Story> = new Map();
storyMap = {};
stories: BehaviorSubject<any> = new BehaviorSubject([]);
subscribedStandUpIds: number[] = [];
isListening: boolean = false;
constructor(
private supa: SupaService,
) { }
/**
* Get Storys from local store.
* Requests data if store is emtpy.
* @param standup_id
* @returns Observable<Story[]>
*/
getStories(standup_id: number): Observable<Story[]> {
if (!this.subscribedStandUpIds.includes(standup_id)) {
this.subscribeToStories(standup_id);
console.log('getStories - REFRESH', standup_id)
const subject: Subject<Story[]> = new Subject();
this.supa.client.from<Story>('story').select()
.filter(<never>'standup_id', 'eq', standup_id)
.then(data => {
this.updateStore(data.body);
subject.next(data.body);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
} else {
console.log('getStories - LOCAL', standup_id)
const stories = Object.values<Story>(this.storyMap).filter(e => e.standup_id === standup_id)
console.log('filteredStories', stories)
return of(stories);
}
}
/**
* Listen to realtime events from story db.
*/
subscribeToStories(standup_id: number) {
// TODO check for this.subscribedStandUpIds.includes(standup_id)
if (!this.isListening) {
this.subscribedStandUpIds.push(standup_id);
this.supa.client.from<Story>('story').on('*', payload => {
console.log('subscribeToStories - REALTIME EVENT', payload)
if ((payload.eventType === 'INSERT') || (payload.eventType === 'UPDATE')) {
this.storyMap[payload.new.id] = payload.new;
} else {
delete this.storyMap[payload.old.id];
}
this.next();
}).subscribe();
}
}
/**
* Requests up to date data from API.
* Returns the local copy.
* @returns Observable<Story>
*/
// refreshStorys(): Observable<Story[]> {
// this.subscribeToStories();
// this.supa.client.from<Story>('story').select()
// .then(stories => this.updateStore(stories.body))
// .catch(error => console.log('Error: ', error));
// return this.stories.asObservable();
// }
/**
* Update the local store with provided stories
* @param stories
*/
updateStore(stories: Story[]) {
stories.forEach(e => {
// this.storyMap.set(e.id, e);
this.storyMap[e.id] = e;
});
this.next();
}
/**
* Emits local store.
*/
next = () => this.stories.next(Object.values(this.storyMap));
/**
* Retrieve story from local store.
* @param id
*/
getOne(id: number) {
if (this.storyMap[id]) {
return of(this.storyMap[id]);
} else {
const subject: Subject<Story> = new Subject();
this.supa.client.from<Story>('story').select('id, name, description')
.filter(<never>'id', 'eq', id)
.then(data => {
this.updateStore([data.body[0]]);
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
}
/**
*
* @param story
*/
updateOne(story: Story): Observable<Story> {
const subject: Subject<Story> = new Subject();
this.supa.client.from<Story>('story').update(story)
.match({ id: story.id })
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
/**
* Removes one story from db.
* @param story
*/
deleteOne(story: Story): Observable<Story> {
const subject: Subject<Story> = new Subject();
this.supa.client.from<Story>('story').delete()
.match({ id: story.id })
.then(data => {
subject.next(story);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
/**
* Creates a story on the db.
* @param story
*/
addOne(story: Story): Observable<Story> {
const subject: Subject<Story> = new Subject();
this.supa.client.from<Story>('story').insert(story)
.then(data => {
subject.next(data.body[0]);
subject.complete();
})
.catch(error => {
subject.error(error);
subject.complete();
});
return subject.asObservable();
}
}

View file

@ -0,0 +1,13 @@
export class Story {
id: number;
standup_id: number;
user_id: string
base64: string;
created_at: string | Date;
constructor(user_id: string, standup_id: number, base64: string) {
this.user_id = user_id;
this.standup_id = standup_id;
this.base64 = base64;
}
}

View file

@ -4,7 +4,7 @@
</nav> </nav>
<div class="container-fluid p-0"> <div class="container-fluid p-0">
<div class="myContainer d-flex"> <div class="myContainer d-flex">
<div class="menu"> <div *ngIf="supa.user | async" class="menu">
<app-channel-list></app-channel-list> <app-channel-list></app-channel-list>
</div> </div>
<div class="container content"> <div class="container content">

View file

@ -7,6 +7,7 @@ import { FormsModule } from '@angular/forms';
import { SupaService } from './api/supabase/supa.service'; import { SupaService } from './api/supabase/supa.service';
import { StandupService } from './api/supabase/standup.service'; import { StandupService } from './api/supabase/standup.service';
import { StoryService } from './api/supabase/story.service';
import { AuthGuard } from './authguard.service'; import { AuthGuard } from './authguard.service';
import { AppComponent } from "./app.component"; import { AppComponent } from "./app.component";
@ -40,6 +41,7 @@ import { DashboardComponent } from './dashboard/dashboard.component';
providers: [ providers: [
SupaService, SupaService,
StandupService, StandupService,
StoryService,
AuthGuard, AuthGuard,
], ],
bootstrap: [ bootstrap: [

View file

@ -1,5 +1,5 @@
<div class="list bg-dark text-white p-3"> <div class="list bg-dark text-white p-3">
<p><strong>Standup</strong> <strong class="newBtn">+</strong></p> <p><strong>Standup</strong> <strong class="newBtn" (click)="createStandUp()">+</strong></p>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li *ngFor="let s of standups" [title]="s.description" [routerLink]="'/huddle/'+s.id">{{s.name}}</li> <li *ngFor="let s of standups" [title]="s.description" [routerLink]="'/huddle/'+s.id">{{s.name}}</li>
</ul> </ul>

View file

@ -26,4 +26,11 @@ export class ChannelListComponent implements OnInit {
return this.standupSevice.getStandUps(); return this.standupSevice.getStandUps();
} }
createStandUp() {
this.standupSevice.addOne(new StandUp('test', 'test')).subscribe(
data => console.log(data),
error => console.error(error)
)
}
} }

View file

@ -1,6 +1,16 @@
<div class="row"> <div *ngIf="standup" class="row">
<div class="col-12 col-sm-9">
<legend #name contenteditable="true" (click)="dataUpdated=true">{{ standup.name }}</legend>
<p #desc contenteditable="true" (click)="dataUpdated=true">{{ standup.description }}</p>
</div>
<div class="col-12 col-sm-3">
<div class="float-right">
<button class="btn btn-primary ml-1 mr-1" [disabled]="!dataUpdated" (click)="updateStandUp(name.innerText, desc.innerText)">Save</button>
<button class="btn btn-danger ml-1 mr-1" (click)="deleteStandUp()">Delete</button>
</div>
</div>
<div class="col-12"> <div class="col-12">
<ah-recorder></ah-recorder> <ah-recorder (recordingEnded)="uploadStory($event)"></ah-recorder>
</div> </div>
<div *ngFor="let user of users" class="col p-3"> <div *ngFor="let user of users" class="col p-3">
<div class="text-center" (click)="playStory(user)"> <div class="text-center" (click)="playStory(user)">

View file

@ -1,5 +1,11 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from '@angular/router';
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
import { SupabaseAuthUser } from '@supabase/supabase-js';
import { StandUp } from '../api/supabase/standup';
import { StandupService } from '../api/supabase/standup.service';
import { Story } from '../api/supabase/story';
import { StoryService } from '../api/supabase/story.service';
import { SupaService } from '../api/supabase/supa.service'; import { SupaService } from '../api/supabase/supa.service';
@Component({ @Component({
@ -33,13 +39,34 @@ export class HuddleComponent implements OnInit {
} }
]; ];
selectedUser; selectedUser;
standup: StandUp;
stories: Story[] = [];
constructor( constructor(
private modalService: NgbModal, private modalService: NgbModal,
public supa: SupaService, private standupService: StandupService,
private storyService: StoryService,
private supaService: SupaService,
private route: ActivatedRoute,
) {} ) {}
ngOnInit() { ngOnInit() {
this.route.params.subscribe(
params => {
if (params.id) {
this.standupService.getOne(params.id).subscribe(
data => {
console.log(data);
this.standup = data;
this.loadStories(data.id).subscribe(stories => this.stories = stories);
},
error => {
console.error(error);
}
)
}
}
)
} }
playStory(user) { playStory(user) {
@ -91,4 +118,48 @@ export class HuddleComponent implements OnInit {
} }
this.selectedUser = this.users[index + 1]; this.selectedUser = this.users[index + 1];
} }
updateStandUp(name:string, desc:string) {
this.standup.name = name;
this.standup.description = desc;
this.standupService.updateOne(this.standup).subscribe(
data => {
console.log('Success', data);
},
error => {
console.error('Failed', error);
}
)
}
deleteStandUp() {
this.standupService.deleteOne(this.standup).subscribe(
data => {
console.log('Success', data);
},
error => {
console.error('Failed', error);
}
);
}
async uploadStory(base64: string) {
console.log('uploadStory', event);
const user: SupabaseAuthUser = await this.supaService.client.auth.user();
// TODO ID muss in DB noch autoincremented werden.
const story = new Story(user.id, this.standup.id, base64);
this.storyService.addOne(story).subscribe(
data => {
console.log('Success', data);
},
error => {
console.error('Failed', error);
}
);
}
loadStories(id: number) {
return this.storyService.getStories(id);
}
} }

View file

@ -1,11 +1,21 @@
<button class="btn btn-primary" (click)="openModal()">Record</button> <button class="btn btn-primary" (click)="openModal()">
<svg width="20" height="20" style="height: 20px; width: 20px; margin-top: -4px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z"
fill="currentColor" />
</svg>
Record
</button>
<ng-template #content let-modal> <ng-template #content let-modal>
<div class="modal-header"> <div class="modal-header">
<strong>Record a stoy</strong> <strong>Record a stoy</strong>
<span class="float-right cursor-pointer" (click)="modal.dismiss('Cross click')"> <span class="float-right cursor-pointer" (click)="modal.dismiss('Cross click')">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" style="height: 20px; width: 20px; margin-top: -4px;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M6.2253 4.81108C5.83477 4.42056 5.20161 4.42056 4.81108 4.81108C4.42056 5.20161 4.42056 5.83477 4.81108 6.2253L10.5858 12L4.81114 17.7747C4.42062 18.1652 4.42062 18.7984 4.81114 19.1889C5.20167 19.5794 5.83483 19.5794 6.22535 19.1889L12 13.4142L17.7747 19.1889C18.1652 19.5794 18.7984 19.5794 19.1889 19.1889C19.5794 18.7984 19.5794 18.1652 19.1889 17.7747L13.4142 12L19.189 6.2253C19.5795 5.83477 19.5795 5.20161 19.189 4.81108C18.7985 4.42056 18.1653 4.42056 17.7748 4.81108L12 10.5858L6.2253 4.81108Z" d="M6.2253 4.81108C5.83477 4.42056 5.20161 4.42056 4.81108 4.81108C4.42056 5.20161 4.42056 5.83477 4.81108 6.2253L10.5858 12L4.81114 17.7747C4.42062 18.1652 4.42062 18.7984 4.81114 19.1889C5.20167 19.5794 5.83483 19.5794 6.22535 19.1889L12 13.4142L17.7747 19.1889C18.1652 19.5794 18.7984 19.5794 19.1889 19.1889C19.5794 18.7984 19.5794 18.1652 19.1889 17.7747L13.4142 12L19.189 6.2253C19.5795 5.83477 19.5795 5.20161 19.189 4.81108C18.7985 4.42056 18.1653 4.42056 17.7748 4.81108L12 10.5858L6.2253 4.81108Z"
fill="currentColor" /> fill="currentColor" />
@ -23,7 +33,7 @@
d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM20 12C20 16.4183 16.4183 20 12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4C16.4183 4 20 7.58172 20 12Z"
fill="currentColor" /> fill="currentColor" />
</svg> </svg>
Record Start recording
</button> </button>
<button *ngIf="isRecording" class="btn btn-danger" (click)="stopRecording()">Stop</button> <button *ngIf="isRecording" class="btn btn-danger" (click)="stopRecording()">Stop</button>
</div> </div>

View file

@ -1,4 +1,4 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
import * as rrtc from 'recordrtc'; import * as rrtc from 'recordrtc';
@ -10,6 +10,7 @@ import * as rrtc from 'recordrtc';
export class RecorderComponent implements OnInit { export class RecorderComponent implements OnInit {
@ViewChild("content") content; @ViewChild("content") content;
@ViewChild("activeStory") video; @ViewChild("activeStory") video;
@Output('recordingEnded') recordingEnded: EventEmitter<string> = new EventEmitter();
stream: MediaStream; stream: MediaStream;
recorder: rrtc.RecordRTCPromisesHandler; recorder: rrtc.RecordRTCPromisesHandler;
modal: NgbModalRef; modal: NgbModalRef;
@ -46,12 +47,24 @@ export class RecorderComponent implements OnInit {
} }
async stopRecording() { async stopRecording() {
const that = this;
this.isRecording = false; this.isRecording = false;
await this.recorder.stopRecording(); await this.recorder.stopRecording();
let blob = await this.recorder.getBlob(); let blob = await this.recorder.getBlob();
// TODO upload instead // read as b64
rrtc.invokeSaveAsDialog(blob, 'Recorded-Video.webm'); const reader = new FileReader();
this.modal.close(); reader.readAsDataURL(blob);
reader.onloadend = function() {
const base64data:string = String(reader.result);
console.log(base64data);
that.recordingEnded.emit(base64data);
that.modal.close();
}
const handleError = (error) => {
console.error(error);
}
reader.onerror = handleError;
reader.onabort = handleError;
} }
} }