merge
This commit is contained in:
commit
e9baef5963
30
.github/workflows/build-and-deploy.yml
vendored
30
.github/workflows/build-and-deploy.yml
vendored
@ -2,7 +2,7 @@ name: Build, push and deploy Docker image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branch: [master]
|
branches: [master]
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -16,7 +16,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-front:
|
build-front:
|
||||||
if: ${{ github.event.release || github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -36,11 +36,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-front
|
repository: thecodingmachine/workadventure-front
|
||||||
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || github.event.release && env.GITHUB_REF_SLUG || 'master' }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-back:
|
build-back:
|
||||||
if: ${{ github.event.release || github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -59,11 +59,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-back
|
repository: thecodingmachine/workadventure-back
|
||||||
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || github.event.release && env.GITHUB_REF_SLUG || 'master' }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-pusher:
|
build-pusher:
|
||||||
if: ${{ github.event.release || github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -82,11 +82,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-pusher
|
repository: thecodingmachine/workadventure-pusher
|
||||||
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || github.event.release && env.GITHUB_REF_SLUG || 'master' }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-uploader:
|
build-uploader:
|
||||||
if: ${{ github.event.release || github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -105,11 +105,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-uploader
|
repository: thecodingmachine/workadventure-uploader
|
||||||
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || github.event.release && env.GITHUB_REF_SLUG || 'master' }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-maps:
|
build-maps:
|
||||||
if: ${{ github.event.release || github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -129,7 +129,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-maps
|
repository: thecodingmachine/workadventure-maps
|
||||||
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || github.event.release && env.GITHUB_REF_SLUG || 'master' }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
@ -140,7 +140,7 @@ jobs:
|
|||||||
- build-maps
|
- build-maps
|
||||||
- build-uploader
|
- build-uploader
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.push || contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -158,13 +158,13 @@ jobs:
|
|||||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
||||||
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
||||||
DEPLOY_REF: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || 'master' }}
|
DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
with:
|
with:
|
||||||
namespace: workadventure-${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || 'master' }}
|
namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
|
|
||||||
- name: Add a comment in PR
|
- name: Add a comment in PR
|
||||||
uses: unsplash/comment-on-pr@v1.2.0
|
uses: unsplash/comment-on-pr@v1.2.0
|
||||||
if: ${{ github.event.pull_request }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
local env = std.extVar("env"),
|
local env = std.extVar("env"),
|
||||||
local namespace = env.DEPLOY_REF,
|
local namespace = env.DEPLOY_REF,
|
||||||
local tag = namespace,
|
local tag = namespace,
|
||||||
local url = if namespace == "master" then "workadventu.re" else namespace+".test.workadventu.re",
|
local url = namespace+".test.workadventu.re",
|
||||||
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
|
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
|
||||||
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null,
|
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null,
|
||||||
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
||||||
@ -25,10 +25,7 @@
|
|||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
} else {})
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"back2": {
|
"back2": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
@ -47,10 +44,7 @@
|
|||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
} else {})
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"pusher": {
|
"pusher": {
|
||||||
"replicas": 2,
|
"replicas": 2,
|
||||||
@ -69,10 +63,7 @@
|
|||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
} else {})
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"front": {
|
"front": {
|
||||||
"image": "thecodingmachine/workadventure-front:"+tag,
|
"image": "thecodingmachine/workadventure-front:"+tag,
|
||||||
|
BIN
front/dist/resources/logos/logo-WA-min.png
vendored
Normal file
BIN
front/dist/resources/logos/logo-WA-min.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -17,7 +17,6 @@ export class SoundMeter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private init(context: AudioContext) {
|
private init(context: AudioContext) {
|
||||||
if (this.context === undefined) {
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.analyser = this.context.createAnalyser();
|
this.analyser = this.context.createAnalyser();
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ export class SoundMeter {
|
|||||||
const bufferLength = this.analyser.fftSize;
|
const bufferLength = this.analyser.fftSize;
|
||||||
this.dataArray = new Uint8Array(bufferLength);
|
this.dataArray = new Uint8Array(bufferLength);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public connectToSource(stream: MediaStream, context: AudioContext): void
|
public connectToSource(stream: MediaStream, context: AudioContext): void
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@ import {ResizableScene} from "../Login/ResizableScene";
|
|||||||
import GameObject = Phaser.GameObjects.GameObject;
|
import GameObject = Phaser.GameObjects.GameObject;
|
||||||
import Events = Phaser.Scenes.Events;
|
import Events = Phaser.Scenes.Events;
|
||||||
import AnimationEvents = Phaser.Animations.Events;
|
import AnimationEvents = Phaser.Animations.Events;
|
||||||
|
import StructEvents = Phaser.Structs.Events;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A scene that can track its dirty/pristine state.
|
* A scene that can track its dirty/pristine state.
|
||||||
@ -23,12 +24,11 @@ export abstract class DirtyScene extends ResizableScene {
|
|||||||
}
|
}
|
||||||
this.isAlreadyTracking = true;
|
this.isAlreadyTracking = true;
|
||||||
const trackAnimationFunction = this.trackAnimation.bind(this);
|
const trackAnimationFunction = this.trackAnimation.bind(this);
|
||||||
this.events.on(Events.ADDED_TO_SCENE, (gameObject: GameObject) => {
|
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_ADD, (gameObject: GameObject) => {
|
||||||
this.objectListChanged = true;
|
this.objectListChanged = true;
|
||||||
gameObject.on(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
gameObject.on(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
||||||
});
|
});
|
||||||
|
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_REMOVE, (gameObject: GameObject) => {
|
||||||
this.events.on(Events.REMOVED_FROM_SCENE, (gameObject: GameObject) => {
|
|
||||||
this.objectListChanged = true;
|
this.objectListChanged = true;
|
||||||
gameObject.removeListener(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
gameObject.removeListener(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
||||||
});
|
});
|
||||||
|
@ -187,6 +187,7 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
private originalMapUrl: string|undefined;
|
private originalMapUrl: string|undefined;
|
||||||
private pinchManager: PinchManager|undefined;
|
private pinchManager: PinchManager|undefined;
|
||||||
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
|
||||||
|
private onVisibilityChangeCallback: () => void;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
|
||||||
super({
|
super({
|
||||||
@ -202,10 +203,11 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
|
|
||||||
this.createPromise = new Promise<void>((resolve, reject): void => {
|
this.createPromise = new Promise<void>((resolve, reject): void => {
|
||||||
this.createPromiseResolve = resolve;
|
this.createPromiseResolve = resolve;
|
||||||
})
|
});
|
||||||
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
|
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
|
||||||
this.connectionAnswerPromiseResolve = resolve;
|
this.connectionAnswerPromiseResolve = resolve;
|
||||||
});
|
});
|
||||||
|
this.onVisibilityChangeCallback = this.onVisibilityChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
//hook preload scene
|
//hook preload scene
|
||||||
@ -504,6 +506,8 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
if (!this.room.isDisconnected()) {
|
if (!this.room.isDisconnected()) {
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -625,6 +629,7 @@ export class GameScene extends DirtyScene implements CenterListener {
|
|||||||
self.chatModeSprite.setVisible(false);
|
self.chatModeSprite.setVisible(false);
|
||||||
self.openChatIcon.setVisible(false);
|
self.openChatIcon.setVisible(false);
|
||||||
audioManager.restoreVolume();
|
audioManager.restoreVolume();
|
||||||
|
self.onVisibilityChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -947,6 +952,8 @@ ${escapedMessage}
|
|||||||
for(const iframeEvents of this.iframeSubscriptionList){
|
for(const iframeEvents of this.iframeSubscriptionList){
|
||||||
iframeEvents.unsubscribe();
|
iframeEvents.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('visibilitychange', this.onVisibilityChangeCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeAllRemotePlayers(): void {
|
private removeAllRemotePlayers(): void {
|
||||||
@ -1511,6 +1518,8 @@ ${escapedMessage}
|
|||||||
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
|
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
|
||||||
this.stopJitsi();
|
this.stopJitsi();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.onVisibilityChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopJitsi(): void {
|
public stopJitsi(): void {
|
||||||
@ -1546,6 +1555,7 @@ ${escapedMessage}
|
|||||||
openJitsiRoomFunction();
|
openJitsiRoomFunction();
|
||||||
}, this.userInputManager);
|
}, this.userInputManager);
|
||||||
}
|
}
|
||||||
|
this.onVisibilityChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
//todo: put this into an 'orchestrator' scene (EntryScene?)
|
||||||
@ -1585,4 +1595,20 @@ ${escapedMessage}
|
|||||||
waScaleManager.zoomModifier *= zoomFactor;
|
waScaleManager.zoomModifier *= zoomFactor;
|
||||||
this.updateCameraOffset();
|
this.updateCameraOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onVisibilityChange(): void {
|
||||||
|
// If the overlay is not displayed, we are in Jitsi. We don't need the webcam.
|
||||||
|
if (!mediaManager.isGameOverlayVisible()) {
|
||||||
|
mediaManager.blurCamera();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
mediaManager.focusCamera();
|
||||||
|
} else {
|
||||||
|
if (this.simplePeer.getNbConnections() === 0) {
|
||||||
|
mediaManager.blurCamera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,6 @@ export class EnableCameraScene extends ResizableScene {
|
|||||||
|
|
||||||
update(time: number, delta: number): void {
|
update(time: number, delta: number): void {
|
||||||
this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
|
this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
|
||||||
mediaManager.updateScene();
|
|
||||||
|
|
||||||
this.centerXDomElement(this.enableCameraSceneElement, 300);
|
this.centerXDomElement(this.enableCameraSceneElement, 300);
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,10 @@ interface jitsiConfigInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultConfig = () : jitsiConfigInterface => {
|
const getDefaultConfig = () : jitsiConfigInterface => {
|
||||||
|
const constraints = mediaManager.getConstraintRequestedByUser();
|
||||||
return {
|
return {
|
||||||
startWithAudioMuted: !mediaManager.constraintsMedia.audio,
|
startWithAudioMuted: !constraints.audio,
|
||||||
startWithVideoMuted: mediaManager.constraintsMedia.video === false,
|
startWithVideoMuted: constraints.video === false,
|
||||||
prejoinPageEnabled: false
|
prejoinPageEnabled: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,7 +72,7 @@ class JitsiFactory {
|
|||||||
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
private audioCallback = this.onAudioChange.bind(this);
|
private audioCallback = this.onAudioChange.bind(this);
|
||||||
private videoCallback = this.onVideoChange.bind(this);
|
private videoCallback = this.onVideoChange.bind(this);
|
||||||
private previousConfigMeet? : jitsiConfigInterface;
|
private previousConfigMeet! : jitsiConfigInterface;
|
||||||
private jitsiScriptLoaded: boolean = false;
|
private jitsiScriptLoaded: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,32 +137,24 @@ class JitsiFactory {
|
|||||||
|
|
||||||
//restore previous config
|
//restore previous config
|
||||||
if(this.previousConfigMeet?.startWithAudioMuted){
|
if(this.previousConfigMeet?.startWithAudioMuted){
|
||||||
mediaManager.disableMicrophone();
|
await mediaManager.disableMicrophone();
|
||||||
}else{
|
}else{
|
||||||
mediaManager.enableMicrophone();
|
await mediaManager.enableMicrophone();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.previousConfigMeet?.startWithVideoMuted){
|
if(this.previousConfigMeet?.startWithVideoMuted){
|
||||||
mediaManager.disableCamera();
|
await mediaManager.disableCamera();
|
||||||
}else{
|
}else{
|
||||||
mediaManager.enableCamera();
|
await mediaManager.enableCamera();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAudioChange({muted}: {muted: boolean}): void {
|
private onAudioChange({muted}: {muted: boolean}): void {
|
||||||
if (muted && mediaManager.constraintsMedia.audio === true) {
|
this.previousConfigMeet.startWithAudioMuted = muted;
|
||||||
mediaManager.disableMicrophone();
|
|
||||||
} else if(!muted && mediaManager.constraintsMedia.audio === false) {
|
|
||||||
mediaManager.enableMicrophone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onVideoChange({muted}: {muted: boolean}): void {
|
private onVideoChange({muted}: {muted: boolean}): void {
|
||||||
if (muted && mediaManager.constraintsMedia.video !== false) {
|
this.previousConfigMeet.startWithVideoMuted = muted;
|
||||||
mediaManager.disableCamera();
|
|
||||||
} else if(!muted && mediaManager.constraintsMedia.video === false) {
|
|
||||||
mediaManager.enableCamera();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadJitsiScript(domain: string): Promise<void> {
|
private async loadJitsiScript(domain: string): Promise<void> {
|
||||||
|
@ -20,7 +20,7 @@ const audioConstraint: boolean|MediaTrackConstraints = {
|
|||||||
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
//TODO: make these values configurable in the game settings menu and store them in localstorage
|
||||||
autoGainControl: false,
|
autoGainControl: false,
|
||||||
echoCancellation: true,
|
echoCancellation: true,
|
||||||
noiseSuppression: false
|
noiseSuppression: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
|
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
|
||||||
@ -43,7 +43,8 @@ export class MediaManager {
|
|||||||
microphoneClose: HTMLImageElement;
|
microphoneClose: HTMLImageElement;
|
||||||
microphone: HTMLImageElement;
|
microphone: HTMLImageElement;
|
||||||
webrtcInAudio: HTMLAudioElement;
|
webrtcInAudio: HTMLAudioElement;
|
||||||
mySoundMeterElement: HTMLDivElement;
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
|
//mySoundMeterElement: HTMLDivElement;
|
||||||
private webrtcOutAudio: HTMLAudioElement;
|
private webrtcOutAudio: HTMLAudioElement;
|
||||||
constraintsMedia : MediaStreamConstraints = {
|
constraintsMedia : MediaStreamConstraints = {
|
||||||
audio: audioConstraint,
|
audio: audioConstraint,
|
||||||
@ -62,18 +63,16 @@ export class MediaManager {
|
|||||||
private previousConstraint : MediaStreamConstraints;
|
private previousConstraint : MediaStreamConstraints;
|
||||||
private focused : boolean = true;
|
private focused : boolean = true;
|
||||||
|
|
||||||
private lastUpdateScene : Date = new Date();
|
|
||||||
private setTimeOutlastUpdateScene? : NodeJS.Timeout;
|
|
||||||
|
|
||||||
private hasCamera = true;
|
private hasCamera = true;
|
||||||
|
|
||||||
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
|
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
|
||||||
|
|
||||||
private userInputManager?: UserInputManager;
|
private userInputManager?: UserInputManager;
|
||||||
|
|
||||||
private mySoundMeter?: SoundMeter|null;
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
|
/*private mySoundMeter?: SoundMeter|null;
|
||||||
private soundMeters: Map<string, SoundMeter> = new Map<string, SoundMeter>();
|
private soundMeters: Map<string, SoundMeter> = new Map<string, SoundMeter>();
|
||||||
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
|
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();*/
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
||||||
@ -132,17 +131,19 @@ export class MediaManager {
|
|||||||
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
|
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
|
||||||
this.pingCameraStatus();
|
this.pingCameraStatus();
|
||||||
|
|
||||||
this.checkActiveUser(); //todo: desactivated in case of bug
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
|
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
|
||||||
this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
|
|
||||||
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
|
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
|
||||||
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
|
||||||
});
|
});*/
|
||||||
|
|
||||||
|
//Check of ask notification navigator permission
|
||||||
|
this.getNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateScene(){
|
public updateScene(){
|
||||||
this.lastUpdateScene = new Date();
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
this.updateSoudMeter();
|
//this.updateSoudMeter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public blurCamera() {
|
public blurCamera() {
|
||||||
@ -154,6 +155,13 @@ export class MediaManager {
|
|||||||
this.disableCamera();
|
this.disableCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the constraint that the user wants (independently of the visibility / jitsi state...)
|
||||||
|
*/
|
||||||
|
public getConstraintRequestedByUser(): MediaStreamConstraints {
|
||||||
|
return this.previousConstraint ?? this.constraintsMedia;
|
||||||
|
}
|
||||||
|
|
||||||
public focusCamera() {
|
public focusCamera() {
|
||||||
if(this.focused){
|
if(this.focused){
|
||||||
return;
|
return;
|
||||||
@ -196,7 +204,7 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public showGameOverlay(){
|
public showGameOverlay(): void {
|
||||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
||||||
gameOverlay.classList.add('active');
|
gameOverlay.classList.add('active');
|
||||||
|
|
||||||
@ -207,7 +215,7 @@ export class MediaManager {
|
|||||||
buttonCloseFrame.removeEventListener('click', functionTrigger);
|
buttonCloseFrame.removeEventListener('click', functionTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public hideGameOverlay(){
|
public hideGameOverlay(): void {
|
||||||
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
||||||
gameOverlay.classList.remove('active');
|
gameOverlay.classList.remove('active');
|
||||||
|
|
||||||
@ -218,6 +226,11 @@ export class MediaManager {
|
|||||||
buttonCloseFrame.addEventListener('click', functionTrigger);
|
buttonCloseFrame.addEventListener('click', functionTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isGameOverlayVisible(): boolean {
|
||||||
|
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
|
||||||
|
return gameOverlay.classList.contains('active');
|
||||||
|
}
|
||||||
|
|
||||||
public updateCameraQuality(value: number) {
|
public updateCameraQuality(value: number) {
|
||||||
this.enableCameraStyle();
|
this.enableCameraStyle();
|
||||||
const newVideoConstraint = JSON.parse(JSON.stringify(videoConstraint));
|
const newVideoConstraint = JSON.parse(JSON.stringify(videoConstraint));
|
||||||
@ -229,29 +242,32 @@ export class MediaManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public enableCamera() {
|
public async enableCamera() {
|
||||||
this.constraintsMedia.video = videoConstraint;
|
this.constraintsMedia.video = videoConstraint;
|
||||||
|
|
||||||
this.getCamera().then((stream: MediaStream) => {
|
try {
|
||||||
|
const stream = await this.getCamera()
|
||||||
//TODO show error message tooltip upper of camera button
|
//TODO show error message tooltip upper of camera button
|
||||||
//TODO message : please check camera permission of your navigator
|
//TODO message : please check camera permission of your navigator
|
||||||
if(stream.getVideoTracks().length === 0) {
|
if(stream.getVideoTracks().length === 0) {
|
||||||
throw Error('Video track is empty, please check camera permission of your navigator')
|
throw new Error('Video track is empty, please check camera permission of your navigator')
|
||||||
}
|
}
|
||||||
this.enableCameraStyle();
|
this.enableCameraStyle();
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||||
}).catch((err) => {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.disableCameraStyle();
|
this.disableCameraStyle();
|
||||||
|
this.stopCamera();
|
||||||
|
|
||||||
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
|
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => {
|
||||||
this.showHelpCameraSettingsCallBack();
|
this.showHelpCameraSettingsCallBack();
|
||||||
}, this.userInputManager);
|
}, this.userInputManager);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disableCamera() {
|
public async disableCamera() {
|
||||||
this.disableCameraStyle();
|
this.disableCameraStyle();
|
||||||
|
this.stopCamera();
|
||||||
|
|
||||||
if (this.constraintsMedia.audio !== false) {
|
if (this.constraintsMedia.audio !== false) {
|
||||||
const stream = await this.getCamera();
|
const stream = await this.getCamera();
|
||||||
@ -261,25 +277,27 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enableMicrophone() {
|
public async enableMicrophone() {
|
||||||
this.constraintsMedia.audio = audioConstraint;
|
this.constraintsMedia.audio = audioConstraint;
|
||||||
|
|
||||||
this.getCamera().then((stream) => {
|
try {
|
||||||
|
const stream = await this.getCamera();
|
||||||
|
|
||||||
//TODO show error message tooltip upper of camera button
|
//TODO show error message tooltip upper of camera button
|
||||||
//TODO message : please check microphone permission of your navigator
|
//TODO message : please check microphone permission of your navigator
|
||||||
if(stream.getAudioTracks().length === 0) {
|
if (stream.getAudioTracks().length === 0) {
|
||||||
throw Error('Audio track is empty, please check microphone permission of your navigator')
|
throw Error('Audio track is empty, please check microphone permission of your navigator')
|
||||||
}
|
}
|
||||||
this.enableMicrophoneStyle();
|
this.enableMicrophoneStyle();
|
||||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||||
}).catch((err) => {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
this.disableMicrophoneStyle();
|
this.disableMicrophoneStyle();
|
||||||
|
|
||||||
layoutManager.addInformation('warning', 'Microphone access denied. Click here and check navigators permissions.', () => {
|
layoutManager.addInformation('warning', 'Microphone access denied. Click here and check navigators permissions.', () => {
|
||||||
this.showHelpCameraSettingsCallBack();
|
this.showHelpCameraSettingsCallBack();
|
||||||
}, this.userInputManager);
|
}, this.userInputManager);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disableMicrophone() {
|
public async disableMicrophone() {
|
||||||
@ -324,7 +342,6 @@ export class MediaManager {
|
|||||||
this.cinemaBtn.classList.add("disabled");
|
this.cinemaBtn.classList.add("disabled");
|
||||||
this.constraintsMedia.video = false;
|
this.constraintsMedia.video = false;
|
||||||
this.myCamVideo.srcObject = null;
|
this.myCamVideo.srcObject = null;
|
||||||
this.stopCamera();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enableMicrophoneStyle(){
|
private enableMicrophoneStyle(){
|
||||||
@ -435,6 +452,8 @@ export class MediaManager {
|
|||||||
return this.getLocalStream().catch((err) => {
|
return this.getLocalStream().catch((err) => {
|
||||||
console.info('Error get camera, trying with video option at null =>', err);
|
console.info('Error get camera, trying with video option at null =>', err);
|
||||||
this.disableCameraStyle();
|
this.disableCameraStyle();
|
||||||
|
this.stopCamera();
|
||||||
|
|
||||||
return this.getLocalStream().then((stream : MediaStream) => {
|
return this.getLocalStream().then((stream : MediaStream) => {
|
||||||
this.hasCamera = false;
|
this.hasCamera = false;
|
||||||
return stream;
|
return stream;
|
||||||
@ -457,12 +476,12 @@ export class MediaManager {
|
|||||||
this.localStream = stream;
|
this.localStream = stream;
|
||||||
this.myCamVideo.srcObject = this.localStream;
|
this.myCamVideo.srcObject = this.localStream;
|
||||||
|
|
||||||
//init sound meter
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
this.mySoundMeter = null;
|
/*this.mySoundMeter = null;
|
||||||
if(this.constraintsMedia.audio){
|
if(this.constraintsMedia.audio){
|
||||||
this.mySoundMeter = new SoundMeter();
|
this.mySoundMeter = new SoundMeter();
|
||||||
this.mySoundMeter.connectToSource(stream, new AudioContext());
|
this.mySoundMeter.connectToSource(stream, new AudioContext());
|
||||||
}
|
}*/
|
||||||
return stream;
|
return stream;
|
||||||
}).catch((err: Error) => {
|
}).catch((err: Error) => {
|
||||||
throw err;
|
throw err;
|
||||||
@ -489,7 +508,7 @@ export class MediaManager {
|
|||||||
track.stop();
|
track.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.mySoundMeter?.stop();
|
//this.mySoundMeter?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCamera(id: string): Promise<MediaStream> {
|
setCamera(id: string): Promise<MediaStream> {
|
||||||
@ -632,11 +651,12 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
remoteVideo.srcObject = stream;
|
remoteVideo.srcObject = stream;
|
||||||
|
|
||||||
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
//sound metter
|
//sound metter
|
||||||
const soundMeter = new SoundMeter();
|
/*const soundMeter = new SoundMeter();
|
||||||
soundMeter.connectToSource(stream, new AudioContext());
|
soundMeter.connectToSource(stream, new AudioContext());
|
||||||
this.soundMeters.set(userId, soundMeter);
|
this.soundMeters.set(userId, soundMeter);
|
||||||
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));
|
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/
|
||||||
}
|
}
|
||||||
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){
|
addStreamRemoteScreenSharing(userId: string, stream : MediaStream){
|
||||||
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
|
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
|
||||||
@ -652,9 +672,10 @@ export class MediaManager {
|
|||||||
layoutManager.remove(userId);
|
layoutManager.remove(userId);
|
||||||
this.remoteVideo.delete(userId);
|
this.remoteVideo.delete(userId);
|
||||||
|
|
||||||
this.soundMeters.get(userId)?.stop();
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
|
/*this.soundMeters.get(userId)?.stop();
|
||||||
this.soundMeters.delete(userId);
|
this.soundMeters.delete(userId);
|
||||||
this.soundMeterElements.delete(userId);
|
this.soundMeterElements.delete(userId);*/
|
||||||
|
|
||||||
//permit to remove user in discussion part
|
//permit to remove user in discussion part
|
||||||
this.removeParticipant(userId);
|
this.removeParticipant(userId);
|
||||||
@ -776,22 +797,6 @@ export class MediaManager {
|
|||||||
this.userInputManager = userInputManager;
|
this.userInputManager = userInputManager;
|
||||||
discussionManager.setUserInputManager(userInputManager);
|
discussionManager.setUserInputManager(userInputManager);
|
||||||
}
|
}
|
||||||
//check if user is active
|
|
||||||
private checkActiveUser(){
|
|
||||||
if(this.setTimeOutlastUpdateScene){
|
|
||||||
clearTimeout(this.setTimeOutlastUpdateScene);
|
|
||||||
}
|
|
||||||
this.setTimeOutlastUpdateScene = setTimeout(() => {
|
|
||||||
const now = new Date();
|
|
||||||
//if last update is more of 10 sec
|
|
||||||
if( (now.getTime() - this.lastUpdateScene.getTime()) > 10000) {
|
|
||||||
this.blurCamera();
|
|
||||||
}else{
|
|
||||||
this.focusCamera();
|
|
||||||
}
|
|
||||||
this.checkActiveUser();
|
|
||||||
}, this.focused ? 10000 : 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setShowReportModalCallBacks(callback: ShowReportCallBack){
|
public setShowReportModalCallBacks(callback: ShowReportCallBack){
|
||||||
this.showReportModalCallBacks.add(callback);
|
this.showReportModalCallBacks.add(callback);
|
||||||
@ -807,7 +812,8 @@ export class MediaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSoudMeter(){
|
//FIX ME SOUNDMETER: check stalability of sound meter calculation
|
||||||
|
/*updateSoudMeter(){
|
||||||
try{
|
try{
|
||||||
const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0));
|
const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0));
|
||||||
this.setVolumeSoundMeter(volume, this.mySoundMeterElement);
|
this.setVolumeSoundMeter(volume, this.mySoundMeterElement);
|
||||||
@ -824,7 +830,7 @@ export class MediaManager {
|
|||||||
}catch(err){
|
}catch(err){
|
||||||
//console.error(err);
|
//console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
private setVolumeSoundMeter(volume: number, element: HTMLDivElement){
|
private setVolumeSoundMeter(volume: number, element: HTMLDivElement){
|
||||||
if(volume <= 0 && !element.classList.contains('active')){
|
if(volume <= 0 && !element.classList.contains('active')){
|
||||||
@ -847,6 +853,32 @@ export class MediaManager {
|
|||||||
elementChildre.classList.add('active');
|
elementChildre.classList.add('active');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNotification(){
|
||||||
|
//Get notification
|
||||||
|
if (window.Notification && Notification.permission !== "granted") {
|
||||||
|
Notification.requestPermission().catch((err) => {
|
||||||
|
console.error(`Notification permission error`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createNotification(userName: string){
|
||||||
|
if(this.focused){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (window.Notification && Notification.permission === "granted") {
|
||||||
|
const title = 'WorkAdventure';
|
||||||
|
const options = {
|
||||||
|
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
|
||||||
|
icon: '/resources/logos/logo-WA-min.png',
|
||||||
|
image: '/resources/logos/logo-WA-min.png',
|
||||||
|
badge: '/resources/logos/logo-WA-min.png',
|
||||||
|
};
|
||||||
|
new Notification(title, options);
|
||||||
|
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mediaManager = new MediaManager();
|
export const mediaManager = new MediaManager();
|
||||||
|
@ -158,6 +158,11 @@ export class SimplePeer {
|
|||||||
this.sendLocalScreenSharingStreamToUser(user.userId);
|
this.sendLocalScreenSharingStreamToUser(user.userId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Create a notification for first user in circle discussion
|
||||||
|
if(this.PeerConnectionArray.size === 0){
|
||||||
|
mediaManager.createNotification(user.name??'');
|
||||||
|
}
|
||||||
this.PeerConnectionArray.set(user.userId, peer);
|
this.PeerConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||||
|
Loading…
Reference in New Issue
Block a user