Merge pull request #5309 from tsiegleauq/integrate-jitsi-meet-client
Integrate jitsi-meet in OpenSlides
This commit is contained in:
commit
a47285c0ff
@ -43,7 +43,7 @@
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": ["node_modules/tinymce/tinymce.min.js"],
|
||||
"scripts": ["node_modules/tinymce/tinymce.min.js", "src/assets/jitsi/external_api.js"],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||
},
|
||||
"configurations": {
|
||||
|
149
client/src/app/shared/components/jitsi/jitsi.component.html
Normal file
149
client/src/app/shared/components/jitsi/jitsi.component.html
Normal file
@ -0,0 +1,149 @@
|
||||
<div class="jitsi-integration">
|
||||
<div
|
||||
class="jitsi-bar"
|
||||
[ngClass]="{
|
||||
'cdk-visually-hidden': !enableJitsi,
|
||||
'cast-shadow': !showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="control-icon-wrapper apply-theme"
|
||||
[ngClass]="{
|
||||
'cast-shadow': showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<!-- mute/unmute button -->
|
||||
<button
|
||||
class="indicator quick-icon"
|
||||
mat-mini-fab
|
||||
*ngIf="isJoined"
|
||||
(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">cloud_off</mat-icon>
|
||||
|
||||
<!-- hide unhide video -->
|
||||
<!-- <button class="quick-icon" mat-mini-fab [disabled]="!isJoined">
|
||||
<mat-icon *ngIf="isJoined" color="primary">videocam_off</mat-icon>
|
||||
</button> -->
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="list-wrapper apply-theme"
|
||||
[ngClass]="{
|
||||
'cast-shadow': showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<!-- <span class="list-wrapper apply-theme regular-shadow"> -->
|
||||
<!-- open-window button -->
|
||||
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
|
||||
<span> {{ 'Audio Conference' | translate }}</span>
|
||||
<mat-icon class="opened-indicator" *ngIf="!showJitsiWindow">keyboard_arrow_up</mat-icon>
|
||||
<mat-icon class="opened-indicator" *ngIf="showJitsiWindow">keyboard_arrow_down </mat-icon>
|
||||
|
||||
<div class="one-line">
|
||||
|
||||
<span *ngIf="currentDominantSpeaker">
|
||||
» <span class="dominant-speaker">{{ currentDominantSpeaker.displayName }}</span>
|
||||
</span>
|
||||
<span *ngIf="!isJitsiActive">
|
||||
<i>{{ 'disconnected' | translate }}</i>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- unfolded list -->
|
||||
<div
|
||||
class="jitsi-list"
|
||||
[ngClass]="{
|
||||
'cdk-visually-hidden': !showJitsiWindow
|
||||
}"
|
||||
>
|
||||
<!-- Jitsi content window -->
|
||||
<div class="content">
|
||||
<!-- The "somewhere else active" warning -->
|
||||
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||
<span>{{ 'Another instance of Jitsi is active in you OpenSlides session' | translate }}</span>
|
||||
<button mat-button color="warn" (click)="forceStart()">
|
||||
<span>{{ 'Force Jitsi to reload' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||
<span>{{ 'disconnected' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Hidden jitsy container -->
|
||||
<div [ngStyle]="{ display: 'none' }" #jitsi></div>
|
||||
|
||||
<!-- user list -->
|
||||
<div class="room-members" *ngIf="isJitsiActive">
|
||||
<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)="stopJitsi()"
|
||||
*ngIf="isJitsiActive && isJoined"
|
||||
matTooltip="{{ 'Leave' | translate }}"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Enter jitsi manually -->
|
||||
<button
|
||||
mat-mini-fab
|
||||
color="accent"
|
||||
(click)="enterConversation()"
|
||||
[disabled]="isJitsiActive || isJitsiActiveInAnotherTab"
|
||||
*ngIf="!isJoined"
|
||||
matTooltip="{{ 'Enter conference' | translate }}"
|
||||
>
|
||||
<mat-icon>call</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Open in new tab -->
|
||||
<button
|
||||
mat-icon-button
|
||||
class="open-jitsi-in-tab"
|
||||
color="accent"
|
||||
(click)="openExternal()"
|
||||
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||
[disabled]="isRoomPasswordProtected"
|
||||
>
|
||||
<mat-icon>
|
||||
open_in_new
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
116
client/src/app/shared/components/jitsi/jitsi.component.scss
Normal file
116
client/src/app/shared/components/jitsi/jitsi.component.scss
Normal file
@ -0,0 +1,116 @@
|
||||
.jitsi-integration {
|
||||
.cast-shadow {
|
||||
box-shadow: -3px -3px 10px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.jitsi-bar {
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 0;
|
||||
$wrapper-padding: 5px;
|
||||
$bar-height: 40px;
|
||||
|
||||
.control-icon-wrapper {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
min-height: $bar-height;
|
||||
padding-top: $wrapper-padding;
|
||||
border-top-right-radius: 4px;
|
||||
width: 250px;
|
||||
max-width: 250px;
|
||||
|
||||
.toggle-list-button {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
width: 100%;
|
||||
padding: 0 2.5em;
|
||||
margin-bottom: $wrapper-padding;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
|
||||
.opened-indicator {
|
||||
position: absolute;
|
||||
right: $wrapper-padding;
|
||||
top: $wrapper-padding;
|
||||
}
|
||||
|
||||
.dominant-speaker {
|
||||
font-weight: 500;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-list {
|
||||
.content {
|
||||
height: 40vh;
|
||||
clear: both;
|
||||
|
||||
.disconnected {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: inherit;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
|
||||
span {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.room-members {
|
||||
height: inherit;
|
||||
|
||||
.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: 'helper buttons new-tab';
|
||||
grid-template-columns: 40px auto 40px;
|
||||
|
||||
.control-buttons {
|
||||
grid-area: buttons;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.open-jitsi-in-tab {
|
||||
grid-area: new-tab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-jitsi-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, accent);
|
||||
$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);
|
||||
}
|
||||
|
||||
.toggle-list-button {
|
||||
span {
|
||||
color: mat-color($primary, default-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-list {
|
||||
background-color: mat-color($background, card);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { JitsiComponent } from './jitsi.component';
|
||||
|
||||
describe('JitsiComponent', () => {
|
||||
let component: JitsiComponent;
|
||||
let fixture: ComponentFixture<JitsiComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [JitsiComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(JitsiComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
341
client/src/app/shared/components/jitsi/jitsi.component.ts
Normal file
341
client/src/app/shared/components/jitsi/jitsi.component.ts
Normal file
@ -0,0 +1,341 @@
|
||||
import { Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
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 { ConfigService } from 'app/core/ui-services/config.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;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'os-jitsi',
|
||||
templateUrl: './jitsi.component.html',
|
||||
styleUrls: ['./jitsi.component.scss']
|
||||
})
|
||||
export class JitsiComponent extends BaseComponent implements OnDestroy {
|
||||
public enableJitsi: boolean;
|
||||
private autoconnect: boolean;
|
||||
private roomName: string;
|
||||
private roomPassword: string;
|
||||
private jitsiDomain: string;
|
||||
|
||||
// do not set the password twice
|
||||
private isPasswortSet = false;
|
||||
|
||||
public showJitsiWindow = false;
|
||||
public muted = true;
|
||||
|
||||
@ViewChild('jitsi')
|
||||
private jitsiNode: ElementRef;
|
||||
|
||||
// JitsiMeet api object
|
||||
private api: any | null;
|
||||
|
||||
public get isJitsiActive(): boolean {
|
||||
return !!this.api;
|
||||
}
|
||||
|
||||
public isJoined: boolean;
|
||||
|
||||
private options: object;
|
||||
|
||||
private lockLoaded: Deferred<void> = new Deferred();
|
||||
private constantsLoaded: Deferred<void> = new Deferred();
|
||||
|
||||
// storage locks
|
||||
public isJitsiActiveInAnotherTab: boolean;
|
||||
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private configOverwrite = {
|
||||
startAudioOnly: true,
|
||||
// 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
|
||||
};
|
||||
|
||||
private interfaceConfigOverwrite = {
|
||||
filmStripOnly: true,
|
||||
INITIAL_TOOLBAR_TIMEOUT: 2000,
|
||||
TOOLBAR_TIMEOUT: 400,
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
SHOW_WATERMARK_FOR_GUESTS: false,
|
||||
INVITATION_POWERED_BY: false,
|
||||
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
|
||||
TOOLBAR_BUTTONS: [],
|
||||
SETTINGS_SECTIONS: []
|
||||
};
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
private operator: OperatorService,
|
||||
private storageMap: StorageMap,
|
||||
private userRepo: UserRepositoryService,
|
||||
private constantsService: ConstantsService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
this.setUp();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.stopJitsi();
|
||||
}
|
||||
|
||||
// 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.stopJitsi();
|
||||
}
|
||||
|
||||
private async setUp(): Promise<void> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
await this.lockLoaded;
|
||||
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
||||
if (settings) {
|
||||
this.jitsiDomain = settings.JITSI_DOMAIN;
|
||||
this.roomName = settings.JITSI_ROOM_NAME;
|
||||
this.roomPassword = settings.JITSI_ROOM_PASSWORD;
|
||||
this.constantsLoaded.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await this.constantsLoaded;
|
||||
this.configService
|
||||
.get<boolean>('general_system_conference_show')
|
||||
.subscribe(autoconnect => (this.autoconnect = autoconnect));
|
||||
|
||||
this.configService.get<boolean>('general_system_conference_auto_connect').subscribe(show => {
|
||||
this.enableJitsi = show && !!this.jitsiDomain && !!this.roomName;
|
||||
if (this.enableJitsi && this.autoconnect) {
|
||||
this.startJitsi();
|
||||
} else {
|
||||
this.stopJitsi();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public toggleMute(): void {
|
||||
if (this.isJitsiActive) {
|
||||
this.api.executeCommand('toggleAudio');
|
||||
}
|
||||
}
|
||||
|
||||
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.enterConversation();
|
||||
}
|
||||
}
|
||||
|
||||
public async enterConversation(): Promise<void> {
|
||||
await this.operator.loaded;
|
||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||
this.setOptions();
|
||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||
|
||||
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
||||
this.api.executeCommand('displayName', jitsiname);
|
||||
this.loadApiCallbacks();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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> {
|
||||
if (this.isJitsiActive) {
|
||||
this.api.executeCommand('hangup');
|
||||
this.clearMembers();
|
||||
await this.deleteJitsiLock();
|
||||
this.api.dispose();
|
||||
this.api = undefined;
|
||||
}
|
||||
this.isJoined = false;
|
||||
this.isPasswortSet = false;
|
||||
this.currentDominantSpeaker = null;
|
||||
}
|
||||
|
||||
private setOptions(): void {
|
||||
this.options = {
|
||||
roomName: this.roomName,
|
||||
parentNode: this.jitsiNode.nativeElement,
|
||||
configOverwrite: this.configOverwrite,
|
||||
interfaceConfigOverwrite: this.interfaceConfigOverwrite
|
||||
};
|
||||
}
|
||||
|
||||
public toggleShowJitsi(): void {
|
||||
this.showJitsiWindow = !this.showJitsiWindow;
|
||||
}
|
||||
|
||||
private getJitsiMeetUrl(): string {
|
||||
return `https://${this.jitsiDomain}/${this.roomName}`;
|
||||
}
|
||||
|
||||
public openExternal(): void {
|
||||
this.stopJitsi();
|
||||
window.open(this.getJitsiMeetUrl(), '_blank');
|
||||
}
|
||||
|
||||
private async deleteJitsiLock(): Promise<void> {
|
||||
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
|
||||
}
|
||||
}
|
@ -10,6 +10,11 @@ $pbl-height: var(--pbl-height);
|
||||
height: $pbl-height;
|
||||
}
|
||||
|
||||
// additional space if the jitsi integration is active
|
||||
.pbl-ngrid-row:last-of-type {
|
||||
margin-bottom: 30px !important;
|
||||
}
|
||||
|
||||
.pbl-ngrid-cell {
|
||||
height: inherit;
|
||||
}
|
||||
|
@ -126,6 +126,7 @@ 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';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -292,7 +293,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||
PollPercentBasePipe,
|
||||
VotingPrivacyWarningComponent,
|
||||
MotionPollDetailContentComponent,
|
||||
AssignmentPollDetailContentComponent
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -352,7 +354,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||
PollPercentBasePipe,
|
||||
VotingPrivacyWarningComponent,
|
||||
MotionPollDetailContentComponent,
|
||||
AssignmentPollDetailContentComponent
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -41,7 +41,7 @@
|
||||
<os-grid-layout *ngIf="projector">
|
||||
<os-tile [preferredSize]="projectorTileSizeLeft">
|
||||
<div *ngIf="projector" class="projector-detail-wrapper column-left">
|
||||
<a [routerLink]="['/projector', projector.id]">
|
||||
<a [routerLink]="['/projector', projector.id]" target="_blank">
|
||||
<div id="projector">
|
||||
<os-projector [projector]="projector"></os-projector>
|
||||
</div>
|
||||
|
@ -24,7 +24,7 @@
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container class="meta-text-block-content">
|
||||
<a class="no-markup" [routerLink]="getDetailLink()">
|
||||
<a class="no-markup" [routerLink]="getDetailLink()" [target]="projectionTarget">
|
||||
<div class="projector">
|
||||
<os-projector [projector]="projector"></os-projector>
|
||||
</div>
|
||||
|
@ -35,6 +35,14 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
|
||||
return this._projector;
|
||||
}
|
||||
|
||||
public get projectionTarget(): '_blank' | '_self' {
|
||||
if (this.operator.hasPerms('core.can_manage_projector')) {
|
||||
return '_self';
|
||||
} else {
|
||||
return '_blank';
|
||||
}
|
||||
}
|
||||
|
||||
private _projector: ViewProjector;
|
||||
|
||||
/**
|
||||
|
@ -89,6 +89,7 @@
|
||||
<mat-icon>arrow_forward_ios</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<os-jitsi></os-jitsi>
|
||||
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
|
||||
<main>
|
||||
<router-outlet #o="outlet"></router-outlet>
|
||||
|
2
client/src/assets/jitsi/external_api.js
Normal file
2
client/src/assets/jitsi/external_api.js
Normal file
File diff suppressed because one or more lines are too long
@ -32,6 +32,7 @@
|
||||
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
||||
@import './app/site/assignments/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';
|
||||
|
||||
/** fonts */
|
||||
@import './assets/styles/fonts.scss';
|
||||
@ -64,6 +65,7 @@ $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);
|
||||
}
|
||||
|
||||
/** Load projector specific SCSS values */
|
||||
@ -335,9 +337,15 @@ b,
|
||||
text-align: center;
|
||||
color: gray;
|
||||
}
|
||||
mat-card {
|
||||
|
||||
.regular-shadow {
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37) !important;
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
@extend .regular-shadow;
|
||||
}
|
||||
|
||||
.os-card {
|
||||
max-width: 770px;
|
||||
margin-top: 20px !important;
|
||||
|
@ -145,6 +145,9 @@ class CoreAppConfig(AppConfig):
|
||||
"PING_INTERVAL",
|
||||
"PING_TIMEOUT",
|
||||
"ENABLE_ELECTRONIC_VOTING",
|
||||
"JITSI_DOMAIN",
|
||||
"JITSI_ROOM_NAME",
|
||||
"JITSI_ROOM_PASSWORD",
|
||||
]
|
||||
client_settings_dict = {}
|
||||
for key in client_settings_keys:
|
||||
|
@ -122,6 +122,24 @@ def get_config_variables():
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name="general_system_conference_auto_connect",
|
||||
default_value=False,
|
||||
input_type="boolean",
|
||||
label="Show the audio conferece integration at the bottom right corner",
|
||||
weight=142,
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
yield ConfigVariable(
|
||||
name="general_system_conference_show",
|
||||
default_value=False,
|
||||
input_type="boolean",
|
||||
label="Allow users to automatically connect to the audio conference",
|
||||
weight=143,
|
||||
subgroup="System",
|
||||
)
|
||||
|
||||
# General export settings
|
||||
|
||||
yield ConfigVariable(
|
||||
|
Loading…
Reference in New Issue
Block a user