diff --git a/client/src/app/shared/components/applause-bar-display/applause-bar-display.component.scss b/client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.scss
similarity index 76%
rename from client/src/app/shared/components/applause-bar-display/applause-bar-display.component.scss
rename to client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.scss
index e5caf4ee5..ae0795216 100644
--- a/client/src/app/shared/components/applause-bar-display/applause-bar-display.component.scss
+++ b/client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.scss
@@ -11,16 +11,10 @@
}
.level-indicator {
- height: 50px;
display: block;
text-align: center;
.level {
display: inline-block;
- margin-top: 25px;
}
}
-
-.particle-display {
- // height: 100%;
-}
diff --git a/client/src/app/shared/components/applause-bar-display/applause-bar-display.component.spec.ts b/client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.spec.ts
similarity index 100%
rename from client/src/app/shared/components/applause-bar-display/applause-bar-display.component.spec.ts
rename to client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.spec.ts
diff --git a/client/src/app/shared/components/applause-bar-display/applause-bar-display.component.ts b/client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.ts
similarity index 89%
rename from client/src/app/shared/components/applause-bar-display/applause-bar-display.component.ts
rename to client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.ts
index 5e59e66de..08ea847e6 100644
--- a/client/src/app/shared/components/applause-bar-display/applause-bar-display.component.ts
+++ b/client/src/app/site/interaction/components/applause-bar-display/applause-bar-display.component.ts
@@ -4,10 +4,10 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
-import { Applause, ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { fadeAnimation } from 'app/shared/animations';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ApplauseService, ApplauseType } from 'app/site/interaction/services/applause.service';
@Component({
selector: 'os-applause-bar-display',
@@ -26,10 +26,6 @@ export class ApplauseBarDisplayComponent extends BaseViewComponentDirective {
return !!this.level;
}
- public get isApplauseTypeBar(): boolean {
- return this.applauseService.applauseType === ApplauseType.bar;
- }
-
public constructor(
title: Title,
translate: TranslateService,
diff --git a/client/src/app/shared/components/applause-particle-display/applause-particle-display.component.html b/client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.html
similarity index 100%
rename from client/src/app/shared/components/applause-particle-display/applause-particle-display.component.html
rename to client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.html
diff --git a/client/src/app/shared/components/applause-particle-display/applause-particle-display.component.scss b/client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.scss
similarity index 100%
rename from client/src/app/shared/components/applause-particle-display/applause-particle-display.component.scss
rename to client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.scss
diff --git a/client/src/app/shared/components/applause-particle-display/applause-particle-display.component.spec.ts b/client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.spec.ts
similarity index 100%
rename from client/src/app/shared/components/applause-particle-display/applause-particle-display.component.spec.ts
rename to client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.spec.ts
diff --git a/client/src/app/shared/components/applause-particle-display/applause-particle-display.component.ts b/client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.ts
similarity index 96%
rename from client/src/app/shared/components/applause-particle-display/applause-particle-display.component.ts
rename to client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.ts
index 77be271e2..14c19dab4 100644
--- a/client/src/app/shared/components/applause-particle-display/applause-particle-display.component.ts
+++ b/client/src/app/site/interaction/components/applause-particle-display/applause-particle-display.component.ts
@@ -7,10 +7,10 @@ import { Subject } from 'rxjs';
import { auditTime } from 'rxjs/operators';
import { Container } from 'tsparticles';
-import { ApplauseService } from 'app/core/ui-services/applause.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ElementSize } from 'app/shared/directives/resized.directive';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ApplauseService } from 'app/site/interaction/services/applause.service';
import { particleConfig, particleOptions } from './particle-options';
@Component({
@@ -79,7 +79,6 @@ export class ApplauseParticleDisplayComponent extends BaseViewComponentDirective
private setParticleLevel(level: number): void {
if (this.particleContainer) {
const emitters = this.particleContainer.plugins.get('emitters') as any;
- // TODO: Use `Emitters` instead of any.
if (emitters) {
emitters.array[0].emitterOptions.rate.quantity = level;
}
diff --git a/client/src/app/shared/components/applause-particle-display/particle-options.ts b/client/src/app/site/interaction/components/applause-particle-display/particle-options.ts
similarity index 100%
rename from client/src/app/shared/components/applause-particle-display/particle-options.ts
rename to client/src/app/site/interaction/components/applause-particle-display/particle-options.ts
diff --git a/client/src/app/site/interaction/components/call-dialog/call-dialog.component.html b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.html
new file mode 100644
index 000000000..1cae073ac
--- /dev/null
+++ b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.html
@@ -0,0 +1,54 @@
+
+
diff --git a/client/src/app/site/interaction/components/call-dialog/call-dialog.component.scss b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.scss
new file mode 100644
index 000000000..7ac3c2778
--- /dev/null
+++ b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.scss
@@ -0,0 +1,32 @@
+.dialog-mobile {
+ left: 15px !important;
+}
+
+.jitsi-fake-dialog-wrapper {
+ z-index: -1;
+ position: fixed;
+ pointer-events: none;
+ left: 270px;
+ top: 20px;
+ right: 30px;
+ bottom: 0;
+
+ .jitsi-fake-dialog {
+ padding: 0 5px 5px 5px;
+ pointer-events: all;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 90%;
+
+ .jitsi-iframe-wrapper {
+ flex: 1;
+ }
+
+ .jitsi-dialog-actions {
+ .right {
+ float: right;
+ }
+ }
+ }
+}
diff --git a/client/src/app/site/interaction/components/call-dialog/call-dialog.component.spec.ts b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.spec.ts
new file mode 100644
index 000000000..282e7a0e8
--- /dev/null
+++ b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.spec.ts
@@ -0,0 +1,27 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { CallDialogComponent } from './call-dialog.component';
+
+describe('CallDialogComponent', () => {
+ let component: CallDialogComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [CallDialogComponent],
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CallDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/components/call-dialog/call-dialog.component.ts b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.ts
new file mode 100644
index 000000000..ba2c3ab22
--- /dev/null
+++ b/client/src/app/site/interaction/components/call-dialog/call-dialog.component.ts
@@ -0,0 +1,51 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Output,
+ ViewChild
+} from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { ViewportService } from 'app/core/ui-services/viewport.service';
+import { RtcService } from '../../services/rtc.service';
+
+@Component({
+ selector: 'os-call-dialog',
+ templateUrl: './call-dialog.component.html',
+ styleUrls: ['./call-dialog.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class CallDialogComponent implements AfterViewInit {
+ @ViewChild('jitsi')
+ private jitsiNode: ElementRef;
+
+ public jitsiMeetUrl: Observable = this.rtcService.jitsiMeetUrl;
+
+ public isMobile: Observable = this.vp.isMobileSubject;
+
+ public constructor(private cd: ChangeDetectorRef, private rtcService: RtcService, private vp: ViewportService) {}
+
+ public ngAfterViewInit(): void {
+ this.rtcService.setJitsiNode(this.jitsiNode);
+ this.cd.markForCheck();
+ }
+
+ public fullScreen(): void {
+ this.rtcService.enterFullScreen();
+ this.cd.markForCheck();
+ }
+
+ public hangUp(): void {
+ this.rtcService.stopJitsi();
+ this.cd.markForCheck();
+ }
+
+ public hideDialog(): void {
+ this.rtcService.showCallDialog = false;
+ }
+}
diff --git a/client/src/app/site/interaction/components/call/call.component.html b/client/src/app/site/interaction/components/call/call.component.html
new file mode 100644
index 000000000..5233fd94c
--- /dev/null
+++ b/client/src/app/site/interaction/components/call/call.component.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+ {{ 'A conference is already running in your OpenSlides session.' | translate }}
+
+
+
+
+ cloud_off
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ {{ members[memberId].name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/site/interaction/components/call/call.component.scss b/client/src/app/site/interaction/components/call/call.component.scss
new file mode 100644
index 000000000..222d3c955
--- /dev/null
+++ b/client/src/app/site/interaction/components/call/call.component.scss
@@ -0,0 +1,64 @@
+$wrapper-padding: 5px;
+
+.jitsi-list {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ .content {
+ flex: 1 0 auto;
+
+ .disconnected {
+ display: flex;
+ height: 100%;
+
+ * {
+ margin: auto;
+ }
+ }
+
+ .room-members {
+ height: 100%;
+ position: relative;
+
+ .room-list-applause-particles {
+ position: absolute;
+ height: 100%;
+ width: 70px;
+ right: 0;
+ }
+
+ .member-list {
+ max-height: 100%;
+ overflow-y: auto;
+
+ .member-list-entry {
+ margin: 5px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .focused {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .control-grid {
+ padding: $wrapper-padding;
+ display: grid;
+ grid-template-areas: 'empty buttons exit';
+ grid-template-columns: 40px auto 40px;
+
+ .exit {
+ grid-area: exit;
+ }
+
+ .control-buttons {
+ grid-area: buttons;
+ margin: auto;
+ }
+ }
+}
diff --git a/client/src/app/shared/components/jitsi/jitsi.component.spec.ts b/client/src/app/site/interaction/components/call/call.component.spec.ts
similarity index 58%
rename from client/src/app/shared/components/jitsi/jitsi.component.spec.ts
rename to client/src/app/site/interaction/components/call/call.component.spec.ts
index ec145d053..5597e3fd5 100644
--- a/client/src/app/shared/components/jitsi/jitsi.component.spec.ts
+++ b/client/src/app/site/interaction/components/call/call.component.spec.ts
@@ -2,21 +2,21 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
-import { JitsiComponent } from './jitsi.component';
+import { CallComponent } from './call.component';
-describe('JitsiComponent', () => {
- let component: JitsiComponent;
- let fixture: ComponentFixture;
+describe('CallComponent', () => {
+ let component: CallComponent;
+ let fixture: ComponentFixture;
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [E2EImportsModule],
- declarations: [JitsiComponent]
+ declarations: [CallComponent],
+ imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
- fixture = TestBed.createComponent(JitsiComponent);
+ fixture = TestBed.createComponent(CallComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/client/src/app/site/interaction/components/call/call.component.ts b/client/src/app/site/interaction/components/call/call.component.ts
new file mode 100644
index 000000000..00852e182
--- /dev/null
+++ b/client/src/app/site/interaction/components/call/call.component.ts
@@ -0,0 +1,189 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ HostListener,
+ OnDestroy,
+ OnInit,
+ Output
+} from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { Title } from '@angular/platform-browser';
+
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { TranslateService } from '@ngx-translate/core';
+import { Observable } from 'rxjs';
+
+import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ApplauseService } from '../../services/applause.service';
+import { CallRestrictionService } from '../../services/call-restriction.service';
+import { InteractionService } from '../../services/interaction.service';
+import { RtcService } from '../../services/rtc.service';
+import { StreamService } from '../../services/stream.service';
+
+const helpDeskTitle = _('Help desk');
+const liveConferenceTitle = _('Conference room');
+const disconnectedTitle = _('disconnected');
+const connectingTitle = _('connecting ...');
+
+@Component({
+ selector: 'os-call',
+ templateUrl: './call.component.html',
+ styleUrls: ['./call.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class CallComponent extends BaseViewComponentDirective implements OnInit, AfterViewInit, OnDestroy {
+ public isJitsiActiveInAnotherTab: Observable = this.rtcService.inOtherTab;
+ public canEnterCall: Observable = this.callRestrictionService.canEnterCallObservable;
+ public isJitsiDialogOpen: Observable = this.rtcService.showCallDialogObservable;
+
+ public isJitsiActive: boolean;
+ public isJoined: boolean;
+
+ public get showHangUp(): boolean {
+ return this.isJitsiActive && this.isJoined;
+ }
+
+ private dominantSpeaker: string;
+ private members = {};
+ public get memberList(): string[] {
+ return Object.keys(this.members);
+ }
+
+ public get isDisconnected(): boolean {
+ return !this.isJitsiActive && !this.isJoined;
+ }
+
+ public get isConnecting(): boolean {
+ return this.isJitsiActive && !this.isJoined;
+ }
+
+ public get isConnected(): boolean {
+ return this.isJitsiActive && this.isJoined;
+ }
+
+ public get showParticles(): Observable {
+ return this.applauseService.showParticles;
+ }
+
+ public get canSeeLiveStream(): Observable {
+ return this.streamService.canSeeLiveStreamObservable;
+ }
+
+ public get liveStreamUrl(): Observable {
+ return this.streamService.liveStreamUrlObservable;
+ }
+
+ private autoConnect: boolean;
+
+ @Output()
+ public conferenceTitle: EventEmitter = new EventEmitter();
+
+ @Output()
+ public conferenceSubtitle: EventEmitter = new EventEmitter();
+
+ public constructor(
+ titleService: Title,
+ translate: TranslateService,
+ snackBar: MatSnackBar,
+ private callRestrictionService: CallRestrictionService,
+ private rtcService: RtcService,
+ private applauseService: ApplauseService,
+ private interactionService: InteractionService,
+ private streamService: StreamService,
+ private cd: ChangeDetectorRef
+ ) {
+ super(titleService, translate, snackBar);
+
+ this.subscriptions.push(
+ this.rtcService.isJitsiActiveObservable.subscribe(active => {
+ this.isJitsiActive = active;
+ this.updateSubtitle();
+ this.cd.markForCheck();
+ }),
+
+ this.rtcService.isJoinedObservable.subscribe(isJoined => {
+ this.isJoined = isJoined;
+ this.updateSubtitle();
+ this.cd.markForCheck();
+ }),
+
+ this.rtcService.memberObservableObservable.subscribe(members => {
+ this.members = members;
+ this.cd.markForCheck();
+ }),
+
+ this.rtcService.dominantSpeakerObservable.subscribe(domSpeaker => {
+ this.dominantSpeaker = domSpeaker?.displayName;
+ this.updateSubtitle();
+ this.cd.markForCheck();
+ }),
+
+ this.rtcService.autoConnect.subscribe(auto => {
+ this.autoConnect = auto;
+ }),
+
+ this.rtcService.connectedToHelpDesk.subscribe(onHelpDesk => {
+ if (onHelpDesk) {
+ this.conferenceTitle.next(helpDeskTitle);
+ } else {
+ this.conferenceTitle.next(liveConferenceTitle);
+ }
+ this.cd.markForCheck();
+ })
+ );
+ }
+
+ public ngOnInit(): void {
+ this.updateSubtitle();
+ }
+
+ public ngAfterViewInit(): void {
+ if (this.autoConnect) {
+ this.callRoom();
+ }
+ }
+
+ // closing the tab should also try to stop jitsi.
+ // this will usually not be caught by ngOnDestroy
+ @HostListener('window:beforeunload', ['$event'])
+ public beforeunload($event: any): void {
+ this.rtcService.stopJitsi();
+ }
+
+ public ngOnDestroy(): void {
+ super.ngOnDestroy();
+ this.rtcService.stopJitsi();
+ }
+
+ private updateSubtitle(): void {
+ if (this.isJitsiActive && this.isJoined) {
+ this.conferenceSubtitle.next(this.dominantSpeaker || '');
+ } else if (this.isJitsiActive && !this.isJoined) {
+ this.conferenceSubtitle.next(connectingTitle);
+ } else {
+ this.conferenceSubtitle.next(disconnectedTitle);
+ }
+ }
+
+ public async callRoom(): Promise {
+ await this.rtcService.enterConferenceRoom().catch(this.raiseError);
+ this.cd.markForCheck();
+ }
+
+ public forceStart(): void {
+ this.rtcService.enterConferenceRoom(true).catch(this.raiseError);
+ this.cd.markForCheck();
+ }
+
+ public hangUp(): void {
+ this.rtcService.stopJitsi();
+ this.cd.markForCheck();
+ }
+
+ public viewStream(): void {
+ this.interactionService.viewStream();
+ }
+}
diff --git a/client/src/app/site/interaction/components/interaction-container/interaction-container.component.html b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.html
new file mode 100644
index 000000000..21ae32a0d
--- /dev/null
+++ b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.html
@@ -0,0 +1,36 @@
+
+
+
+
+ {{ containerHeadTitle | translate }}
+
+
+ {{ containerHeadSubtitle | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/site/interaction/components/interaction-container/interaction-container.component.scss b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.scss
new file mode 100644
index 000000000..8af177298
--- /dev/null
+++ b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.scss
@@ -0,0 +1,59 @@
+$radius: 5px;
+
+:host {
+ margin-top: auto;
+}
+
+.interaction-container-wrapper {
+ box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
+ border-top-left-radius: $radius;
+ border-top-right-radius: $radius;
+ min-width: 250px;
+ max-width: 70vw;
+}
+
+.container-head {
+ height: 50px;
+ display: flex;
+ border-top-left-radius: $radius;
+ border-top-right-radius: $radius;
+ cursor: pointer;
+
+ .container-head-wrapper {
+ margin-left: 1em;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+
+ .container-head-title {
+ font-weight: bold;
+ }
+}
+
+.container-body {
+ display: flex;
+ max-width: 500px;
+ max-height: 280px;
+ transition: all 350ms ease-out;
+}
+
+.container-body-with-applause-bar {
+ max-width: 530px !important;
+}
+
+.video-player {
+ display: block;
+ width: 500px;
+ height: 280px;
+}
+
+.call-body {
+ display: block;
+ width: 250px;
+ height: 280px;
+}
+
+.container-body-hide {
+ max-width: 250px;
+ max-height: 0px;
+}
diff --git a/client/src/app/site/interaction/components/interaction-container/interaction-container.component.spec.ts b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.spec.ts
new file mode 100644
index 000000000..84f19185f
--- /dev/null
+++ b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.spec.ts
@@ -0,0 +1,27 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { InteractionContainerComponent } from './interaction-container.component';
+
+describe('InteractionContainerComponent', () => {
+ let component: InteractionContainerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [InteractionContainerComponent],
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InteractionContainerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/components/interaction-container/interaction-container.component.ts b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.ts
new file mode 100644
index 000000000..6a8a50e44
--- /dev/null
+++ b/client/src/app/site/interaction/components/interaction-container/interaction-container.component.ts
@@ -0,0 +1,117 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { Title } from '@angular/platform-browser';
+
+import { TranslateService } from '@ngx-translate/core';
+import { combineLatest, forkJoin, merge, Observable } from 'rxjs';
+import { distinctUntilChanged, filter, mergeAll, mergeMap, withLatestFrom } from 'rxjs/operators';
+
+import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ApplauseService } from '../../services/applause.service';
+import { InteractionService } from '../../services/interaction.service';
+import { RtcService } from '../../services/rtc.service';
+import { StreamService } from '../../services/stream.service';
+
+@Component({
+ selector: 'os-interaction-container',
+ templateUrl: './interaction-container.component.html',
+ styleUrls: ['./interaction-container.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class InteractionContainerComponent extends BaseViewComponentDirective {
+ public showBody = false;
+
+ private streamRunning = false;
+ private streamLoadedOnce = false;
+
+ public containerHeadTitle = '';
+ public containerHeadSubtitle = '';
+
+ public get isApplausEnabled(): Observable {
+ return this.applauseService.showApplause;
+ }
+
+ public get showApplauseBar(): Observable {
+ return this.applauseService.showBar;
+ }
+
+ public get isConfStateStream(): Observable {
+ return this.interactionService.isConfStateStream;
+ }
+
+ public get isConfStateJitsi(): Observable {
+ return this.interactionService.isConfStateJitsi;
+ }
+
+ public get isConfStateNone(): Observable {
+ return this.interactionService.isConfStateNone;
+ }
+
+ public get isStreamInOtherTab(): boolean {
+ return !this.streamRunning && this.streamLoadedOnce;
+ }
+
+ public constructor(
+ titleService: Title,
+ translate: TranslateService,
+ matSnackBar: MatSnackBar,
+ rtcService: RtcService,
+ streamService: StreamService,
+ private interactionService: InteractionService,
+ private applauseService: ApplauseService,
+ private cd: ChangeDetectorRef
+ ) {
+ super(titleService, translate, matSnackBar);
+ this.subscriptions.push(
+ interactionService.conferenceStateObservable.pipe(distinctUntilChanged()).subscribe(state => {
+ if (state) {
+ this.clearTitles();
+ }
+ }),
+ rtcService.showCallDialogObservable.subscribe(show => {
+ if (show) {
+ this.showBody = false;
+ }
+ this.cd.markForCheck();
+ }),
+ streamService.streamLoadedOnceObservable.subscribe(loadedOnce => {
+ this.streamLoadedOnce = loadedOnce;
+ if (!this.isStreamInOtherTab) {
+ this.showBody = true;
+ }
+ this.cd.markForCheck();
+ }),
+ streamService.isStreamRunningObservable.subscribe(running => {
+ this.streamRunning = running || false;
+ if (!this.isStreamInOtherTab) {
+ this.showBody = true;
+ }
+ this.cd.markForCheck();
+ })
+ );
+ }
+
+ private clearTitles(): void {
+ this.containerHeadTitle = '';
+ this.containerHeadSubtitle = '';
+ this.cd.detectChanges();
+ }
+
+ public showHideBody(): void {
+ this.showBody = !this.showBody;
+ }
+
+ public updateTitle(title: string): void {
+ if (title !== this.containerHeadTitle) {
+ this.containerHeadTitle = title ?? '';
+ this.cd.detectChanges();
+ }
+ }
+
+ public updateSubtitle(title: string): void {
+ if (title !== this.containerHeadSubtitle) {
+ this.containerHeadSubtitle = title ?? '';
+ this.cd.detectChanges();
+ }
+ }
+}
diff --git a/client/src/app/site/interaction/components/stream/stream.component.html b/client/src/app/site/interaction/components/stream/stream.component.html
new file mode 100644
index 000000000..9fd678744
--- /dev/null
+++ b/client/src/app/site/interaction/components/stream/stream.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ {{ 'You are not allowed to see the live stream' | translate }}
+
+
+
+
diff --git a/client/src/app/site/interaction/components/stream/stream.component.scss b/client/src/app/site/interaction/components/stream/stream.component.scss
new file mode 100644
index 000000000..296629d10
--- /dev/null
+++ b/client/src/app/site/interaction/components/stream/stream.component.scss
@@ -0,0 +1,8 @@
+.applause-particles {
+ position: absolute;
+ display: block;
+ pointer-events: none !important;
+ width: 100px;
+ height: 100%;
+ z-index: 1;
+}
diff --git a/client/src/app/site/interaction/components/stream/stream.component.spec.ts b/client/src/app/site/interaction/components/stream/stream.component.spec.ts
new file mode 100644
index 000000000..ba60ad784
--- /dev/null
+++ b/client/src/app/site/interaction/components/stream/stream.component.spec.ts
@@ -0,0 +1,27 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { StreamComponent } from './stream.component';
+
+describe('StreamComponent', () => {
+ let component: StreamComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [StreamComponent],
+ imports: [E2EImportsModule]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StreamComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/components/stream/stream.component.ts b/client/src/app/site/interaction/components/stream/stream.component.ts
new file mode 100644
index 000000000..3bc2fc784
--- /dev/null
+++ b/client/src/app/site/interaction/components/stream/stream.component.ts
@@ -0,0 +1,115 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ EventEmitter,
+ HostListener,
+ OnDestroy,
+ OnInit,
+ Output
+} from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { Title } from '@angular/platform-browser';
+
+import { TranslateService } from '@ngx-translate/core';
+import { Observable } from 'rxjs';
+
+import { BaseViewComponentDirective } from 'app/site/base/base-view';
+import { ApplauseService } from '../../services/applause.service';
+import { StreamService } from '../../services/stream.service';
+
+@Component({
+ selector: 'os-stream',
+ templateUrl: './stream.component.html',
+ styleUrls: ['./stream.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class StreamComponent extends BaseViewComponentDirective implements AfterViewInit, OnDestroy {
+ private streamRunning = false;
+
+ public liveStreamUrl: string;
+ private streamLoadedOnce: boolean;
+
+ public get showParticles(): Observable {
+ return this.applauseService.showParticles;
+ }
+
+ public get showVideoPlayer(): boolean {
+ return this.streamRunning || this.streamLoadedOnce === false;
+ }
+
+ public get isStreamInOtherTab(): boolean {
+ return !this.streamRunning && this.streamLoadedOnce;
+ }
+
+ @Output()
+ public streamTitle: EventEmitter = new EventEmitter();
+
+ @Output()
+ public streamSubtitle: EventEmitter = new EventEmitter();
+
+ public constructor(
+ titleService: Title,
+ translate: TranslateService,
+ matSnackBar: MatSnackBar,
+ private streamService: StreamService,
+ private applauseService: ApplauseService,
+ private cd: ChangeDetectorRef
+ ) {
+ super(titleService, translate, matSnackBar);
+
+ this.subscriptions.push(
+ this.streamService.liveStreamUrlObservable.subscribe(url => {
+ this.liveStreamUrl = url?.trim();
+ this.cd.markForCheck();
+ }),
+ this.streamService.streamLoadedOnceObservable.subscribe(loadedOnce => {
+ this.streamLoadedOnce = loadedOnce || false;
+ this.cd.markForCheck();
+ }),
+ this.streamService.isStreamRunningObservable.subscribe(running => {
+ this.streamRunning = running || false;
+ this.cd.markForCheck();
+ })
+ );
+ }
+
+ public ngAfterViewInit(): void {
+ this.streamTitle.next('Livestream');
+ this.streamSubtitle.next('');
+ this.cd.detectChanges();
+ }
+
+ // closing the tab should also try to stop jitsi.
+ // this will usually not be caught by ngOnDestroy
+ @HostListener('window:beforeunload', ['$event'])
+ public async beforeunload($event: any): Promise {
+ this.beforeViewCloses();
+ }
+
+ public ngOnDestroy(): void {
+ super.ngOnDestroy();
+ this.beforeViewCloses();
+ }
+
+ private beforeViewCloses(): void {
+ if (this.streamLoadedOnce && this.streamRunning) {
+ this.streamService.deleteStreamingLock();
+ }
+ }
+
+ public forceLoadStream(): void {
+ this.streamService.deleteStreamingLock();
+ }
+
+ public onSteamLoaded(): void {
+ /**
+ * explicit false check, undefined would mean that this was not checked yet
+ */
+ if (this.streamLoadedOnce === false) {
+ this.streamService.setStreamingLock();
+ this.streamService.setStreamRunning(true);
+ }
+ }
+}
diff --git a/client/src/app/site/interaction/interaction.module.ts b/client/src/app/site/interaction/interaction.module.ts
new file mode 100644
index 000000000..39043278f
--- /dev/null
+++ b/client/src/app/site/interaction/interaction.module.ts
@@ -0,0 +1,28 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { NgParticlesModule } from 'ng-particles';
+
+import { ActionBarComponent } from './components/action-bar/action-bar.component';
+import { ApplauseBarDisplayComponent } from './components/applause-bar-display/applause-bar-display.component';
+import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
+import { CallDialogComponent } from './components/call-dialog/call-dialog.component';
+import { CallComponent } from './components/call/call.component';
+import { InteractionContainerComponent } from './components/interaction-container/interaction-container.component';
+import { SharedModule } from '../../shared/shared.module';
+import { StreamComponent } from './components/stream/stream.component';
+
+@NgModule({
+ declarations: [
+ ApplauseBarDisplayComponent,
+ ApplauseParticleDisplayComponent,
+ ActionBarComponent,
+ InteractionContainerComponent,
+ StreamComponent,
+ CallComponent,
+ CallDialogComponent
+ ],
+ imports: [CommonModule, SharedModule, NgParticlesModule],
+ exports: [ActionBarComponent, InteractionContainerComponent]
+})
+export class InteractionModule {}
diff --git a/client/src/app/core/ui-services/applause.service.spec.ts b/client/src/app/site/interaction/services/applause.service.spec.ts
similarity index 100%
rename from client/src/app/core/ui-services/applause.service.spec.ts
rename to client/src/app/site/interaction/services/applause.service.spec.ts
diff --git a/client/src/app/core/ui-services/applause.service.ts b/client/src/app/site/interaction/services/applause.service.ts
similarity index 63%
rename from client/src/app/core/ui-services/applause.service.ts
rename to client/src/app/site/interaction/services/applause.service.ts
index ebf3a0b03..0d94616c8 100644
--- a/client/src/app/core/ui-services/applause.service.ts
+++ b/client/src/app/site/interaction/services/applause.service.ts
@@ -3,9 +3,9 @@ import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
-import { ConfigService } from './config.service';
-import { HttpService } from '../core-services/http.service';
-import { NotifyService } from '../core-services/notify.service';
+import { ConfigService } from '../../../core/ui-services/config.service';
+import { HttpService } from '../../../core/core-services/http.service';
+import { NotifyService } from '../../../core/core-services/notify.service';
export interface Applause {
level: number;
@@ -21,35 +21,54 @@ const applausePath = '/system/applause';
const applauseNotifyMessageName = 'applause';
@Injectable({
+ // providedIn: InteractionModule
providedIn: 'root'
+ // provided: InteractionModule
})
export class ApplauseService {
private minApplauseLevel: number;
private maxApplauseLevel: number;
private presentApplauseUsers: number;
+ private applauseTypeObservable: Observable;
- public applauseType: ApplauseType;
+ public showApplause: Observable;
+ public showApplauseLevel: boolean;
+ public applauseTimeout: number;
private applauseLevelSubject: Subject = new Subject();
- public applauseLevelObservable = this.applauseLevelSubject.asObservable();
+ public applauseLevelObservable: Observable = this.applauseLevelSubject.asObservable();
private get maxApplause(): number {
return this.maxApplauseLevel || this.presentApplauseUsers || 0;
}
+ public get showParticles(): Observable {
+ return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.particles));
+ }
+
+ public get showBar(): Observable {
+ return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.bar));
+ }
+
public constructor(
configService: ConfigService,
private httpService: HttpService,
private notifyService: NotifyService
) {
+ this.showApplause = configService.get('general_system_applause_enable');
+ this.applauseTypeObservable = configService.get('general_system_applause_type');
+
configService.get('general_system_applause_min_amount').subscribe(minLevel => {
this.minApplauseLevel = minLevel;
});
configService.get('general_system_applause_max_amount').subscribe(maxLevel => {
this.maxApplauseLevel = maxLevel;
});
- configService.get('general_system_applause_type').subscribe((type: ApplauseType) => {
- this.applauseType = type;
+ configService.get('general_system_applause_show_level').subscribe(show => {
+ this.showApplauseLevel = show;
+ });
+ configService.get('general_system_stream_applause_timeout').subscribe(timeout => {
+ this.applauseTimeout = (timeout || 1) * 1000;
});
this.notifyService
.getMessageObservable(applauseNotifyMessageName)
diff --git a/client/src/app/site/interaction/services/call-restriction.service.spec.ts b/client/src/app/site/interaction/services/call-restriction.service.spec.ts
new file mode 100644
index 000000000..dd33f5c03
--- /dev/null
+++ b/client/src/app/site/interaction/services/call-restriction.service.spec.ts
@@ -0,0 +1,18 @@
+import { TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { CallRestrictionService } from './call-restriction.service';
+
+describe('CallRestrictionService', () => {
+ let service: CallRestrictionService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({ imports: [E2EImportsModule] });
+ service = TestBed.inject(CallRestrictionService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/services/call-restriction.service.ts b/client/src/app/site/interaction/services/call-restriction.service.ts
new file mode 100644
index 000000000..7939ee764
--- /dev/null
+++ b/client/src/app/site/interaction/services/call-restriction.service.ts
@@ -0,0 +1,95 @@
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject, Observable, Subject } from 'rxjs';
+import { distinctUntilChanged, map } from 'rxjs/operators';
+
+import { OperatorService, Permission } from 'app/core/core-services/operator.service';
+import { ConfigService } from 'app/core/ui-services/config.service';
+import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
+import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CallRestrictionService {
+ private canManageSpeaker: boolean;
+ private restricted: boolean;
+ private isOnCurrentLos: boolean;
+ private nextSpeakerAmount: number;
+
+ public isAccessRestricted: Observable;
+
+ private canEnterCallSubject = new BehaviorSubject(false);
+ public canEnterCallObservable = this.canEnterCallSubject.asObservable();
+
+ private hasToEnterCallSubject = new Subject();
+ public hasToEnterCallObservable = this.hasToEnterCallSubject.asObservable();
+
+ private hasToLeaveCallSubject = new Subject();
+ public hasToLeaveCallObservable = this.hasToLeaveCallSubject.asObservable();
+
+ public constructor(
+ configService: ConfigService,
+ operator: OperatorService,
+ closService: CurrentListOfSpeakersService
+ ) {
+ /**
+ * general access perm
+ */
+ operator.getUserObservable().subscribe(() => {
+ this.canManageSpeaker = operator.hasPerms(Permission.agendaCanManageListOfSpeakers);
+ this.updateCanEnterCall();
+ });
+
+ /**
+ * LosRestriction
+ */
+ this.isAccessRestricted = configService.get('general_system_conference_los_restriction');
+ this.isAccessRestricted.subscribe(restricted => {
+ this.restricted = restricted;
+ this.updateCanEnterCall();
+ });
+
+ /**
+ * Is User In Clos
+ */
+ closService.currentListOfSpeakersObservable
+ .pipe(
+ map(los => los?.findUserIndexOnList(operator.user.id) ?? -1),
+ distinctUntilChanged()
+ )
+ .subscribe(userLosIndex => {
+ this.isOnCurrentLos = userLosIndex !== UserListIndexType.NotOnList;
+ this.updateCanEnterCall();
+ this.updateAutoJoinJitsiByLosIndex(userLosIndex);
+ });
+
+ /**
+ * Amount of next speakers
+ */
+ configService
+ .get('general_system_conference_auto_connect_next_speakers')
+ .subscribe(nextSpeakerAmount => {
+ this.nextSpeakerAmount = nextSpeakerAmount;
+ });
+ }
+
+ private updateCanEnterCall(): void {
+ this.canEnterCallSubject.next(!this.restricted || this.canManageSpeaker || this.isOnCurrentLos);
+ }
+
+ private updateAutoJoinJitsiByLosIndex(operatorClosIndex: number): void {
+ if (operatorClosIndex !== UserListIndexType.NotOnList) {
+ if (
+ this.nextSpeakerAmount &&
+ this.nextSpeakerAmount > 0 &&
+ operatorClosIndex > UserListIndexType.Active &&
+ operatorClosIndex <= this.nextSpeakerAmount
+ ) {
+ this.hasToEnterCallSubject.next();
+ }
+ } else if (operatorClosIndex === UserListIndexType.NotOnList && this.restricted) {
+ this.hasToLeaveCallSubject.next();
+ }
+ }
+}
diff --git a/client/src/app/site/interaction/services/interaction.service.spec.ts b/client/src/app/site/interaction/services/interaction.service.spec.ts
new file mode 100644
index 000000000..06df66e3c
--- /dev/null
+++ b/client/src/app/site/interaction/services/interaction.service.spec.ts
@@ -0,0 +1,18 @@
+import { TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { InteractionService } from './interaction.service';
+
+describe('InteractionService', () => {
+ let service: InteractionService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({ imports: [E2EImportsModule] });
+ service = TestBed.inject(InteractionService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/services/interaction.service.ts b/client/src/app/site/interaction/services/interaction.service.ts
new file mode 100644
index 000000000..98d0abe33
--- /dev/null
+++ b/client/src/app/site/interaction/services/interaction.service.ts
@@ -0,0 +1,153 @@
+import { Injectable } from '@angular/core';
+
+import { BehaviorSubject, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { ConfigService } from 'app/core/ui-services/config.service';
+import { CallRestrictionService } from './call-restriction.service';
+import { RtcService } from './rtc.service';
+import { StreamService } from './stream.service';
+
+export enum ConferenceState {
+ none,
+ stream,
+ jitsi
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class InteractionService {
+ private conferenceStateSubject = new BehaviorSubject(ConferenceState.none);
+ public conferenceStateObservable = this.conferenceStateSubject.asObservable();
+ public showLiveConfObservable: Observable;
+ private get conferenceState(): ConferenceState {
+ return this.conferenceStateSubject.value;
+ }
+
+ private isJitsiEnabled: boolean;
+ private isInCall: boolean;
+ private isJitsiActive: boolean;
+ private hasLiveStreamUrl: boolean;
+ private canSeeLiveStream: boolean;
+ private showLiveConf: boolean;
+
+ public get isConfStateStream(): Observable {
+ return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.stream));
+ }
+
+ public get isConfStateJitsi(): Observable {
+ return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.jitsi));
+ }
+
+ public get isConfStateNone(): Observable {
+ return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.none));
+ }
+
+ public constructor(
+ private configService: ConfigService,
+ private streamService: StreamService,
+ private rtcService: RtcService,
+ private callRestrictionService: CallRestrictionService
+ ) {
+ this.showLiveConfObservable = this.configService.get('general_system_conference_show');
+
+ /**
+ * If you want to somehow simplify this using rxjs merge-map magic or something
+ * be my guest.
+ */
+ this.streamService.liveStreamUrlObservable.subscribe(url => {
+ this.hasLiveStreamUrl = !!url?.trim() ?? false;
+ this.detectDeadState();
+ });
+
+ this.streamService.canSeeLiveStreamObservable.subscribe(canSee => {
+ this.canSeeLiveStream = canSee;
+ this.detectDeadState();
+ });
+
+ this.rtcService.isJitsiEnabledObservable.subscribe(enabled => {
+ this.isJitsiEnabled = enabled;
+ this.detectDeadState();
+ });
+
+ this.rtcService.isJoinedObservable.subscribe(joined => {
+ this.isInCall = joined;
+ this.detectDeadState();
+ });
+
+ this.rtcService.isJitsiActiveObservable.subscribe(isActive => {
+ this.isJitsiActive = isActive;
+ this.detectDeadState();
+ });
+
+ this.callRestrictionService.hasToEnterCallObservable.subscribe(() => {
+ if (!this.isInCall) {
+ this.enterCall();
+ this.rtcService.enterConferenceRoom();
+ }
+ });
+
+ this.callRestrictionService.hasToLeaveCallObservable.subscribe(() => {
+ this.viewStream();
+ });
+
+ this.showLiveConfObservable.subscribe(showConf => {
+ this.showLiveConf = showConf;
+ this.detectDeadState();
+ });
+
+ this.detectDeadState();
+ }
+
+ public async enterCall(): Promise {
+ if (this.conferenceState !== ConferenceState.jitsi) {
+ this.setConferenceState(ConferenceState.jitsi);
+ }
+ }
+
+ public viewStream(): void {
+ if (this.conferenceState !== ConferenceState.stream) {
+ this.setConferenceState(ConferenceState.stream);
+ }
+ }
+
+ private setConferenceState(newState: ConferenceState): void {
+ if (newState !== this.conferenceState) {
+ this.conferenceStateSubject.next(newState);
+ }
+ }
+
+ /**
+ * this is the "dead" state; you would see the jitsi state; but are not connected
+ * or the connection is prohibited. If this occurs and a live stream
+ * becomes available, switch to the stream state
+ */
+ private detectDeadState(): void {
+ if (
+ this.isInCall === undefined ||
+ this.isJitsiActive === undefined ||
+ this.hasLiveStreamUrl === undefined ||
+ this.conferenceState === undefined ||
+ this.canSeeLiveStream === undefined ||
+ this.isJitsiEnabled === undefined
+ ) {
+ return;
+ }
+
+ /**
+ * most importantly, if there is a call, to not change the state!
+ */
+ if (this.isInCall || this.isJitsiActive) {
+ return;
+ }
+
+ if (this.hasLiveStreamUrl && this.canSeeLiveStream) {
+ this.viewStream();
+ } else if (this.showLiveConf && (!this.hasLiveStreamUrl || !this.canSeeLiveStream) && this.isJitsiEnabled) {
+ this.enterCall();
+ } else {
+ this.setConferenceState(ConferenceState.none);
+ }
+ }
+}
diff --git a/client/src/app/site/interaction/services/rtc.service.spec.ts b/client/src/app/site/interaction/services/rtc.service.spec.ts
new file mode 100644
index 000000000..63b0f8b4b
--- /dev/null
+++ b/client/src/app/site/interaction/services/rtc.service.spec.ts
@@ -0,0 +1,18 @@
+import { TestBed } from '@angular/core/testing';
+
+import { E2EImportsModule } from 'e2e-imports.module';
+
+import { RtcService } from './rtc.service';
+
+describe('RtcService', () => {
+ let service: RtcService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({ imports: [E2EImportsModule] });
+ service = TestBed.inject(RtcService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/client/src/app/site/interaction/services/rtc.service.ts b/client/src/app/site/interaction/services/rtc.service.ts
new file mode 100644
index 000000000..cf7994447
--- /dev/null
+++ b/client/src/app/site/interaction/services/rtc.service.ts
@@ -0,0 +1,397 @@
+import { ElementRef, Injectable } from '@angular/core';
+
+import { StorageMap } from '@ngx-pwa/local-storage';
+import { BehaviorSubject, Observable, Subject } from 'rxjs';
+import { distinctUntilChanged } from 'rxjs/operators';
+
+import { ConstantsService } from 'app/core/core-services/constants.service';
+import { OperatorService } from 'app/core/core-services/operator.service';
+import { ConfigService } from 'app/core/ui-services/config.service';
+import { CallRestrictionService } from './call-restriction.service';
+import { UserMediaPermService } from './user-media-perm.service';
+
+export const RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
+
+interface JitsiMember {
+ id: string;
+ displayName: string;
+}
+
+interface ConferenceJoinedResult {
+ roomName: string;
+ id: string;
+ displayName: string;
+ formattedDisplayName: string;
+}
+
+interface ConferenceMember {
+ name: string;
+ focus: boolean;
+}
+
+interface DisplayNameChangeResult {
+ // Yes, in this case "displayname" really does not have a capital n. Thank you jitsi.
+ displayname: string;
+ formattedDisplayName: string;
+ id: string;
+}
+
+interface MemberKicked {
+ kicked: {
+ id: string;
+ local: boolean;
+ };
+ kicker: {
+ id: string;
+ };
+}
+
+/**
+ * Jitsi
+ */
+declare var JitsiMeetExternalAPI: any;
+
+const configOverwrite = {
+ startAudioOnly: false,
+ // allows jitsi on mobile devices
+ disableDeepLinking: true,
+ startWithAudioMuted: true,
+ startWithVideoMuted: true,
+ useNicks: true,
+ enableWelcomePage: false,
+ enableUserRolesBasedOnToken: false,
+ enableFeaturesBasedOnToken: false,
+ disableThirdPartyRequests: true,
+ enableNoAudioDetection: false,
+ enableNoisyMicDetection: false
+};
+
+const interfaceConfigOverwrite = {
+ DISABLE_VIDEO_BACKGROUND: true,
+ INVITATION_POWERED_BY: false,
+ DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
+ DISABLE_PRESENCE_STATUS: true,
+ TOOLBAR_ALWAYS_VISIBLE: true,
+ TOOLBAR_TIMEOUT: 10000000,
+ TOOLBAR_BUTTONS: [
+ 'microphone',
+ 'camera',
+ 'desktop',
+ 'fullscreen',
+ 'fodeviceselection',
+ 'profile',
+ 'chat',
+ 'recording',
+ 'livestreaming',
+ 'etherpad',
+ 'sharedvideo',
+ 'settings',
+ 'videoquality',
+ 'filmstrip',
+ 'feedback',
+ 'stats',
+ 'shortcuts',
+ 'tileview',
+ 'download',
+ 'help',
+ 'mute-everyone',
+ 'hangup'
+ ]
+};
+
+export interface JitsiConfig {
+ JITSI_DOMAIN: string;
+ JITSI_ROOM_NAME: string;
+ JITSI_ROOM_PASSWORD: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RtcService {
+ private jitsiConfig: JitsiConfig;
+ private isJitsiEnabledSubject = new BehaviorSubject(false);
+ public isJitsiEnabledObservable = this.isJitsiEnabledSubject.asObservable();
+
+ public autoConnect: Observable;
+
+ // JitsiMeet api object
+ private api: any | null;
+ private get isJitsiActive(): boolean {
+ return !!this.api;
+ }
+
+ private options: Object;
+ private jitsiNode: ElementRef;
+
+ private actualRoomName: string;
+
+ public isSupportEnabled: Observable;
+ private connectedToHelpDeskSubject = new BehaviorSubject(false);
+ public connectedToHelpDesk = this.connectedToHelpDeskSubject.asObservable();
+
+ public isJitsiActiveInAnotherTab = false;
+ private isJoinedSubject = new BehaviorSubject(false);
+ public isJoinedObservable = this.isJoinedSubject.asObservable();
+
+ private isPasswordSet = false;
+
+ private isJitsiActiveSubject = new BehaviorSubject(false);
+ public isJitsiActiveObservable = this.isJitsiActiveSubject.asObservable();
+
+ private get defaultRoomName(): string {
+ return this.jitsiConfig?.JITSI_ROOM_NAME;
+ }
+
+ private jitsiMeetUrlSubject = new Subject();
+ public jitsiMeetUrl = this.jitsiMeetUrlSubject.asObservable();
+
+ private members = {};
+ private memberSubject = new BehaviorSubject
-
+
+
diff --git a/client/src/app/site/site.component.scss b/client/src/app/site/site.component.scss
index 6aee28f9a..15306f4ac 100644
--- a/client/src/app/site/site.component.scss
+++ b/client/src/app/site/site.component.scss
@@ -136,9 +136,16 @@ mat-sidenav-container {
.toolbars {
display: flex;
-
+ position: fixed;
+ bottom: 0;
+ /**
+ * right 0 would overlap the browser scrollbar
+ */
+ right: 20px;
+ z-index: 99;
+ pointer-events: none;
* {
- margin-top: auto;
+ pointer-events: initial;
}
}
diff --git a/client/src/app/site/site.module.ts b/client/src/app/site/site.module.ts
index 289add0f5..166b961d0 100644
--- a/client/src/app/site/site.module.ts
+++ b/client/src/app/site/site.module.ts
@@ -2,11 +2,12 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from 'app/shared/shared.module';
+import { InteractionModule } from './interaction/interaction.module';
import { SiteRoutingModule } from './site-routing.module';
import { SiteComponent } from './site.component';
@NgModule({
- imports: [CommonModule, SharedModule, SiteRoutingModule],
+ imports: [CommonModule, SharedModule, SiteRoutingModule, InteractionModule],
declarations: [SiteComponent]
})
export class SiteModule {}
diff --git a/client/src/assets/styles/component-themes.scss b/client/src/assets/styles/component-themes.scss
index 1812af7ae..35293fbae 100644
--- a/client/src/assets/styles/component-themes.scss
+++ b/client/src/assets/styles/component-themes.scss
@@ -42,7 +42,6 @@ $narrow-spacing: (
@import '~app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
@import '~app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
@import '~app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
-@import '~app/shared/components/jitsi/jitsi.component.scss-theme.scss';
@import '~app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
@import '~app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss';
@import '~app/site/login/components/login-wrapper/login-wrapper.component.scss-theme.scss';
@@ -69,7 +68,6 @@ $narrow-spacing: (
@include os-motion-poll-detail-style($theme);
@include os-assignment-poll-detail-style($theme);
@include os-progress-snack-bar-style($theme);
- @include os-jitsi-theme($theme);
@include os-list-view-table-theme($theme);
@include os-user-statistics-style($theme);
@include os-login-wrapper-theme($theme);
diff --git a/client/src/assets/styles/global-components-style.scss b/client/src/assets/styles/global-components-style.scss
index e2a0904b6..0ff7a9a60 100644
--- a/client/src/assets/styles/global-components-style.scss
+++ b/client/src/assets/styles/global-components-style.scss
@@ -35,10 +35,32 @@
color: mat-color(if($is-dark-theme, $accent, $primary));
}
- //custom table header for search button, filtering and more. Used in ListViews
+ /**
+ * normal current mat bg color with primary text color.
+ * important is required to overwrite materials default
+ * button color
+ */
.custom-table-header,
- .background--default {
- background: mat-color($background, background);
+ .background-default {
+ background: mat-color($background, background) !important;
+ }
+
+ .fake-disabled {
+ background: mat-color($background, unselected-chip) !important;
+ opacity: 1 !important;
+
+ .mat-button-wrapper {
+ .mat-icon {
+ color: mat-color($foreground, disabled-button) !important;
+ svg path {
+ fill: mat-color($foreground, disabled-button) !important;
+ }
+ }
+ }
+ }
+
+ .background-default[disabled] {
+ @extend .fake-disabled;
}
.underline {
@@ -192,6 +214,11 @@
color: mat-color($primary, default-contrast) !important;
}
+ .background-card {
+ background: mat-color($background, card);
+ color: mat-color($foreground, text);
+ }
+
.primary-foreground {
color: mat-color($primary);
}
@@ -199,4 +226,10 @@
.accent-foreground {
color: mat-color($accent);
}
+
+ .svg-primary {
+ svg path {
+ fill: mat-color($primary) !important;
+ }
+ }
}
diff --git a/server/openslides/core/config_variables.py b/server/openslides/core/config_variables.py
index 58bad5d61..08f7a5a62 100644
--- a/server/openslides/core/config_variables.py
+++ b/server/openslides/core/config_variables.py
@@ -164,7 +164,7 @@ def get_config_variables():
name="general_system_stream_poster",
default_value="",
label="Livestream poster image url",
- help_text="Shows if livestream is not started. Recommended image format: 500x200px, PNG or JPG",
+ help_text="Shows if livestream is not started. Recommended image format: 500x280px, PNG or JPG",
weight=147,
subgroup="Live conference",
)