Restructure communication components
separates the "Jitsi component" into an own module, several services and components.
This commit is contained in:
parent
e0da18a0e6
commit
1504e33607
@ -256,7 +256,7 @@ export class DiffService {
|
||||
* @returns {Element}
|
||||
*/
|
||||
public getLineNumberNode(fragment: DocumentFragment, lineNumber: number): Element {
|
||||
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
||||
return fragment?.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -16,6 +16,22 @@ const slideOut = [
|
||||
)
|
||||
];
|
||||
|
||||
export const fadeInOut = trigger('fadeInOut', [
|
||||
state(
|
||||
'true',
|
||||
style({
|
||||
opacity: 1
|
||||
})
|
||||
),
|
||||
state(
|
||||
'false',
|
||||
style({
|
||||
opacity: 0.2
|
||||
})
|
||||
),
|
||||
transition('true <=> false', animate('1s'))
|
||||
]);
|
||||
|
||||
export const collapseAndFade = trigger('collapse', [
|
||||
state('in', style({ opacity: 1, height: '100%' })),
|
||||
transition(':enter', [style({ opacity: 0, height: 0 }), animate(fadeSpeed.fast)]),
|
||||
|
@ -1,295 +0,0 @@
|
||||
<!-- iFrame Dialog -->
|
||||
<div class="jitsi-fake-dialog-wrapper" [ngClass]="{ 'jitsi-dialog-hide': !isJitsiDialogOpen }">
|
||||
<mat-card class="jitsi-fake-dialog">
|
||||
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
||||
<div class="jitsi-dialog-actions">
|
||||
<div>
|
||||
<a
|
||||
type="button"
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||
target="_blank"
|
||||
(click)="stopJitsi()"
|
||||
[href]="jitsiMeetUrl"
|
||||
>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="dialog-hangup"
|
||||
type="button"
|
||||
mat-button
|
||||
color="warn"
|
||||
(click)="!!videoStreamUrl ? viewStream() : stopJitsi()"
|
||||
>
|
||||
<os-icon-container icon="{{ !!videoStreamUrl ? 'meeting_room' : 'call_end' }}">
|
||||
<span *ngIf="videoStreamUrl">
|
||||
{{ 'Exit conference' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!videoStreamUrl">
|
||||
{{ 'Hang up' | translate }}
|
||||
</span>
|
||||
</os-icon-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="dialog-hide" type="button" mat-button color="primary" (click)="hideJitsiDialog()">
|
||||
<os-icon-container icon="aspect_ratio">
|
||||
<span>{{ 'Minimize' | translate }}</span>
|
||||
</os-icon-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="jitsi-integration" *ngIf="showConferenceBar">
|
||||
<!-- Audio-Conference-bar -->
|
||||
<div class="jitsi-bar">
|
||||
<span class="control-icon-wrapper apply-theme">
|
||||
<ng-container *ngIf="currentState == state.jitsi">
|
||||
<!-- Exit jitsi -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
class="indicator quick-icon"
|
||||
color="accent"
|
||||
(click)="viewStream()"
|
||||
matTooltip="{{ 'Exit live conference and continue livestream' | translate }}"
|
||||
*ngIf="videoStreamUrl && canSeeLiveStream && !isJitsiDialogOpen"
|
||||
>
|
||||
<mat-icon color="warn">meeting_room</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- mute/unmute button -->
|
||||
<button
|
||||
class="indicator quick-icon"
|
||||
mat-mini-fab
|
||||
*ngIf="isJoined && !isJitsiDialogOpen"
|
||||
(click)="toggleMute()"
|
||||
matTooltip="{{ 'Mute / Unmute' | translate }}"
|
||||
>
|
||||
<mat-icon color="{{ muted ? 'primary' : 'warn' }}">{{ muted ? 'moff' : 'mic' }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- disconnected icon -->
|
||||
<mat-icon class="indicator" *ngIf="!isJoined && !videoStreamUrl">cloud_off</mat-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState == state.stream">
|
||||
<!-- Enter conference from stream -->
|
||||
<button
|
||||
*ngIf="enableJitsi && isAccessPermitted"
|
||||
class="quick-icon indicator"
|
||||
mat-mini-fab
|
||||
(click)="enterConferenceRoom()"
|
||||
matTooltip="{{ 'Enter live conference' | translate }}"
|
||||
>
|
||||
<mat-icon
|
||||
color="primary"
|
||||
[@fadeInOut]="isEnterMeetingRoomVisible"
|
||||
(@fadeInOut.done)="triggerMeetingRoomButtonAnimation()"
|
||||
>
|
||||
meeting_room
|
||||
</mat-icon>
|
||||
</button>
|
||||
|
||||
<a
|
||||
class="indicator"
|
||||
type="button"
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Add yourself to the current list of speakers to join the conference' | translate }}"
|
||||
*ngIf="enableJitsi && !isAccessPermitted"
|
||||
[routerLink]="['/agenda/speakers']"
|
||||
>
|
||||
<mat-icon> no_meeting_room </mat-icon>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<!-- Call support button -->
|
||||
<button
|
||||
class="indicator quick-icon"
|
||||
mat-mini-fab
|
||||
(click)="enterSupportRoom()"
|
||||
[disabled]="isJitsiActive"
|
||||
matTooltip="{{ 'Help desk' | translate }}"
|
||||
*ngIf="canAccessSupport"
|
||||
>
|
||||
<mat-icon color="primary">live_help</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- applause button -->
|
||||
<button
|
||||
class="quick-icon indicator"
|
||||
[disabled]="applauseDisabled"
|
||||
mat-mini-fab
|
||||
(click)="sendApplause()"
|
||||
matTooltip="{{ 'Give applause' | translate }}"
|
||||
*ngIf="showApplause"
|
||||
[matBadge]="showApplauseBadge ? applauseLevel : null"
|
||||
matBadgeColor="accent"
|
||||
>
|
||||
<mat-icon svgIcon="clapping_hands"></mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="list-wrapper apply-theme"
|
||||
[ngClass]="{
|
||||
'stream-width-wrapper': currentState == state.stream,
|
||||
'audio-list-wrapper': currentState == state.jitsi,
|
||||
'cast-shadow': showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<os-applause-bar-display *ngIf="showApplause && isApplauseTypeBar" class="applause"></os-applause-bar-display>
|
||||
|
||||
<!-- open-window button -->
|
||||
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
|
||||
<ng-container *ngIf="currentState == state.jitsi">
|
||||
<div *ngIf="!connectToHelpDesk" class="ellipsis-overflow">{{ 'Live conference' | translate }}</div>
|
||||
<div *ngIf="connectToHelpDesk" class="ellipsis-overflow">{{ 'Help desk' | translate }}</div>
|
||||
<div class="one-line">
|
||||
|
||||
<span *ngIf="currentDominantSpeaker">
|
||||
» <span class="dominant-speaker">{{ currentDominantSpeaker.displayName }}</span>
|
||||
</span>
|
||||
<span *ngIf="!isJitsiActive">
|
||||
<i>{{ 'disconnected' | translate }}</i>
|
||||
</span>
|
||||
<span *ngIf="isJitsiActive && !isJoined">
|
||||
<i>{{ 'connecting ...' | translate }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState == state.stream">
|
||||
<!-- os-icon-container does weird things here -->
|
||||
<div class="ellipsis-overflow">{{ 'Livestream' | translate }}</div>
|
||||
</ng-container>
|
||||
|
||||
<mat-icon class="opened-indicator" *ngIf="!showJitsiWindow">keyboard_arrow_up</mat-icon>
|
||||
<mat-icon class="opened-indicator" *ngIf="showJitsiWindow">keyboard_arrow_down </mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- unfolded list -->
|
||||
<div
|
||||
class="jitsi-list"
|
||||
[ngClass]="{
|
||||
'hide-height': !showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<ng-container *ngIf="currentState == state.jitsi">
|
||||
<!-- Jitsi content window -->
|
||||
<div class="content">
|
||||
<!-- The "somewhere else active" warning -->
|
||||
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||
<span>{{
|
||||
'The live conference is already running in your OpenSlides session.' | translate
|
||||
}}</span>
|
||||
<button mat-button color="warn" (click)="forceStart()">
|
||||
<span>{{ 'Reenter to live conference' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||
<span>{{ 'disconnected' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="disconnected" *ngIf="isJitsiActive && !isJoined">
|
||||
<span>{{ 'connecting ...' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- user list -->
|
||||
<div class="room-members" *ngIf="isJitsiActive && isJoined">
|
||||
<os-applause-particle-display
|
||||
*ngIf="isApplauseTypeParticles"
|
||||
class="room-list-applause-particles"
|
||||
></os-applause-particle-display>
|
||||
<div class="member-list">
|
||||
<ol>
|
||||
<li
|
||||
*ngFor="let memberId of memberList; trackBy: trackByIndex"
|
||||
[ngClass]="{
|
||||
focused: members[memberId].focus
|
||||
}"
|
||||
>
|
||||
<div class="member-list-entry">
|
||||
{{ members[memberId].name }}
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState == state.stream">
|
||||
<ng-container *ngIf="showVideoPlayer()">
|
||||
<os-video-player
|
||||
[videoUrl]="videoStreamUrl"
|
||||
[showParticles]="isApplauseTypeParticles"
|
||||
(started)="onSteamLoaded()"
|
||||
></os-video-player>
|
||||
</ng-container>
|
||||
<div class="disconnected" *ngIf="isStreamInOtherTab()">
|
||||
<button class="restart-stream-button" mat-button color="warn" (click)="deleteStreamingLock()">
|
||||
<span>{{ 'Restart livestream' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState == state.jitsi">
|
||||
<!-- Custom control buttons -->
|
||||
<div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="control-grid">
|
||||
<div class="control-buttons">
|
||||
<!-- Hangup -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="warn"
|
||||
(click)="stopJitsi()"
|
||||
*ngIf="isJitsiActive && isJoined"
|
||||
matTooltip="{{ 'Leave' | translate }}"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Enter jitsi manually -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="accent"
|
||||
(click)="enterConferenceRoom()"
|
||||
[disabled]="
|
||||
!enableJitsi || isJitsiActive || isJitsiActiveInAnotherTab || !isAccessPermitted
|
||||
"
|
||||
*ngIf="!isJoined"
|
||||
matTooltip="{{ 'Enter conference' | translate }}"
|
||||
>
|
||||
<mat-icon>call</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- open dialog -->
|
||||
<button
|
||||
mat-icon-button
|
||||
class="open-jitsi-in-tab"
|
||||
color="accent"
|
||||
(click)="toggleConferenceDialog()"
|
||||
[disabled]="!isJitsiActive"
|
||||
matTooltip="{{ 'Show/Hide video conference' | translate }}"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ isJitsiDialogOpen ? 'aspect_ratio' : 'voice_chat' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
@ -1,200 +0,0 @@
|
||||
.jitsi-fake-dialog-wrapper {
|
||||
z-index: 98;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 20px 10% 20px 5%;
|
||||
|
||||
.jitsi-fake-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
|
||||
.jitsi-iframe-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.jitsi-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
min-width: 33%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dialog-hangup {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.dialog-hide {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-dialog-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jitsi-integration {
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
|
||||
.cast-shadow {
|
||||
box-shadow: -3px -3px 10px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.jitsi-bar {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: flex-end;
|
||||
$wrapper-padding: 5px;
|
||||
$bar-height: 40px;
|
||||
|
||||
.control-icon-wrapper {
|
||||
pointer-events: all;
|
||||
z-index: 1;
|
||||
min-height: $bar-height;
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
padding-right: 0.5em;
|
||||
padding: $wrapper-padding 0 $wrapper-padding $wrapper-padding;
|
||||
border-top-left-radius: 4px;
|
||||
|
||||
.indicator {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
margin: auto $wrapper-padding auto 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stream-width-wrapper {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.audio-list-wrapper {
|
||||
width: 100%;
|
||||
min-width: 100px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
position: relative;
|
||||
pointer-events: all;
|
||||
min-height: $bar-height;
|
||||
padding-top: $wrapper-padding;
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
.applause {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 90px;
|
||||
left: -90px;
|
||||
bottom: 50px;
|
||||
}
|
||||
|
||||
.toggle-list-button {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 2.5em;
|
||||
margin-bottom: $wrapper-padding;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
|
||||
.opened-indicator {
|
||||
position: absolute;
|
||||
right: $wrapper-padding;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.dominant-speaker {
|
||||
font-weight: 500;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-list {
|
||||
.content {
|
||||
height: 40vh;
|
||||
max-height: 100%;
|
||||
clear: both;
|
||||
|
||||
.disconnected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: inherit;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
|
||||
span {
|
||||
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 0;
|
||||
display: grid;
|
||||
grid-template-areas: 'exit buttons new-tab';
|
||||
grid-template-columns: 40px auto 40px;
|
||||
|
||||
.exit-conference {
|
||||
grid-area: exit;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
grid-area: buttons;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.open-jitsi-in-tab {
|
||||
grid-area: new-tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-jitsi-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.jitsi-bar {
|
||||
.apply-theme {
|
||||
background-color: mat-color($primary);
|
||||
|
||||
.quick-icon:not([disabled]) {
|
||||
background-color: mat-color($primary, default-contrast);
|
||||
}
|
||||
|
||||
.indicator {
|
||||
color: mat-color($primary, default-contrast);
|
||||
|
||||
svg path {
|
||||
fill: mat-color($primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-list-button {
|
||||
span {
|
||||
color: mat-color($primary, default-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-list {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
}
|
@ -1,742 +0,0 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { delay, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
|
||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||
import { Deferred } from 'app/core/promises/deferred';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { UserMediaPermService } from 'app/core/ui-services/user-media-perm.service';
|
||||
import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
|
||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
|
||||
|
||||
declare var JitsiMeetExternalAPI: any;
|
||||
|
||||
interface JitsiMember {
|
||||
id: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface ConferenceJoinedResult {
|
||||
roomName: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
formattedDisplayName: string;
|
||||
}
|
||||
|
||||
interface DisplayNameChangeResult {
|
||||
// Yes, in this case "displayname" really does not have a capital n. Thank you jitsi.
|
||||
displayname: string;
|
||||
formattedDisplayName: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface JitsiSettings {
|
||||
JITSI_DOMAIN: string;
|
||||
JITSI_ROOM_NAME: string;
|
||||
JITSI_ROOM_PASSWORD: string;
|
||||
}
|
||||
|
||||
interface ConferenceMember {
|
||||
name: string;
|
||||
focus: boolean;
|
||||
}
|
||||
|
||||
enum ConferenceState {
|
||||
stream,
|
||||
jitsi
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'os-jitsi',
|
||||
templateUrl: './jitsi.component.html',
|
||||
styleUrls: ['./jitsi.component.scss'],
|
||||
animations: [
|
||||
trigger('fadeInOut', [
|
||||
state(
|
||||
'true',
|
||||
style({
|
||||
opacity: 1
|
||||
})
|
||||
),
|
||||
state(
|
||||
'false',
|
||||
style({
|
||||
opacity: 0.2
|
||||
})
|
||||
),
|
||||
transition('true <=> false', animate('1s'))
|
||||
])
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class JitsiComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
|
||||
public enableJitsi: boolean;
|
||||
|
||||
private autoconnect: boolean;
|
||||
private defaultRoomName: string;
|
||||
private actualRoomName: string;
|
||||
private roomPassword: string;
|
||||
private jitsiDomain: string;
|
||||
private isSupportEnabled: boolean;
|
||||
|
||||
public connectToHelpDesk = false;
|
||||
|
||||
public restricted = false;
|
||||
public videoStreamUrl: string;
|
||||
private nextSpeakerAmount: number;
|
||||
|
||||
// do not set the password twice
|
||||
private isPasswortSet = false;
|
||||
|
||||
public isJitsiDialogOpen = false;
|
||||
public showJitsiWindow = true;
|
||||
public muted = true;
|
||||
|
||||
public showApplause: boolean;
|
||||
public applauseDisabled = false;
|
||||
private applauseTimeout: number;
|
||||
|
||||
@ViewChild('jitsi')
|
||||
private jitsiNode: ElementRef;
|
||||
|
||||
// JitsiMeet api object
|
||||
private api: any | null;
|
||||
|
||||
public get isJitsiActive(): boolean {
|
||||
return !!this.api;
|
||||
}
|
||||
|
||||
public isJoined: boolean;
|
||||
private streamRunning = false;
|
||||
|
||||
private options: object;
|
||||
|
||||
private lockLoaded: Deferred<void> = new Deferred();
|
||||
private constantsLoaded: Deferred<void> = new Deferred();
|
||||
private configsLoaded: Deferred<void> = new Deferred();
|
||||
|
||||
// storage locks
|
||||
public isJitsiActiveInAnotherTab = false;
|
||||
|
||||
/**
|
||||
* undefined is controlled behaviour, meaning, this property was not
|
||||
* checked yet.
|
||||
* Thus, false-checks have to be explicit
|
||||
*/
|
||||
private streamLoadedOnce: boolean;
|
||||
|
||||
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
||||
|
||||
// JitsiID to ConferenceMember
|
||||
public members = {};
|
||||
public currentDominantSpeaker: JitsiMember;
|
||||
|
||||
public get memberList(): string[] {
|
||||
return Object.keys(this.members);
|
||||
}
|
||||
|
||||
public get isRoomPasswordProtected(): boolean {
|
||||
return this.roomPassword?.length > 0;
|
||||
}
|
||||
|
||||
public get canAccessSupport(): boolean {
|
||||
return this.isSupportEnabled && this.enableJitsi && !!this.defaultRoomName;
|
||||
}
|
||||
|
||||
private isOnCurrentLos: boolean;
|
||||
|
||||
public canSeeLiveStream: boolean;
|
||||
|
||||
public canManageSpeaker: boolean;
|
||||
|
||||
/**
|
||||
* Jitsi|URL|Perm||Show
|
||||
* =====|===|====||====
|
||||
* 0 | 0 | 0 || 0
|
||||
* 0 | 0 | 1 || 0
|
||||
* 0 | 1 | 0 || 0
|
||||
* 0 | 1 | 1 || 1
|
||||
* 1 | 0 | 0 || 1
|
||||
* 1 | 0 | 1 || 1
|
||||
* 1 | 1 | 0 || 0
|
||||
* 1 | 1 | 1 || 1
|
||||
*/
|
||||
public get showConferenceBar(): boolean {
|
||||
if (this.enableJitsi) {
|
||||
if (this.videoStreamUrl && !this.canSeeLiveStream) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return this.videoStreamUrl && this.canSeeLiveStream;
|
||||
}
|
||||
}
|
||||
|
||||
public get isAccessPermitted(): boolean {
|
||||
return !this.restricted || this.canManageSpeaker || this.isOnCurrentLos;
|
||||
}
|
||||
|
||||
public get jitsiMeetUrl(): string {
|
||||
return `https://${this.jitsiDomain}/${this.actualRoomName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The conference state, to determine if the user consumes the stream or can
|
||||
* contribute to jitsi
|
||||
*/
|
||||
public state = ConferenceState;
|
||||
public currentState: ConferenceState;
|
||||
public isEnterMeetingRoomVisible = true;
|
||||
|
||||
public applauseLevel = 0;
|
||||
private showApplauseLevel: boolean;
|
||||
private isApplausBarUsed: boolean;
|
||||
|
||||
public get showApplauseBadge(): boolean {
|
||||
return this.showApplauseLevel && this.applauseLevel > 0 && (!this.showJitsiWindow || !this.isApplausBarUsed);
|
||||
}
|
||||
|
||||
private get applauseType(): ApplauseType {
|
||||
return this.applauseService.applauseType;
|
||||
}
|
||||
|
||||
public get isApplauseTypeBar(): boolean {
|
||||
return this.applauseType === ApplauseType.bar;
|
||||
}
|
||||
|
||||
public get isApplauseTypeParticles(): boolean {
|
||||
return this.applauseType === ApplauseType.particles;
|
||||
}
|
||||
|
||||
private configOverwrite = {
|
||||
startAudioOnly: false,
|
||||
// allows jitsi on mobile devices
|
||||
disableDeepLinking: true,
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false,
|
||||
useNicks: true,
|
||||
enableWelcomePage: false,
|
||||
enableUserRolesBasedOnToken: false,
|
||||
enableFeaturesBasedOnToken: false,
|
||||
disableThirdPartyRequests: true,
|
||||
enableNoAudioDetection: false,
|
||||
enableNoisyMicDetection: false
|
||||
};
|
||||
|
||||
private 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'
|
||||
]
|
||||
};
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
snackBar: MatSnackBar,
|
||||
private operator: OperatorService,
|
||||
private storageMap: StorageMap,
|
||||
private userRepo: UserRepositoryService,
|
||||
private constantsService: ConstantsService,
|
||||
private configService: ConfigService,
|
||||
private closService: CurrentListOfSpeakersService,
|
||||
private userMediaPermService: UserMediaPermService,
|
||||
private applauseService: ApplauseService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(titleService, translate, snackBar);
|
||||
}
|
||||
|
||||
public async ngOnInit(): Promise<void> {
|
||||
await this.setUp();
|
||||
if (this.canSeeLiveStream && this.videoStreamUrl) {
|
||||
this.currentState = ConferenceState.stream;
|
||||
} else {
|
||||
this.currentState = ConferenceState.jitsi;
|
||||
}
|
||||
}
|
||||
|
||||
public async ngOnDestroy(): Promise<void> {
|
||||
super.ngOnDestroy();
|
||||
this.stopConference();
|
||||
}
|
||||
|
||||
// closing the tab should also try to stop jitsi.
|
||||
// this will usually not be cought by ngOnDestroy
|
||||
@HostListener('window:beforeunload', ['$event'])
|
||||
public async beforeunload($event: any): Promise<void> {
|
||||
await this.stopConference();
|
||||
}
|
||||
|
||||
public triggerMeetingRoomButtonAnimation(): void {
|
||||
if (this.canManageSpeaker) {
|
||||
this.isEnterMeetingRoomVisible = true;
|
||||
} else {
|
||||
this.isEnterMeetingRoomVisible = !this.isEnterMeetingRoomVisible;
|
||||
}
|
||||
}
|
||||
|
||||
private async stopConference(): Promise<void> {
|
||||
await this.stopJitsi();
|
||||
if (this.streamLoadedOnce && this.streamRunning) {
|
||||
await this.deleteStreamingLock();
|
||||
}
|
||||
}
|
||||
|
||||
private async setUp(): Promise<void> {
|
||||
this.subscriptions.push(
|
||||
// if the operators users has changes, check if we have to start the animation
|
||||
this.operator
|
||||
.getUserObservable()
|
||||
.pipe(delay(0))
|
||||
.subscribe(() => {
|
||||
this.canManageSpeaker = this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
||||
this.canSeeLiveStream = this.operator.hasPerms(this.permission.coreCanSeeLiveStream);
|
||||
this.isEnterMeetingRoomVisible = this.canManageSpeaker;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
|
||||
this.storageMap
|
||||
.watch(this.RTC_LOGGED_STORAGE_KEY)
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((inUse: boolean) => {
|
||||
this.isJitsiActiveInAnotherTab = inUse;
|
||||
this.lockLoaded.resolve();
|
||||
if (!inUse && !this.isJitsiActive) {
|
||||
this.startJitsi();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}),
|
||||
this.storageMap
|
||||
.watch(this.STREAM_RUNNING_STORAGE_KEY)
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((running: boolean) => {
|
||||
this.streamLoadedOnce = !!running;
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
|
||||
await this.lockLoaded;
|
||||
|
||||
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
||||
if (settings) {
|
||||
this.jitsiDomain = settings.JITSI_DOMAIN;
|
||||
this.defaultRoomName = settings.JITSI_ROOM_NAME;
|
||||
this.roomPassword = settings.JITSI_ROOM_PASSWORD;
|
||||
this.constantsLoaded.resolve();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
await this.constantsLoaded;
|
||||
|
||||
this.subscriptions.push(
|
||||
this.configService.get<boolean>('general_system_conference_auto_connect').subscribe(autoconnect => {
|
||||
this.autoconnect = autoconnect;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_conference_show').subscribe(show => {
|
||||
this.enableJitsi = show && !!this.jitsiDomain && !!this.defaultRoomName;
|
||||
if (this.enableJitsi && this.autoconnect) {
|
||||
this.startJitsi();
|
||||
} else {
|
||||
this.stopJitsi();
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_conference_los_restriction').subscribe(restricted => {
|
||||
this.restricted = restricted;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService
|
||||
.get<number>('general_system_conference_auto_connect_next_speakers')
|
||||
.subscribe(nextSpeakerAmount => {
|
||||
this.nextSpeakerAmount = nextSpeakerAmount;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<string>('general_system_stream_url').subscribe(url => {
|
||||
this.onLiveStreamAvailable(url);
|
||||
this.configsLoaded.resolve();
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_conference_open_microphone').subscribe(open => {
|
||||
this.configOverwrite.startWithAudioMuted = !open;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
|
||||
this.configOverwrite.startWithVideoMuted = !open;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_applause_enable').subscribe(enable => {
|
||||
this.showApplause = enable;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<number>('general_system_stream_applause_timeout').subscribe(timeout => {
|
||||
this.applauseTimeout = (timeout || 1) * 1000;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
|
||||
this.showApplauseLevel = show;
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<any>('general_system_applause_type').subscribe(type => {
|
||||
if (type === 'applause-type-bar') {
|
||||
this.isApplausBarUsed = true;
|
||||
} else {
|
||||
this.isApplausBarUsed = false;
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.configService.get<boolean>('general_system_conference_enable_helpdesk').subscribe(enabled => {
|
||||
this.isSupportEnabled = enabled;
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
|
||||
await this.configsLoaded;
|
||||
|
||||
this.subscriptions.push(
|
||||
// check if the operator is on the clos, remove from room if not permitted
|
||||
this.closService.currentListOfSpeakersObservable
|
||||
.pipe(
|
||||
map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(userLosIndex => {
|
||||
this.autoJoinJitsiByLosIndex(userLosIndex);
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
|
||||
this.applauseLevel = applauseLevel || 0;
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public toggleMute(): void {
|
||||
if (this.isJitsiActive) {
|
||||
this.api.executeCommand('toggleAudio');
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
public async forceStart(): Promise<void> {
|
||||
await this.deleteJitsiLock();
|
||||
await this.stopJitsi();
|
||||
await this.startJitsi();
|
||||
}
|
||||
|
||||
private startJitsi(): void {
|
||||
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
|
||||
this.enterConferenceRoom();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private async enterConversation(): Promise<void> {
|
||||
await this.operator.loaded;
|
||||
try {
|
||||
await this.userMediaPermService.requestMediaAccess();
|
||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||
this.setConferenceState(ConferenceState.jitsi);
|
||||
this.setOptions();
|
||||
if (this.api) {
|
||||
this.api.dispose();
|
||||
this.api = undefined;
|
||||
}
|
||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
||||
this.api.executeCommand('displayName', jitsiname);
|
||||
this.loadApiCallbacks();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private loadApiCallbacks(): void {
|
||||
this.api.on('videoConferenceJoined', (info: ConferenceJoinedResult) => {
|
||||
this.onEnterConference(info);
|
||||
});
|
||||
|
||||
this.api.on('participantJoined', (newMember: JitsiMember) => {
|
||||
this.addMember(newMember);
|
||||
});
|
||||
|
||||
this.api.on('participantLeft', (oldMember: { id: string }) => {
|
||||
this.removeMember(oldMember);
|
||||
});
|
||||
|
||||
this.api.on('displayNameChange', (member: DisplayNameChangeResult) => {
|
||||
this.renameMember(member);
|
||||
});
|
||||
|
||||
this.api.on('audioMuteStatusChanged', (isMuted: { muted: boolean }) => {
|
||||
this.muted = isMuted.muted;
|
||||
});
|
||||
|
||||
this.api.on('readyToClose', () => {
|
||||
this.stopJitsi();
|
||||
});
|
||||
|
||||
this.api.on('dominantSpeakerChanged', (newSpeaker: { id: string }) => {
|
||||
this.newDominantSpeaker(newSpeaker.id);
|
||||
});
|
||||
|
||||
this.api.on('passwordRequired', () => {
|
||||
this.setRoomPassword();
|
||||
});
|
||||
}
|
||||
|
||||
private onEnterConference(info: ConferenceJoinedResult): void {
|
||||
this.isJoined = true;
|
||||
this.addMember({ displayName: info.displayName, id: info.id });
|
||||
this.setRoomPassword();
|
||||
if (this.videoStreamUrl) {
|
||||
this.showJitsiDialog();
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private autoJoinJitsiByLosIndex(operatorClosIndex: number): void {
|
||||
if (operatorClosIndex !== UserListIndexType.NotOnList) {
|
||||
if (!this.isOnCurrentLos) {
|
||||
this.isOnCurrentLos = true;
|
||||
this.triggerMeetingRoomButtonAnimation();
|
||||
}
|
||||
|
||||
if (
|
||||
this.nextSpeakerAmount &&
|
||||
this.nextSpeakerAmount > 0 &&
|
||||
operatorClosIndex > UserListIndexType.Active &&
|
||||
operatorClosIndex <= this.nextSpeakerAmount &&
|
||||
!this.isJitsiActive
|
||||
) {
|
||||
this.enterConferenceRoom();
|
||||
}
|
||||
} else {
|
||||
this.isOnCurrentLos = false;
|
||||
}
|
||||
|
||||
if (!this.isAccessPermitted) {
|
||||
this.viewStream();
|
||||
}
|
||||
}
|
||||
|
||||
private setRoomPassword(): void {
|
||||
if (this.roomPassword && !this.isPasswortSet) {
|
||||
// You can only set the password after the server has recognized that you are
|
||||
// the moderator. There is no event listener for that.
|
||||
setTimeout(() => {
|
||||
this.api.executeCommand('password', this.roomPassword);
|
||||
this.isPasswortSet = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private newDominantSpeaker(newSpeakerId: string): void {
|
||||
if (this.currentDominantSpeaker && this.members[this.currentDominantSpeaker.id]) {
|
||||
this.members[this.currentDominantSpeaker.id].focus = false;
|
||||
}
|
||||
this.members[newSpeakerId].focus = true;
|
||||
this.currentDominantSpeaker = {
|
||||
id: newSpeakerId,
|
||||
displayName: this.members[newSpeakerId].name
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private addMember(newMember: JitsiMember): void {
|
||||
this.members[newMember.id] = {
|
||||
name: newMember.displayName,
|
||||
focus: false
|
||||
} as ConferenceMember;
|
||||
}
|
||||
|
||||
private removeMember(oldMember: { id: string }): void {
|
||||
if (this.members[oldMember.id]) {
|
||||
delete this.members[oldMember.id];
|
||||
}
|
||||
}
|
||||
|
||||
private renameMember(member: DisplayNameChangeResult): void {
|
||||
if (this.members[member.id]) {
|
||||
this.members[member.id].name = member.displayname;
|
||||
}
|
||||
if (this.currentDominantSpeaker?.id === member.id) {
|
||||
this.newDominantSpeaker(member.id);
|
||||
}
|
||||
}
|
||||
|
||||
private clearMembers(): void {
|
||||
this.members = {};
|
||||
}
|
||||
|
||||
public async stopJitsi(): Promise<void> {
|
||||
this.connectToHelpDesk = false;
|
||||
if (this.isJitsiActive) {
|
||||
this.api.executeCommand('hangup');
|
||||
this.clearMembers();
|
||||
await this.deleteJitsiLock();
|
||||
this.api.dispose();
|
||||
this.api = undefined;
|
||||
this.hideJitsiDialog();
|
||||
}
|
||||
this.isJoined = false;
|
||||
this.isPasswortSet = false;
|
||||
this.currentDominantSpeaker = null;
|
||||
}
|
||||
|
||||
private setOptions(): void {
|
||||
this.options = {
|
||||
roomName: this.actualRoomName,
|
||||
parentNode: this.jitsiNode.nativeElement,
|
||||
configOverwrite: this.configOverwrite,
|
||||
interfaceConfigOverwrite: this.interfaceConfigOverwrite
|
||||
};
|
||||
}
|
||||
|
||||
public toggleShowJitsi(): void {
|
||||
this.showJitsiWindow = !this.showJitsiWindow;
|
||||
}
|
||||
|
||||
public toggleConferenceDialog(): void {
|
||||
if (this.isJitsiDialogOpen) {
|
||||
this.hideJitsiDialog();
|
||||
} else {
|
||||
this.showJitsiDialog();
|
||||
}
|
||||
}
|
||||
|
||||
public hideJitsiDialog(): void {
|
||||
this.isJitsiDialogOpen = false;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public showJitsiDialog(): void {
|
||||
this.isJitsiDialogOpen = true;
|
||||
this.showJitsiWindow = false;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public viewStream(): void {
|
||||
this.stopJitsi();
|
||||
this.setConferenceState(ConferenceState.stream);
|
||||
this.showJitsiWindow = true;
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public onSteamLoaded(): void {
|
||||
/**
|
||||
* explicit false check, undefined would mean that this was not checked yet
|
||||
*/
|
||||
if (this.streamLoadedOnce === false) {
|
||||
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {
|
||||
this.streamRunning = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public showVideoPlayer(): boolean {
|
||||
if (!this.canSeeLiveStream) {
|
||||
return false;
|
||||
}
|
||||
return this.streamRunning || this.streamLoadedOnce === false;
|
||||
}
|
||||
|
||||
public isStreamInOtherTab(): boolean {
|
||||
return !this.streamRunning && this.streamLoadedOnce;
|
||||
}
|
||||
|
||||
public enterConferenceRoom(): void {
|
||||
this.actualRoomName = this.defaultRoomName;
|
||||
this.connectToHelpDesk = false;
|
||||
this.enterConversation();
|
||||
}
|
||||
|
||||
public enterSupportRoom(): void {
|
||||
this.actualRoomName = `${this.defaultRoomName}-SUPPORT`;
|
||||
this.connectToHelpDesk = true;
|
||||
this.enterConversation();
|
||||
}
|
||||
|
||||
private onLiveStreamAvailable(liveStreamUrl: string): void {
|
||||
this.videoStreamUrl = liveStreamUrl;
|
||||
// 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
|
||||
if (this.videoStreamUrl && this.currentState === ConferenceState.jitsi && !this.isJitsiActive) {
|
||||
this.viewStream();
|
||||
} else if (!this.videoStreamUrl && this.enableJitsi) {
|
||||
this.setConferenceState(ConferenceState.jitsi);
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
private async deleteJitsiLock(): Promise<void> {
|
||||
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
|
||||
}
|
||||
|
||||
public async deleteStreamingLock(): Promise<void> {
|
||||
await this.storageMap.delete(this.STREAM_RUNNING_STORAGE_KEY).toPromise();
|
||||
}
|
||||
|
||||
private setConferenceState(newState: ConferenceState): void {
|
||||
this.currentState = newState;
|
||||
}
|
||||
|
||||
public sendApplause(): void {
|
||||
this.applauseDisabled = true;
|
||||
this.applauseService.sendApplause();
|
||||
setTimeout(() => {
|
||||
this.applauseDisabled = false;
|
||||
this.cd.markForCheck();
|
||||
}, this.applauseTimeout);
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-progress',
|
||||
templateUrl: './progress.component.html',
|
||||
styleUrls: ['./progress.component.scss']
|
||||
styleUrls: ['./progress.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ProgressComponent {
|
||||
@Input()
|
||||
|
@ -1,6 +1,5 @@
|
||||
<div class="video-wrapper">
|
||||
<os-applause-particle-display *ngIf="showParticles" class="applause-particles"></os-applause-particle-display>
|
||||
<div class="player-container" [ngClass]="{ hide: !isUrlOnline && usingVjs }">
|
||||
<div class="player-container" [ngClass]="{ hide: !isUrlOnline && usingVjs }" *ngIf="isStable">
|
||||
<div *ngIf="usingVjs">
|
||||
<video #vjs class="video-js" controls preload="none"></video>
|
||||
</div>
|
||||
|
@ -4,15 +4,6 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.applause-particles {
|
||||
position: absolute;
|
||||
display: block;
|
||||
pointer-events: none !important;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.is-offline-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { ThrowStmt } from '@angular/compiler';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ApplicationRef,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
@ -17,6 +20,7 @@ import { ajax, AjaxResponse } from 'rxjs/ajax';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import videojs from 'video.js';
|
||||
|
||||
import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
|
||||
enum MimeType {
|
||||
@ -34,32 +38,41 @@ enum Player {
|
||||
selector: 'os-video-player',
|
||||
templateUrl: './video-player.component.html',
|
||||
styleUrls: ['./video-player.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
export class VideoPlayerComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('vjs', { static: false })
|
||||
private vjsPlayerElementRef: ElementRef;
|
||||
|
||||
private _videoUrl: string;
|
||||
|
||||
public isStable = false;
|
||||
private afterViewInitDone = false;
|
||||
|
||||
private youtubeQuerryParams = '?rel=0&iv_load_policy=3&modestbranding=1&autoplay=1';
|
||||
|
||||
@Input()
|
||||
public set videoUrl(value: string) {
|
||||
if (!value.trim()) {
|
||||
return;
|
||||
}
|
||||
this._videoUrl = value.trim();
|
||||
this.playerType = this.determinePlayer(this.videoUrl);
|
||||
|
||||
if (this.usingVjs) {
|
||||
this.mimeType = this.determineContentTypeByUrl(this.videoUrl);
|
||||
this.initVjs();
|
||||
if (this.afterViewInitDone) {
|
||||
this.initVjs();
|
||||
}
|
||||
} else if (this.usingYouTube) {
|
||||
this.stopVJS();
|
||||
this.unloadVjs();
|
||||
this.youTubeVideoId = this.getYouTubeVideoId(this.videoUrl);
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
@Input()
|
||||
public showParticles: boolean;
|
||||
|
||||
public get videoUrl(): string {
|
||||
return this._videoUrl;
|
||||
}
|
||||
@ -83,17 +96,39 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
public get youTubeVideoUrl(): string {
|
||||
return `https://www.youtube.com/embed/${this.youTubeVideoId}?autoplay=1`;
|
||||
return `https://www.youtube.com/embed/${this.youTubeVideoId}${this.youtubeQuerryParams}`;
|
||||
}
|
||||
|
||||
public constructor(config: ConfigService) {
|
||||
public constructor(
|
||||
config: ConfigService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private osStatus: OpenSlidesStatusService
|
||||
) {
|
||||
config.get<string>('general_system_stream_poster').subscribe(posterUrl => {
|
||||
this.posterUrl = posterUrl?.trim();
|
||||
});
|
||||
|
||||
/**
|
||||
* external iFrame will block loading, since for some reason the app will
|
||||
* not become stable if an iFrame was loaded.
|
||||
* (or just goes instable again, for some unknown reason)
|
||||
* This will result in an endless spinner
|
||||
* It's crucial to render external
|
||||
* Videos AFTER the app was stable
|
||||
*/
|
||||
this.osStatus.stable.then(() => {
|
||||
this.isStable = true;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.started.next();
|
||||
if (this.usingVjs) {
|
||||
this.initVjs();
|
||||
} else {
|
||||
this.started.next();
|
||||
}
|
||||
this.afterViewInitDone = true;
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
@ -102,7 +137,6 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
|
||||
private stopVJS(): void {
|
||||
if (this.vjsPlayer) {
|
||||
this.vjsPlayer.src = '';
|
||||
this.vjsPlayer.pause();
|
||||
}
|
||||
}
|
||||
@ -136,6 +170,7 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
} else {
|
||||
this.isUrlOnline = false;
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public async onRefreshVideo(): Promise<void> {
|
||||
@ -145,7 +180,6 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
|
||||
private async initVjs(): Promise<void> {
|
||||
await this.isUrlReachable();
|
||||
|
||||
if (!this.vjsPlayer && this.usingVjs && this.vjsPlayerElementRef) {
|
||||
this.vjsPlayer = videojs(this.vjsPlayerElementRef.nativeElement, {
|
||||
textTrackSettings: false,
|
||||
@ -159,11 +193,15 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private playVjsVideo(): void {
|
||||
if (!this.isUrlOnline) {
|
||||
this.stopVJS();
|
||||
}
|
||||
if (this.usingVjs && this.vjsPlayer && this.isUrlOnline) {
|
||||
this.vjsPlayer.src({
|
||||
src: this.videoUrl,
|
||||
type: this.mimeType
|
||||
});
|
||||
this.started.next();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,14 +127,10 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
|
||||
|
||||
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
||||
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
||||
import { VideoPlayerComponent } from './components/video-player/video-player.component';
|
||||
import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component';
|
||||
import { ApplauseBarDisplayComponent } from './components/applause-bar-display/applause-bar-display.component';
|
||||
import { ProgressComponent } from './components/progress/progress.component';
|
||||
import { NgParticlesModule } from 'ng-particles';
|
||||
import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
|
||||
import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/point-of-order-dialog.component';
|
||||
import { VideoPlayerComponent } from './components/video-player/video-player.component';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -199,8 +195,7 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
||||
PblNgridTargetEventsModule,
|
||||
PdfViewerModule,
|
||||
NgxMaterialTimepickerModule,
|
||||
ChartsModule,
|
||||
NgParticlesModule
|
||||
ChartsModule
|
||||
],
|
||||
exports: [
|
||||
FormsModule,
|
||||
@ -304,10 +299,11 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
||||
VotingPrivacyWarningComponent,
|
||||
MotionPollDetailContentComponent,
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent,
|
||||
VideoPlayerComponent,
|
||||
ListOfSpeakersContentComponent,
|
||||
PointOfOrderDialogComponent
|
||||
PointOfOrderDialogComponent,
|
||||
ListOfSpeakersContentComponent,
|
||||
ProgressComponent,
|
||||
VideoPlayerComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -369,13 +365,10 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
||||
VotingPrivacyWarningComponent,
|
||||
MotionPollDetailContentComponent,
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent,
|
||||
VideoPlayerComponent,
|
||||
ListOfSpeakersContentComponent,
|
||||
ApplauseBarDisplayComponent,
|
||||
ProgressComponent,
|
||||
ApplauseParticleDisplayComponent,
|
||||
PointOfOrderDialogComponent
|
||||
PointOfOrderDialogComponent,
|
||||
VideoPlayerComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -0,0 +1,64 @@
|
||||
<div class="interaction-bar-wrapper" *ngIf="!(isConfStateNone | async)">
|
||||
<ng-container *ngIf="isConfStateJitsi | async">
|
||||
<ng-container *ngIf="(isJoined | async) && !(showCallDialog | async)">
|
||||
<!-- show dialog -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
class="action-bar-shadow background-default"
|
||||
matTooltip="{{ 'Show conference room' | translate }}"
|
||||
(click)="maximizeCallDialog()"
|
||||
@fade
|
||||
>
|
||||
<mat-icon
|
||||
color="primary"
|
||||
[@fadeInOut]="meetingActiveAnimHelper"
|
||||
(@fadeInOut.done)="triggerCallHiddenAnimation()"
|
||||
>
|
||||
fullscreen
|
||||
</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(showLiveConf | async) && (isConfStateStream | async)">
|
||||
<!-- Enter conference from stream -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
class="action-bar-shadow"
|
||||
[class.background-default]="canEnterCall"
|
||||
[class.fake-disabled]="!canEnterCall"
|
||||
matTooltip="{{ enterRoomTooltip | translate }}"
|
||||
(click)="enterConferenceRoom(canEnterCall)"
|
||||
@fade
|
||||
>
|
||||
<mat-icon color="primary"> phone </mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Call support button -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
class="action-bar-shadow background-default"
|
||||
(click)="enterSupportRoom()"
|
||||
matTooltip="{{ 'Access help desk' | translate }}"
|
||||
*ngIf="isSupportEnabled | async"
|
||||
@fade
|
||||
>
|
||||
<mat-icon color="primary">help_outline</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- applause button -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
class="action-bar-shadow background-default"
|
||||
matTooltip="{{ 'Send applause' | translate }}"
|
||||
[disabled]="applauseDisabled"
|
||||
[matBadge]="applauseLevelObservable | async"
|
||||
[matBadgeHidden]="!(applauseLevelObservable | async)"
|
||||
(click)="sendApplause()"
|
||||
*ngIf="showApplause | async"
|
||||
@fade
|
||||
>
|
||||
<mat-icon class="svg-primary" svgIcon="clapping_hands"></mat-icon>
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,17 @@
|
||||
:host {
|
||||
margin: auto 0.5em 0 0;
|
||||
}
|
||||
|
||||
.interaction-bar-wrapper {
|
||||
width: auto;
|
||||
transition: all 2s linear;
|
||||
}
|
||||
|
||||
.action-bar-shadow {
|
||||
box-shadow: 0px 0px 4px 1px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.mat-button-base {
|
||||
margin-left: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { ActionBarComponent } from './action-bar.component';
|
||||
|
||||
describe('ActionBarComponent', () => {
|
||||
let component: ActionBarComponent;
|
||||
let fixture: ComponentFixture<ActionBarComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ActionBarComponent],
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ActionBarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,124 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { fadeAnimation, fadeInOut } from 'app/shared/animations';
|
||||
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';
|
||||
|
||||
const canEnterTooltip = _('Enter conference room');
|
||||
const cannotEnterTooltip = _('Add yourself to the current list of speakers to join the conference');
|
||||
@Component({
|
||||
selector: 'os-action-bar',
|
||||
templateUrl: './action-bar.component.html',
|
||||
styleUrls: ['./action-bar.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [fadeInOut, fadeAnimation]
|
||||
})
|
||||
export class ActionBarComponent extends BaseViewComponentDirective {
|
||||
public applauseLevel = 0;
|
||||
public applauseDisabled = false;
|
||||
|
||||
public showApplause: Observable<boolean> = this.applauseService.showApplause;
|
||||
public applauseLevelObservable: Observable<number> = this.applauseService.applauseLevelObservable;
|
||||
public applauseTimeout = this.applauseService.applauseTimeout;
|
||||
public isJoined: Observable<boolean> = this.rtcService.isJoinedObservable;
|
||||
public showCallDialog: Observable<boolean> = this.rtcService.showCallDialogObservable;
|
||||
public showLiveConf: Observable<boolean> = this.interactionService.showLiveConfObservable;
|
||||
|
||||
public isSupportEnabled: Observable<boolean> = this.rtcService.isSupportEnabled;
|
||||
|
||||
private canEnterCallObservable: Observable<boolean> = this.callRestrictionService.canEnterCallObservable;
|
||||
public canEnterCall = false;
|
||||
|
||||
/**
|
||||
* for the pulse animation
|
||||
*/
|
||||
public enterCallAnimHelper = true;
|
||||
public meetingActiveAnimHelper = true;
|
||||
|
||||
public get isConfStateStream(): Observable<boolean> {
|
||||
return this.interactionService.isConfStateStream;
|
||||
}
|
||||
|
||||
public get isConfStateJitsi(): Observable<boolean> {
|
||||
return this.interactionService.isConfStateJitsi;
|
||||
}
|
||||
|
||||
public get isConfStateNone(): Observable<boolean> {
|
||||
return this.interactionService.isConfStateNone;
|
||||
}
|
||||
|
||||
public get enterRoomTooltip(): string {
|
||||
if (this.canEnterCall) {
|
||||
return _(canEnterTooltip);
|
||||
} else {
|
||||
return _(cannotEnterTooltip);
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private router: Router,
|
||||
private callRestrictionService: CallRestrictionService,
|
||||
private interactionService: InteractionService,
|
||||
private rtcService: RtcService,
|
||||
private applauseService: ApplauseService,
|
||||
private cd: ChangeDetectorRef
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
this.subscriptions.push(
|
||||
this.canEnterCallObservable.subscribe(canEnter => {
|
||||
this.canEnterCall = canEnter;
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async enterConferenceRoom(canEnter: boolean): Promise<void> {
|
||||
if (canEnter) {
|
||||
this.interactionService
|
||||
.enterCall()
|
||||
.then(() => this.rtcService.enterConferenceRoom())
|
||||
.catch(this.raiseError);
|
||||
} else {
|
||||
const navUrl = '/autopilot';
|
||||
this.router.navigate([navUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
public enterSupportRoom(): void {
|
||||
this.interactionService
|
||||
.enterCall()
|
||||
.then(() => this.rtcService.enterSupportRoom())
|
||||
.catch(this.raiseError);
|
||||
}
|
||||
|
||||
public maximizeCallDialog(): void {
|
||||
this.rtcService.showCallDialog = true;
|
||||
}
|
||||
|
||||
public sendApplause(): void {
|
||||
this.applauseDisabled = true;
|
||||
this.applauseService.sendApplause();
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.applauseDisabled = false;
|
||||
this.cd.markForCheck();
|
||||
}, this.applauseTimeout);
|
||||
}
|
||||
|
||||
public triggerCallHiddenAnimation(): void {
|
||||
this.meetingActiveAnimHelper = !this.meetingActiveAnimHelper;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<div class="bar-wrapper" *ngIf="isApplauseTypeBar">
|
||||
<div class="bar-wrapper">
|
||||
<os-progress class="progress-bar" [value]="percent">
|
||||
<div class="level-indicator">
|
||||
<div class="level">
|
@ -11,16 +11,10 @@
|
||||
}
|
||||
|
||||
.level-indicator {
|
||||
height: 50px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
.level {
|
||||
display: inline-block;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.particle-display {
|
||||
// height: 100%;
|
||||
}
|
@ -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,
|
@ -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;
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<!-- iFrame Dialog -->
|
||||
<div class="jitsi-fake-dialog-wrapper" [ngClass]="{ 'dialog-mobile': (isMobile | async) }">
|
||||
<mat-card class="jitsi-fake-dialog">
|
||||
<div class="jitsi-dialog-actions">
|
||||
<span>
|
||||
<a
|
||||
mat-icon-button
|
||||
type="button"
|
||||
color="primary"
|
||||
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||
target="_blank"
|
||||
(click)="hangUp()"
|
||||
[href]="jitsiMeetUrl | async"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
>
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
<span class="right">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
color="primary"
|
||||
matTooltip="{{ 'Minimize' | translate }}"
|
||||
(click)="hideDialog()"
|
||||
>
|
||||
<mat-icon>minimize</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- does not exist yet -->
|
||||
<!-- <button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
color="primary"
|
||||
matTooltip="{{ 'Full screen' | translate }}"
|
||||
(click)="fullScreen()()"
|
||||
>
|
||||
<mat-icon>fullscreen</mat-icon>
|
||||
</button> -->
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
color="primary"
|
||||
matTooltip="{{ 'Exit conference room' | translate }}"
|
||||
(click)="hangUp()"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
||||
</mat-card>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<CallDialogComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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<string> = this.rtcService.jitsiMeetUrl;
|
||||
|
||||
public isMobile: Observable<boolean> = 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<os-call-dialog [ngClass]="{ hide: !(isJitsiDialogOpen | async) }"></os-call-dialog>
|
||||
|
||||
<div class="jitsi-list">
|
||||
<!-- Jitsi content window -->
|
||||
<div class="content">
|
||||
<!-- The "somewhere else active" warning -->
|
||||
<div class="disconnected" *ngIf="(isJitsiActiveInAnotherTab | async) && !isJitsiActive">
|
||||
<span>{{ 'A conference is already running in your OpenSlides session.' | translate }}</span>
|
||||
<button mat-button color="warn" (click)="forceStart()">
|
||||
<span>{{ 'Reenter to conference room' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="disconnected" *ngIf="isDisconnected">
|
||||
<mat-icon>cloud_off</mat-icon>
|
||||
</div>
|
||||
|
||||
<div class="disconnected" *ngIf="isConnecting">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<!-- user list -->
|
||||
<div class="room-members" *ngIf="isConnected">
|
||||
<os-applause-particle-display
|
||||
*ngIf="showParticles | async"
|
||||
class="room-list-applause-particles"
|
||||
></os-applause-particle-display>
|
||||
<div class="member-list">
|
||||
<ol>
|
||||
<li
|
||||
*ngFor="let memberId of memberList; trackBy: trackByIndex"
|
||||
[ngClass]="{
|
||||
focused: members[memberId].focus
|
||||
}"
|
||||
>
|
||||
<div class="member-list-entry">
|
||||
{{ members[memberId].name }}
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom control buttons -->
|
||||
<div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="control-grid">
|
||||
<div class="control-buttons">
|
||||
<!-- Hangup -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="warn"
|
||||
(click)="hangUp()"
|
||||
*ngIf="showHangUp"
|
||||
matTooltip="{{ 'Leave' | translate }}"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Enter jitsi manually -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="accent"
|
||||
(click)="callRoom()"
|
||||
[disabled]="!(canEnterCall | async) || isConnecting"
|
||||
*ngIf="!isJoined"
|
||||
matTooltip="{{ 'Enter conference room' | translate }}"
|
||||
>
|
||||
<mat-icon>call</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="exit">
|
||||
<!-- Exit jitsi, view stream -->
|
||||
<button
|
||||
mat-icon-button
|
||||
color="primary"
|
||||
matTooltip="{{ 'Continue livestream' | translate }}"
|
||||
(click)="viewStream()"
|
||||
*ngIf="!!(liveStreamUrl | async)?.trim()"
|
||||
>
|
||||
<mat-icon>live_tv</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<JitsiComponent>;
|
||||
describe('CallComponent', () => {
|
||||
let component: CallComponent;
|
||||
let fixture: ComponentFixture<CallComponent>;
|
||||
|
||||
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();
|
||||
});
|
@ -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<boolean> = this.rtcService.inOtherTab;
|
||||
public canEnterCall: Observable<boolean> = this.callRestrictionService.canEnterCallObservable;
|
||||
public isJitsiDialogOpen: Observable<boolean> = 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<boolean> {
|
||||
return this.applauseService.showParticles;
|
||||
}
|
||||
|
||||
public get canSeeLiveStream(): Observable<boolean> {
|
||||
return this.streamService.canSeeLiveStreamObservable;
|
||||
}
|
||||
|
||||
public get liveStreamUrl(): Observable<string> {
|
||||
return this.streamService.liveStreamUrlObservable;
|
||||
}
|
||||
|
||||
private autoConnect: boolean;
|
||||
|
||||
@Output()
|
||||
public conferenceTitle: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
public conferenceSubtitle: EventEmitter<string> = 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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<div *ngIf="!(isConfStateNone | async)" class="interaction-container-wrapper">
|
||||
<div class="container-head background-primary" (click)="showHideBody()">
|
||||
<div class="container-head-wrapper">
|
||||
<div class="ellipsis-overflow container-head-title">
|
||||
{{ containerHeadTitle | translate }}
|
||||
</div>
|
||||
<div class="ellipsis-overflow">
|
||||
{{ containerHeadSubtitle | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="container-body background-card"
|
||||
[ngClass]="{ 'container-body-with-applause-bar': showApplauseBar | async, 'container-body-hide': !showBody }"
|
||||
>
|
||||
<os-applause-bar-display
|
||||
*ngIf="(isApplausEnabled | async) && (showApplauseBar | async)"
|
||||
></os-applause-bar-display>
|
||||
<ng-container *ngIf="isConfStateStream | async">
|
||||
<os-stream
|
||||
class="video-player"
|
||||
(streamTitle)="updateTitle($event)"
|
||||
(streamSubtitle)="updateSubtitle($event)"
|
||||
></os-stream>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isConfStateJitsi | async">
|
||||
<os-call
|
||||
class="call-body"
|
||||
(conferenceTitle)="updateTitle($event)"
|
||||
(conferenceSubtitle)="updateSubtitle($event)"
|
||||
></os-call>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
@ -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;
|
||||
}
|
@ -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<InteractionContainerComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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<boolean> {
|
||||
return this.applauseService.showApplause;
|
||||
}
|
||||
|
||||
public get showApplauseBar(): Observable<boolean> {
|
||||
return this.applauseService.showBar;
|
||||
}
|
||||
|
||||
public get isConfStateStream(): Observable<boolean> {
|
||||
return this.interactionService.isConfStateStream;
|
||||
}
|
||||
|
||||
public get isConfStateJitsi(): Observable<boolean> {
|
||||
return this.interactionService.isConfStateJitsi;
|
||||
}
|
||||
|
||||
public get isConfStateNone(): Observable<boolean> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<div *osPerms="permission.coreCanSeeLiveStream; and: showVideoPlayer">
|
||||
<os-applause-particle-display
|
||||
*ngIf="showParticles | async"
|
||||
class="applause-particles"
|
||||
></os-applause-particle-display>
|
||||
<os-video-player [videoUrl]="liveStreamUrl" (started)="onSteamLoaded()"></os-video-player>
|
||||
</div>
|
||||
<div *osPerms="permission.coreCanSeeLiveStream; complement: true">
|
||||
<span> {{ 'You are not allowed to see the live stream' | translate }} </span>
|
||||
</div>
|
||||
<div class="disconnected" *ngIf="isStreamInOtherTab">
|
||||
<button class="restart-stream-button" mat-button color="warn" (click)="forceLoadStream()">
|
||||
<span>{{ 'Restart livestream' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
.applause-particles {
|
||||
position: absolute;
|
||||
display: block;
|
||||
pointer-events: none !important;
|
||||
width: 100px;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
@ -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<StreamComponent>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
@ -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<boolean> {
|
||||
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<string> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
public streamSubtitle: EventEmitter<string> = 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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
28
client/src/app/site/interaction/interaction.module.ts
Normal file
28
client/src/app/site/interaction/interaction.module.ts
Normal file
@ -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 {}
|
@ -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<ApplauseType>;
|
||||
|
||||
public applauseType: ApplauseType;
|
||||
public showApplause: Observable<boolean>;
|
||||
public showApplauseLevel: boolean;
|
||||
public applauseTimeout: number;
|
||||
|
||||
private applauseLevelSubject: Subject<number> = new Subject<number>();
|
||||
public applauseLevelObservable = this.applauseLevelSubject.asObservable();
|
||||
public applauseLevelObservable: Observable<number> = this.applauseLevelSubject.asObservable();
|
||||
|
||||
private get maxApplause(): number {
|
||||
return this.maxApplauseLevel || this.presentApplauseUsers || 0;
|
||||
}
|
||||
|
||||
public get showParticles(): Observable<boolean> {
|
||||
return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.particles));
|
||||
}
|
||||
|
||||
public get showBar(): Observable<boolean> {
|
||||
return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.bar));
|
||||
}
|
||||
|
||||
public constructor(
|
||||
configService: ConfigService,
|
||||
private httpService: HttpService,
|
||||
private notifyService: NotifyService
|
||||
) {
|
||||
this.showApplause = configService.get<boolean>('general_system_applause_enable');
|
||||
this.applauseTypeObservable = configService.get<ApplauseType>('general_system_applause_type');
|
||||
|
||||
configService.get<number>('general_system_applause_min_amount').subscribe(minLevel => {
|
||||
this.minApplauseLevel = minLevel;
|
||||
});
|
||||
configService.get<number>('general_system_applause_max_amount').subscribe(maxLevel => {
|
||||
this.maxApplauseLevel = maxLevel;
|
||||
});
|
||||
configService.get<ApplauseType>('general_system_applause_type').subscribe((type: ApplauseType) => {
|
||||
this.applauseType = type;
|
||||
configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
|
||||
this.showApplauseLevel = show;
|
||||
});
|
||||
configService.get<number>('general_system_stream_applause_timeout').subscribe(timeout => {
|
||||
this.applauseTimeout = (timeout || 1) * 1000;
|
||||
});
|
||||
this.notifyService
|
||||
.getMessageObservable<Applause>(applauseNotifyMessageName)
|
@ -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();
|
||||
});
|
||||
});
|
@ -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<boolean>;
|
||||
|
||||
private canEnterCallSubject = new BehaviorSubject<boolean>(false);
|
||||
public canEnterCallObservable = this.canEnterCallSubject.asObservable();
|
||||
|
||||
private hasToEnterCallSubject = new Subject<void>();
|
||||
public hasToEnterCallObservable = this.hasToEnterCallSubject.asObservable();
|
||||
|
||||
private hasToLeaveCallSubject = new Subject<void>();
|
||||
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<boolean>('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<number>('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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
153
client/src/app/site/interaction/services/interaction.service.ts
Normal file
153
client/src/app/site/interaction/services/interaction.service.ts
Normal file
@ -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>(ConferenceState.none);
|
||||
public conferenceStateObservable = this.conferenceStateSubject.asObservable();
|
||||
public showLiveConfObservable: Observable<boolean>;
|
||||
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<boolean> {
|
||||
return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.stream));
|
||||
}
|
||||
|
||||
public get isConfStateJitsi(): Observable<boolean> {
|
||||
return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.jitsi));
|
||||
}
|
||||
|
||||
public get isConfStateNone(): Observable<boolean> {
|
||||
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<boolean>('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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
18
client/src/app/site/interaction/services/rtc.service.spec.ts
Normal file
18
client/src/app/site/interaction/services/rtc.service.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
397
client/src/app/site/interaction/services/rtc.service.ts
Normal file
397
client/src/app/site/interaction/services/rtc.service.ts
Normal file
@ -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<boolean>(false);
|
||||
public isJitsiEnabledObservable = this.isJitsiEnabledSubject.asObservable();
|
||||
|
||||
public autoConnect: Observable<boolean>;
|
||||
|
||||
// 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<boolean>;
|
||||
private connectedToHelpDeskSubject = new BehaviorSubject<boolean>(false);
|
||||
public connectedToHelpDesk = this.connectedToHelpDeskSubject.asObservable();
|
||||
|
||||
public isJitsiActiveInAnotherTab = false;
|
||||
private isJoinedSubject = new BehaviorSubject<boolean>(false);
|
||||
public isJoinedObservable = this.isJoinedSubject.asObservable();
|
||||
|
||||
private isPasswordSet = false;
|
||||
|
||||
private isJitsiActiveSubject = new BehaviorSubject<boolean>(false);
|
||||
public isJitsiActiveObservable = this.isJitsiActiveSubject.asObservable();
|
||||
|
||||
private get defaultRoomName(): string {
|
||||
return this.jitsiConfig?.JITSI_ROOM_NAME;
|
||||
}
|
||||
|
||||
private jitsiMeetUrlSubject = new Subject<string>();
|
||||
public jitsiMeetUrl = this.jitsiMeetUrlSubject.asObservable();
|
||||
|
||||
private members = {};
|
||||
private memberSubject = new BehaviorSubject<Object>(this.members);
|
||||
public memberObservableObservable = this.memberSubject.asObservable();
|
||||
|
||||
private dominantSpeaker: JitsiMember;
|
||||
private dominantSpeakerSubject = new Subject<JitsiMember>();
|
||||
public dominantSpeakerObservable = this.dominantSpeakerSubject.asObservable();
|
||||
|
||||
private isMutedSubject = new BehaviorSubject<boolean>(false);
|
||||
public isMuted = this.isMutedSubject.asObservable();
|
||||
|
||||
private canEnterCall: boolean;
|
||||
public inOtherTab: Observable<boolean>;
|
||||
|
||||
private showCallDialogSubject = new BehaviorSubject<boolean>(false);
|
||||
public showCallDialogObservable = this.showCallDialogSubject.asObservable();
|
||||
public set showCallDialog(show: boolean) {
|
||||
this.showCallDialogSubject.next(show);
|
||||
}
|
||||
|
||||
public constructor(
|
||||
constantsService: ConstantsService,
|
||||
configService: ConfigService,
|
||||
callRestrictionService: CallRestrictionService,
|
||||
private userMediaPermService: UserMediaPermService,
|
||||
private storageMap: StorageMap,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
this.isSupportEnabled = configService.get<boolean>('general_system_conference_enable_helpdesk');
|
||||
this.autoConnect = configService.get<boolean>('general_system_conference_auto_connect');
|
||||
|
||||
constantsService.get<JitsiConfig>('Settings').subscribe(settings => {
|
||||
this.jitsiConfig = settings;
|
||||
this.isJitsiEnabledSubject.next(!!settings.JITSI_DOMAIN && !!settings.JITSI_ROOM_NAME);
|
||||
});
|
||||
configService.get<boolean>('general_system_conference_open_microphone').subscribe(open => {
|
||||
configOverwrite.startWithAudioMuted = !open;
|
||||
});
|
||||
configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
|
||||
configOverwrite.startWithVideoMuted = !open;
|
||||
});
|
||||
callRestrictionService.canEnterCallObservable.subscribe(canEnter => {
|
||||
this.canEnterCall = canEnter;
|
||||
});
|
||||
|
||||
this.inOtherTab = this.storageMap
|
||||
.watch(RTC_LOGGED_STORAGE_KEY, { type: 'boolean' })
|
||||
.pipe(distinctUntilChanged());
|
||||
}
|
||||
|
||||
public setJitsiNode(jitsiNode: ElementRef): void {
|
||||
this.jitsiNode = jitsiNode;
|
||||
}
|
||||
|
||||
public toggleMute(): void {
|
||||
if (this.isJitsiActive) {
|
||||
this.api.executeCommand('toggleAudio');
|
||||
}
|
||||
}
|
||||
|
||||
public async enterSupportRoom(): Promise<void> {
|
||||
this.connectedToHelpDeskSubject.next(true);
|
||||
this.actualRoomName = `${this.defaultRoomName}-SUPPORT`;
|
||||
await this.enterConversation();
|
||||
}
|
||||
|
||||
public async enterConferenceRoom(force?: boolean): Promise<void> {
|
||||
if (!this.canEnterCall) {
|
||||
return;
|
||||
}
|
||||
if (force) {
|
||||
this.disconnect();
|
||||
}
|
||||
this.connectedToHelpDeskSubject.next(false);
|
||||
this.actualRoomName = this.defaultRoomName;
|
||||
await this.enterConversation();
|
||||
}
|
||||
|
||||
private setRoomPassword(): void {
|
||||
if (this.jitsiConfig?.JITSI_ROOM_PASSWORD && !this.isPasswordSet) {
|
||||
// You can only set the password after the server has recognized that you are
|
||||
// the moderator. There is no event listener for that.
|
||||
setTimeout(() => {
|
||||
this.api.executeCommand('password', this.jitsiConfig?.JITSI_ROOM_PASSWORD);
|
||||
this.isPasswordSet = true;
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async enterConversation(): Promise<void> {
|
||||
await this.operator.loaded;
|
||||
await this.userMediaPermService.requestMediaAccess();
|
||||
this.storageMap.set(RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||
this.setOptions();
|
||||
if (this.api) {
|
||||
this.api.dispose();
|
||||
this.api = undefined;
|
||||
}
|
||||
this.api = new JitsiMeetExternalAPI(this.jitsiConfig?.JITSI_DOMAIN, this.options);
|
||||
this.isJitsiActiveSubject.next(true);
|
||||
const jitsiName = this.operator.viewUser.getShortName();
|
||||
this.api.executeCommand('displayName', jitsiName);
|
||||
this.loadApiCallbacks();
|
||||
}
|
||||
|
||||
private loadApiCallbacks(): void {
|
||||
this.isMutedSubject.next(configOverwrite.startWithAudioMuted);
|
||||
|
||||
this.api.on('videoConferenceJoined', (info: ConferenceJoinedResult) => {
|
||||
this.onEnterConference(info);
|
||||
});
|
||||
|
||||
this.api.on('participantJoined', (newMember: JitsiMember) => {
|
||||
this.addMember(newMember);
|
||||
});
|
||||
|
||||
this.api.on('participantLeft', (oldMember: { id: string }) => {
|
||||
this.removeMember(oldMember);
|
||||
});
|
||||
|
||||
this.api.on('displayNameChange', (member: DisplayNameChangeResult) => {
|
||||
this.renameMember(member);
|
||||
});
|
||||
|
||||
this.api.on('audioMuteStatusChanged', (isMuted: { muted: boolean }) => {
|
||||
this.isMutedSubject.next(isMuted.muted);
|
||||
});
|
||||
|
||||
this.api.on('readyToClose', () => {
|
||||
this.stopJitsi();
|
||||
});
|
||||
|
||||
this.api.on('dominantSpeakerChanged', (newSpeaker: { id: string }) => {
|
||||
this.newDominantSpeaker(newSpeaker.id);
|
||||
});
|
||||
|
||||
this.api.on('passwordRequired', () => {
|
||||
this.setRoomPassword();
|
||||
});
|
||||
|
||||
this.api.on('participantKickedOut', (kicked: MemberKicked) => {
|
||||
this.onMemberKicked(kicked);
|
||||
});
|
||||
}
|
||||
|
||||
private onEnterConference(info: ConferenceJoinedResult): void {
|
||||
this.isJoinedSubject.next(true);
|
||||
this.showCallDialogSubject.next(true);
|
||||
this.addMember({ displayName: info.displayName, id: info.id });
|
||||
this.setRoomPassword();
|
||||
}
|
||||
|
||||
private addMember(newMember: JitsiMember): void {
|
||||
this.members[newMember.id] = {
|
||||
name: newMember.displayName,
|
||||
focus: false
|
||||
} as ConferenceMember;
|
||||
this.updateMemberSubject();
|
||||
}
|
||||
|
||||
private onMemberKicked(kick: MemberKicked): void {
|
||||
if (kick.kicked.local) {
|
||||
this.stopJitsi();
|
||||
}
|
||||
}
|
||||
|
||||
private removeMember(oldMember: { id: string }): void {
|
||||
if (this.members[oldMember.id]) {
|
||||
delete this.members[oldMember.id];
|
||||
this.updateMemberSubject();
|
||||
}
|
||||
}
|
||||
|
||||
private renameMember(member: DisplayNameChangeResult): void {
|
||||
if (this.members[member.id]) {
|
||||
this.members[member.id].name = member.displayname;
|
||||
this.updateMemberSubject();
|
||||
}
|
||||
if (this.dominantSpeaker?.id === member.id) {
|
||||
this.newDominantSpeaker(member.id);
|
||||
}
|
||||
}
|
||||
|
||||
private newDominantSpeaker(newSpeakerId: string): void {
|
||||
if (this.dominantSpeaker && this.members[this.dominantSpeaker.id]) {
|
||||
this.members[this.dominantSpeaker.id].focus = false;
|
||||
}
|
||||
this.members[newSpeakerId].focus = true;
|
||||
this.updateMemberSubject();
|
||||
this.dominantSpeaker = {
|
||||
id: newSpeakerId,
|
||||
displayName: this.members[newSpeakerId].name
|
||||
};
|
||||
this.dominantSpeakerSubject.next(this.dominantSpeaker);
|
||||
}
|
||||
|
||||
private clearMembers(): void {
|
||||
this.members = {};
|
||||
this.updateMemberSubject();
|
||||
}
|
||||
|
||||
private updateMemberSubject(): void {
|
||||
this.memberSubject.next(this.members);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/jitsi/jitsi-meet/issues/8244
|
||||
*/
|
||||
public enterFullScreen(): void {}
|
||||
|
||||
private disconnect(): void {
|
||||
if (this.isJitsiActive) {
|
||||
this.api?.executeCommand('hangup');
|
||||
this.api?.dispose();
|
||||
this.api = undefined;
|
||||
}
|
||||
this.clearMembers();
|
||||
this.deleteJitsiLock();
|
||||
this.dominantSpeaker = null;
|
||||
this.dominantSpeakerSubject.next(this.dominantSpeaker);
|
||||
this.isJoinedSubject.next(false);
|
||||
this.showCallDialogSubject.next(false);
|
||||
this.isPasswordSet = false;
|
||||
}
|
||||
|
||||
public stopJitsi(): void {
|
||||
this.disconnect();
|
||||
this.connectedToHelpDeskSubject.next(false);
|
||||
this.isJitsiActiveSubject.next(false);
|
||||
}
|
||||
|
||||
private setOptions(): void {
|
||||
this.setJitsiMeetUrl();
|
||||
this.options = {
|
||||
roomName: this.actualRoomName,
|
||||
parentNode: this.jitsiNode.nativeElement,
|
||||
configOverwrite: configOverwrite,
|
||||
interfaceConfigOverwrite: interfaceConfigOverwrite
|
||||
};
|
||||
}
|
||||
|
||||
private setJitsiMeetUrl(): void {
|
||||
this.jitsiMeetUrlSubject.next(`https://${this.jitsiConfig.JITSI_DOMAIN}/${this.actualRoomName}`);
|
||||
}
|
||||
|
||||
private deleteJitsiLock(): void {
|
||||
this.storageMap.delete(RTC_LOGGED_STORAGE_KEY).subscribe();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { StreamService } from './stream.service';
|
||||
|
||||
describe('StreamService', () => {
|
||||
let service: StreamService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
|
||||
service = TestBed.inject(StreamService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
58
client/src/app/site/interaction/services/stream.service.ts
Normal file
58
client/src/app/site/interaction/services/stream.service.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
|
||||
const STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StreamService {
|
||||
public liveStreamUrlObservable: Observable<string>;
|
||||
|
||||
/**
|
||||
* undefined is controlled behavior, meaning, this property was not
|
||||
* checked yet.
|
||||
* Thus, false-checks have to be explicit
|
||||
*/
|
||||
public streamLoadedOnceObservable: Observable<boolean>;
|
||||
|
||||
private isStreamRunningSubject = new Subject<boolean>();
|
||||
public isStreamRunningObservable = this.isStreamRunningSubject.asObservable();
|
||||
|
||||
private canSeeLiveStreamSubject = new Subject<boolean>();
|
||||
public canSeeLiveStreamObservable = this.canSeeLiveStreamSubject.asObservable();
|
||||
|
||||
public constructor(private storageMap: StorageMap, operator: OperatorService, configService: ConfigService) {
|
||||
this.liveStreamUrlObservable = configService.get<string>('general_system_stream_url');
|
||||
|
||||
this.streamLoadedOnceObservable = this.storageMap
|
||||
.watch(STREAM_RUNNING_STORAGE_KEY, { type: 'boolean' })
|
||||
.pipe(distinctUntilChanged());
|
||||
|
||||
operator.getUserObservable().subscribe(() => {
|
||||
this.canSeeLiveStreamSubject.next(operator.hasPerms(Permission.coreCanSeeLiveStream));
|
||||
});
|
||||
}
|
||||
|
||||
public setStreamingLock(): void {
|
||||
this.storageMap.set(STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
|
||||
}
|
||||
|
||||
public setStreamRunning(running: boolean): void {
|
||||
this.isStreamRunningSubject.next(running);
|
||||
}
|
||||
|
||||
public deleteStreamingLock(): void {
|
||||
/**
|
||||
* subscriptions are faster than promises. This will fire more reliable
|
||||
* than converting it to promise first
|
||||
*/
|
||||
this.storageMap.delete(STREAM_RUNNING_STORAGE_KEY).subscribe();
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OverlayService } from './overlay.service';
|
||||
import { OverlayService } from '../../../core/ui-services/overlay.service';
|
||||
|
||||
const givePermsMessage = _('Please allow OpenSlides to access your microphone and/or camera');
|
||||
const accessDeniedMessage = _('Media access is denied');
|
@ -114,7 +114,8 @@
|
||||
</main>
|
||||
</div>
|
||||
<div class="toolbars">
|
||||
<os-jitsi></os-jitsi>
|
||||
<os-action-bar></os-action-bar>
|
||||
<os-interaction-container></os-interaction-container>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user