Merge branch 'develop' of github.com:bigbluebutton/bigbluebutton into akka-pekko-migration
This commit is contained in:
commit
225b2f2d74
55
.github/workflows/automated-tests-build-package-job.yml
vendored
Normal file
55
.github/workflows/automated-tests-build-package-job.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
name: Build service and cache
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
build-name:
|
||||
required: true
|
||||
type: string
|
||||
build-list:
|
||||
type: string
|
||||
cache-files-list:
|
||||
type: string
|
||||
cache-urls-list:
|
||||
type: string
|
||||
jobs:
|
||||
b:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || '' }}
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }}
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git config user.name "BBB Automated Tests"
|
||||
git config user.email "tests@bigbluebutton.org"
|
||||
git config pull.rebase false
|
||||
git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }}
|
||||
- name: Set cache-key vars
|
||||
run: |
|
||||
echo "CACHE_KEY_FILES=$(echo '${{ inputs.cache-files-list }} .gitlab-ci.yml build/deb-helper.sh' | xargs -n1 git log -1 --format=%h -- | tr '\n' '-' | sed 's/-$//')" >> $GITHUB_ENV
|
||||
echo "CACHE_KEY_URLS=$(echo '${{ inputs.cache-urls-list }}' | xargs -r -n 1 curl -Is | grep -i 'Last-Modified' | md5sum | cut -c1-10)" >> $GITHUB_ENV
|
||||
cat bigbluebutton-config/bigbluebutton-release >> $GITHUB_ENV
|
||||
echo "RUNNER_OS=$(lsb_release -d | cut -f2 | tr -d ' ')" >> $GITHUB_ENV
|
||||
echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
if: inputs.cache-files-list != ''
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ env.RUNNER_OS }}-${{ inputs.build-name }}-${{ env.BIGBLUEBUTTON_RELEASE }}-commits-${{ env.CACHE_KEY_FILES }}-urls-${{ env.CACHE_KEY_URLS }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
echo "${{ inputs.build-list || inputs.build-name }}" | xargs -n 1 ./build/setup.sh
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_${{ inputs.build-name }}.tar
|
||||
path: artifacts.tar
|
379
.github/workflows/automated-tests.yml
vendored
379
.github/workflows/automated-tests.yml
vendored
@ -15,318 +15,96 @@ on:
|
||||
- '**/*.md'
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-bbb-apps-akka:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_AKKA_APPS_KEY=$(git log -1 --format=%H -- akka-bbb-apps)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-apps-akka-${{ env.CACHE_AKKA_APPS_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-apps-akka
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-apps-akka.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-apps-akka
|
||||
cache-files-list: akka-bbb-apps bbb-common-message
|
||||
build-bbb-config:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-config
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-config.tar
|
||||
path: artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-config
|
||||
cache-files-list: bigbluebutton-config
|
||||
build-bbb-export-annotations:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-export-annotations
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-export-annotations.tar
|
||||
path: artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-export-annotations
|
||||
cache-files-list: bbb-export-annotations
|
||||
build-bbb-learning-dashboard:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_LEARNING_DASHBOARD_KEY=$(git log -1 --format=%H -- bbb-learning-dashboard)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-learning-dashboard-${{ env.CACHE_LEARNING_DASHBOARD_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-learning-dashboard
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-learning-dashboard.tar
|
||||
path: artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-learning-dashboard
|
||||
cache-files-list: bbb-learning-dashboard
|
||||
build-bbb-playback-record:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: ./build/get_external_dependencies.sh
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: ./build/setup.sh bbb-playback
|
||||
- run: ./build/setup.sh bbb-playback-notes
|
||||
- run: ./build/setup.sh bbb-playback-podcast
|
||||
- run: ./build/setup.sh bbb-playback-presentation
|
||||
- run: ./build/setup.sh bbb-playback-screenshare
|
||||
- run: ./build/setup.sh bbb-playback-video
|
||||
- run: ./build/setup.sh bbb-record-core
|
||||
- run: tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-playback-record.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-playback-record
|
||||
build-list: bbb-playback bbb-playback-notes bbb-playback-podcast bbb-playback-presentation bbb-playback-screenshare bbb-playback-video bbb-record-core
|
||||
build-bbb-graphql-server:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: ./build/get_external_dependencies.sh
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: ./build/setup.sh bbb-graphql-middleware
|
||||
- run: ./build/setup.sh bbb-graphql-server
|
||||
- run: tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-graphql-server.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-graphql-server
|
||||
build-list: bbb-graphql-server bbb-graphql-middleware
|
||||
build-bbb-etherpad:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_ETHERPAD_VERSION_KEY=$(git log -1 --format=%H -- bbb-etherpad.placeholder.sh)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_ETHERPAD_BUILD_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-etherpad)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_URL1_KEY=$(curl -s https://api.github.com/repos/mconf/ep_pad_ttl/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_URL2_KEY=$(curl -s https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_URL3_KEY=$(curl -s https://api.github.com/repos/mconf/ep_redis_publisher/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_URL4_KEY=$(curl -s https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-etherpad-${{ env.CACHE_ETHERPAD_VERSION_KEY }}-${{ env.CACHE_ETHERPAD_BUILD_KEY }}-${{ env.CACHE_URL1_KEY }}-${{ env.CACHE_URL2_KEY }}-${{ env.CACHE_URL3_KEY }}-${{ env.CACHE_URL4_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-etherpad
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-etherpad.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-etherpad
|
||||
cache-files-list: bbb-etherpad.placeholder.sh build/packages-template/bbb-etherpad
|
||||
cache-urls-list: https://api.github.com/repos/mconf/ep_pad_ttl/commits https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits https://api.github.com/repos/mconf/ep_redis_publisher/commits https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits
|
||||
build-bbb-bbb-web:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_BBB_WEB_KEY=$(git log -1 --format=%H -- bigbluebutton-web)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_COMMON_WEB_KEY=$(git log -1 --format=%H -- bbb-common-web)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-web-${{ env.CACHE_BBB_WEB_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_COMMON_WEB_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-web
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-web.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-web
|
||||
cache-files-list: bigbluebutton-web bbb-common-message bbb-common-web
|
||||
build-bbb-fsesl-akka:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_AKKA_FSESL_KEY=$(git log -1 --format=%H -- akka-bbb-fsesl)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-fsesl-akka-${{ env.CACHE_AKKA_FSESL_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-fsesl-akka
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-fsesl-akka.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-fsesl-akka
|
||||
cache-files-list: akka-bbb-fsesl bbb-common-message
|
||||
build-bbb-html5:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_KEY=$(git log -1 --format=%H -- bigbluebutton-html5)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-html5-${{ env.CACHE_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-html5-nodejs
|
||||
./build/setup.sh bbb-html5
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-html5.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-html5
|
||||
build-list: bbb-html5-nodejs bbb-html5
|
||||
cache-files-list: bigbluebutton-html5
|
||||
build-bbb-freeswitch:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- run: echo "CACHE_FREESWITCH_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-core)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_FREESWITCH_SOUNDS_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-sounds)" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_SOUNDS_KEY=$(curl -Is http://bigbluebutton.org/downloads/sounds.tar.gz | grep "Last-Modified" | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- name: Handle cache
|
||||
id: cache-action
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: artifacts.tar
|
||||
key: ${{ runner.os }}-bbb-freeswitch-${{ env.CACHE_FREESWITCH_KEY }}-${{ env.CACHE_FREESWITCH_SOUNDS_KEY }}-${{ env.CACHE_SOUNDS_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
|
||||
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||
name: Generate artifacts
|
||||
run: |
|
||||
./build/get_external_dependencies.sh
|
||||
./build/setup.sh bbb-freeswitch-core
|
||||
./build/setup.sh bbb-freeswitch-sounds
|
||||
tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-freeswitch.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-freeswitch
|
||||
build-list: bbb-freeswitch-core bbb-freeswitch-sounds
|
||||
cache-files-list: freeswitch.placeholder.sh build/packages-template/bbb-freeswitch-core build/packages-template/bbb-freeswitch-sounds
|
||||
cache-urls-list: http://bigbluebutton.org/downloads/sounds.tar.gz
|
||||
build-bbb-webrtc:
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: bbb-webrtc
|
||||
build-list: bbb-webrtc-sfu bbb-webrtc-recorder
|
||||
cache-files-list: bbb-webrtc-sfu.placeholder.sh bbb-webrtc-recorder.placeholder.sh build/packages-template/bbb-webrtc-sfu build/packages-template/bbb-webrtc-recorder
|
||||
build-others:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: ./build/get_external_dependencies.sh
|
||||
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||
- run: ./build/setup.sh bbb-mkclean
|
||||
- run: ./build/setup.sh bbb-pads
|
||||
- run: ./build/setup.sh bbb-libreoffice-docker
|
||||
- run: ./build/setup.sh bbb-webrtc-sfu
|
||||
- run: ./build/setup.sh bbb-webrtc-recorder
|
||||
- run: ./build/setup.sh bbb-transcription-controller
|
||||
- run: ./build/setup.sh bigbluebutton
|
||||
- run: tar cvf artifacts.tar artifacts/
|
||||
- name: Archive packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: artifacts.tar
|
||||
path: |
|
||||
artifacts.tar
|
||||
# - name: Fake package build
|
||||
# run: |
|
||||
# sudo -i <<EOF
|
||||
# set -e
|
||||
# echo "Faking a package build (to speed up installation test)"
|
||||
# cd /
|
||||
# wget -nv "http://ci.bbb.imdt.dev/artifacts.tar"
|
||||
# tar xf artifacts.tar
|
||||
# mv artifacts /home/runner/work/bigbluebutton/bigbluebutton/artifacts/
|
||||
# EOF
|
||||
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
|
||||
with:
|
||||
build-name: others
|
||||
build-list: bbb-mkclean bbb-pads bbb-libreoffice-docker bbb-transcription-controller bigbluebutton
|
||||
install-and-run-tests:
|
||||
needs: [build-bbb-apps-akka, build-bbb-config, build-bbb-export-annotations, build-bbb-learning-dashboard, build-bbb-playback-record, build-bbb-graphql-server, build-bbb-etherpad, build-bbb-bbb-web, build-bbb-fsesl-akka, build-bbb-html5, build-bbb-freeswitch, build-others]
|
||||
needs: [build-bbb-apps-akka, build-bbb-config, build-bbb-export-annotations, build-bbb-learning-dashboard, build-bbb-playback-record, build-bbb-graphql-server, build-bbb-etherpad, build-bbb-bbb-web, build-bbb-fsesl-akka, build-bbb-html5, build-bbb-freeswitch, build-bbb-webrtc, build-others]
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref || '' }}
|
||||
fetch-depth: 0 # Fetch all history
|
||||
- name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }}
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
git config user.name "BBB Automated Tests"
|
||||
git config user.email "tests@bigbluebutton.org"
|
||||
git config pull.rebase false
|
||||
git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }}
|
||||
- run: ./build/get_external_dependencies.sh
|
||||
- name: Download artifacts_bbb-apps-akka
|
||||
uses: actions/download-artifact@v3
|
||||
@ -368,6 +146,11 @@ jobs:
|
||||
with:
|
||||
name: artifacts_bbb-freeswitch.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-webrtc
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifacts_bbb-webrtc.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Download artifacts_bbb-web
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@ -386,7 +169,7 @@ jobs:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifacts.tar
|
||||
name: artifacts_others.tar
|
||||
- run: tar xf artifacts.tar
|
||||
- name: Extracting files .tar
|
||||
run: |
|
||||
@ -461,7 +244,7 @@ jobs:
|
||||
run: |
|
||||
sudo -i <<EOF
|
||||
set -e
|
||||
cd /root/ && wget -q https://raw.githubusercontent.com/bigbluebutton/bbb-install/v2.8.x-release/bbb-install.sh -O bbb-install.sh
|
||||
cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v2.8.x-release/bbb-install.sh -O bbb-install.sh
|
||||
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v jammy-28-develop -s bbb-ci.test -j -d /certs/
|
||||
bbb-conf --salt bbbci
|
||||
echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf
|
||||
|
88
.github/workflows/publish-test-report.yml
vendored
Normal file
88
.github/workflows/publish-test-report.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
name: Publish Test Results
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Automated tests
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
get-pr-data:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
outputs:
|
||||
pr-number: ${{ steps.set-env.outputs.pr-number }}
|
||||
workflow-id: ${{ steps.set-env.outputs.workflow-id }}
|
||||
steps:
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "pr-comment-data"
|
||||
})[0];
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr-comment-data.zip`, Buffer.from(download.data));
|
||||
- name: Unzip artifact
|
||||
run: unzip pr-comment-data.zip
|
||||
- name: Set env variables
|
||||
id: set-env
|
||||
run: |
|
||||
echo "pr-number=$(cat ./pr_number)" >> $GITHUB_OUTPUT
|
||||
echo "workflow-id=$(cat ./workflow_id)" >> $GITHUB_OUTPUT
|
||||
comment-pr:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
needs: get-pr-data
|
||||
steps:
|
||||
- name: Find Comment
|
||||
uses: peter-evans/find-comment@v2
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: Automated tests Summary
|
||||
- name: Remove previous comment
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: ${{ steps.fc.outputs.comment-id }}
|
||||
})
|
||||
- name: Passing tests comment
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||
body: |
|
||||
<h1>Automated tests Summary</h1>
|
||||
<h3><strong>:white_check_mark:</strong> All the CI tests have passed!</h3>
|
||||
- name: Failing tests comment
|
||||
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||
uses: peter-evans/create-or-update-comment@v2
|
||||
with:
|
||||
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||
body: |
|
||||
<h1> Automated tests Summary</h1>
|
||||
<h3><strong>:rotating_light:</strong> Test workflow has failed</h3>
|
||||
|
||||
___
|
||||
|
||||
[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ needs.get-pr-data.outputs.workflow-id }}) to check the action test reports
|
@ -41,6 +41,7 @@ trait SystemConfiguration {
|
||||
lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43)
|
||||
lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true)
|
||||
lazy val dialInApprovalAudioPath = Try(config.getString("voiceConf.dialInApprovalAudioPath")).getOrElse("ivr/ivr-please_hold_while_party_contacted.wav")
|
||||
lazy val toggleListenOnlyAfterMuteTimer = Try(config.getInt("voiceConf.toggleListenOnlyAfterMuteTimer")).getOrElse(4)
|
||||
|
||||
lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.{ SharedNotesRevDAO }
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -9,7 +10,6 @@ trait PadContentSysMsgHdlr {
|
||||
this: PadsApp2x =>
|
||||
|
||||
def handle(msg: PadContentSysMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
|
||||
|
||||
def broadcastEvent(externalId: String, padId: String, rev: String, start: Int, end: Int, text: String): Unit = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(PadContentEvtMsg.NAME, routing)
|
||||
@ -22,8 +22,18 @@ trait PadContentSysMsgHdlr {
|
||||
}
|
||||
|
||||
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, msg.body.padId, msg.body.rev, msg.body.start, msg.body.end, msg.body.text)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesRevDAO.update(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
group.externalId,
|
||||
msg.body.rev.toInt,
|
||||
msg.body.start,
|
||||
msg.body.end,
|
||||
msg.body.text
|
||||
)
|
||||
broadcastEvent(group.externalId, msg.body.padId, msg.body.rev, msg.body.start, msg.body.end, msg.body.text)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.SharedNotesDAO
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -22,8 +23,11 @@ trait PadCreatedEvtMsgHdlr {
|
||||
}
|
||||
|
||||
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, group.userId, msg.body.padId, msg.body.name)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesDAO.insert(liveMeeting.props.meetingProp.intId, group, msg.body.padId, msg.body.name)
|
||||
broadcastEvent(group.externalId, group.userId, msg.body.padId, msg.body.name)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.SharedNotesDAO
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -29,8 +30,11 @@ trait PadPinnedReqMsgHdlr extends RightsManagementTrait {
|
||||
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
|
||||
} else {
|
||||
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, msg.body.pinned)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesDAO.updatePinned(liveMeeting.props.meetingProp.intId, msg.body.externalId, msg.body.pinned)
|
||||
broadcastEvent(group.externalId, msg.body.pinned)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.SharedNotesSessionDAO
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -22,8 +23,11 @@ trait PadSessionCreatedEvtMsgHdlr {
|
||||
}
|
||||
|
||||
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesSessionDAO.insert(liveMeeting.props.meetingProp.intId, group.externalId, msg.body.userId, msg.body.sessionId)
|
||||
broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.SharedNotesSessionDAO
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -22,8 +23,11 @@ trait PadSessionDeletedSysMsgHdlr {
|
||||
}
|
||||
|
||||
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesSessionDAO.delete(msg.body.userId, msg.body.sessionId)
|
||||
broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.{ SharedNotesRevDAO }
|
||||
import org.bigbluebutton.core.models.Pads
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
@ -22,8 +23,11 @@ trait PadUpdatedSysMsgHdlr {
|
||||
}
|
||||
|
||||
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
|
||||
case Some(group) => broadcastEvent(group.externalId, msg.body.padId, msg.body.userId, msg.body.rev, msg.body.changeset)
|
||||
case _ =>
|
||||
case Some(group) => {
|
||||
SharedNotesRevDAO.insert(liveMeeting.props.meetingProp.intId, group.externalId, msg.body.rev, msg.body.userId, msg.body.changeset)
|
||||
broadcastEvent(group.externalId, msg.body.padId, msg.body.userId, msg.body.rev, msg.body.changeset)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
package org.bigbluebutton.core.apps.presentationpod
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
||||
import org.bigbluebutton.core.bus.MessageBus
|
||||
import org.bigbluebutton.core.db.PresPageDAO
|
||||
import org.bigbluebutton.core.domain.MeetingState2x
|
||||
import org.bigbluebutton.core.running.LiveMeeting
|
||||
|
||||
trait AddSlidePositionsPubMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
this: PresentationPodHdlrs =>
|
||||
|
||||
def handle(msg: AddSlidePositionsPubMsg, state: MeetingState2x,
|
||||
liveMeeting: LiveMeeting, bus: MessageBus) = {
|
||||
PresPageDAO.addSlidePosition(msg.body.slideId, msg.body.width, msg.body.height,
|
||||
msg.body.viewBoxWidth, msg.body.viewBoxHeight)
|
||||
state
|
||||
}
|
||||
}
|
@ -166,7 +166,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
||||
|
||||
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
|
||||
|
||||
val fileURI = List("bigbluebutton", "presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString("", File.separator, "")
|
||||
val fileURI = List("presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString("", File.separator, "")
|
||||
val event = buildNewPresFileAvailable(fileURI, presId, m.body.typeOfExport)
|
||||
|
||||
handle(event, liveMeeting, bus)
|
||||
|
@ -54,7 +54,9 @@ trait PresentationPageConvertedSysMsgHdlr {
|
||||
msg.body.page.id,
|
||||
msg.body.page.num,
|
||||
msg.body.page.urls,
|
||||
msg.body.page.current
|
||||
msg.body.page.current,
|
||||
width = msg.body.page.width,
|
||||
height = msg.body.page.height
|
||||
)
|
||||
|
||||
val newState = for {
|
||||
|
@ -21,7 +21,6 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
|
||||
with PresentationUploadTokenReqMsgHdlr
|
||||
with MakePresentationDownloadReqMsgHdlr
|
||||
with ResizeAndMovePagePubMsgHdlr
|
||||
with AddSlidePositionsPubMsgHdlr
|
||||
with SlideResizedPubMsgHdlr
|
||||
with SyncGetPresentationPodsMsgHdlr
|
||||
with RemovePresentationPodPubMsgHdlr
|
||||
|
@ -86,7 +86,9 @@ object PresentationPodsApp {
|
||||
xOffset = page.xOffset,
|
||||
yOffset = page.yOffset,
|
||||
widthRatio = page.widthRatio,
|
||||
heightRatio = page.heightRatio
|
||||
heightRatio = page.heightRatio,
|
||||
width = page.width,
|
||||
height = page.height
|
||||
)
|
||||
}
|
||||
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable,
|
||||
|
@ -9,9 +9,9 @@ trait UserConnectedToGlobalAudioMsgHdlr {
|
||||
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg) {
|
||||
def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg): Unit = {
|
||||
log.info("Handling UserConnectedToGlobalAudio: meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId)
|
||||
|
||||
|
||||
def broadcastEvent(vu: VoiceUserState): Unit = {
|
||||
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, props.meetingProp.intId,
|
||||
vu.intId)
|
||||
@ -44,6 +44,8 @@ trait UserConnectedToGlobalAudioMsgHdlr {
|
||||
System.currentTimeMillis(),
|
||||
floor = false,
|
||||
lastFloorTime = "0",
|
||||
hold = false,
|
||||
uuid = "unused"
|
||||
)
|
||||
|
||||
VoiceUsers.add(liveMeeting.voiceUsers, vu)
|
||||
|
@ -0,0 +1,21 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.running.{ MeetingActor, LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait ChannelHoldChangedVoiceConfEvtMsgHdlr {
|
||||
this: MeetingActor =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleChannelHoldChangedVoiceConfEvtMsg(msg: ChannelHoldChangedVoiceConfEvtMsg): Unit = {
|
||||
VoiceApp.handleChannelHoldChanged(
|
||||
liveMeeting,
|
||||
outGW,
|
||||
msg.body.intId,
|
||||
msg.body.uuid,
|
||||
msg.body.hold
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.core.models.VoiceUsers
|
||||
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
|
||||
|
||||
trait ListenOnlyModeToggledInSfuEvtMsgHdlr {
|
||||
this: BaseMeetingActor =>
|
||||
|
||||
val liveMeeting: LiveMeeting
|
||||
val outGW: OutMsgRouter
|
||||
|
||||
def handleListenOnlyModeToggledInSfuEvtMsg(msg: ListenOnlyModeToggledInSfuEvtMsg): Unit = {
|
||||
for {
|
||||
vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
|
||||
} yield {
|
||||
VoiceApp.holdChannelInVoiceConf(
|
||||
liveMeeting,
|
||||
outGW,
|
||||
vu.uuid,
|
||||
msg.body.enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -93,7 +93,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
||||
userColor,
|
||||
msg.body.muted,
|
||||
msg.body.talking,
|
||||
"freeswitch"
|
||||
"freeswitch",
|
||||
msg.body.hold,
|
||||
msg.body.uuid
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.bigbluebutton.core.apps.voice
|
||||
|
||||
import akka.actor.{ ActorContext, ActorSystem, Cancellable }
|
||||
import org.bigbluebutton.SystemConfiguration
|
||||
import org.bigbluebutton.LockSettingsUtil
|
||||
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
|
||||
@ -12,9 +13,14 @@ import org.bigbluebutton.core.models._
|
||||
import org.bigbluebutton.core.apps.users.UsersApp
|
||||
import org.bigbluebutton.core.util.ColorPicker
|
||||
import org.bigbluebutton.core.util.TimeUtil
|
||||
import scala.collection.immutable.Map
|
||||
import scala.concurrent.duration._
|
||||
|
||||
|
||||
object VoiceApp extends SystemConfiguration {
|
||||
// Key is userId
|
||||
var toggleListenOnlyTasks: Map[String, Cancellable] = Map()
|
||||
|
||||
def genRecordPath(
|
||||
recordDir: String,
|
||||
meetingId: String,
|
||||
@ -104,7 +110,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
outGW: OutMsgRouter,
|
||||
voiceUserId: String,
|
||||
muted: Boolean
|
||||
): Unit = {
|
||||
)(implicit context: ActorContext): Unit = {
|
||||
for {
|
||||
mutedUser <- VoiceUsers.userMuted(liveMeeting.voiceUsers, voiceUserId, muted)
|
||||
} yield {
|
||||
@ -117,13 +123,32 @@ object VoiceApp extends SystemConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
broadcastUserMutedVoiceEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
mutedUser,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
outGW
|
||||
// Ask for the audio channel to be switched to listen only mode
|
||||
// if the user is muted, otherwise switch back to normal mode
|
||||
// This is only effective if the "transparent listen only" mode is active
|
||||
// for the target user.
|
||||
toggleListenOnlyMode(
|
||||
liveMeeting,
|
||||
outGW,
|
||||
mutedUser.intId,
|
||||
muted,
|
||||
toggleListenOnlyAfterMuteTimer
|
||||
)
|
||||
|
||||
// If the user is muted or unmuted with an unheld channel, broadcast
|
||||
// the event right away.
|
||||
// If the user is unmuted, but channel is held, we need to wait for the
|
||||
// channel to be active again to broadcast the event. See
|
||||
// VoiceApp.handleChannelHoldChanged for this second case.
|
||||
if (muted || (!muted && !mutedUser.hold)) {
|
||||
broadcastUserMutedVoiceEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
mutedUser,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
outGW
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +157,7 @@ object VoiceApp extends SystemConfiguration {
|
||||
outGW: OutMsgRouter,
|
||||
eventBus: InternalEventBus,
|
||||
users: Vector[ConfVoiceUser]
|
||||
): Unit = {
|
||||
)(implicit context: ActorContext): Unit = {
|
||||
users foreach { cvu =>
|
||||
VoiceUsers.findWithVoiceUserId(
|
||||
liveMeeting.voiceUsers,
|
||||
@ -179,7 +204,9 @@ object VoiceApp extends SystemConfiguration {
|
||||
ColorPicker.nextColor(liveMeeting.props.meetingProp.intId),
|
||||
cvu.muted,
|
||||
cvu.talking,
|
||||
cvu.calledInto
|
||||
cvu.calledInto,
|
||||
cvu.hold,
|
||||
cvu.uuid,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -229,7 +256,9 @@ object VoiceApp extends SystemConfiguration {
|
||||
color: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
callingInto: String
|
||||
callingInto: String,
|
||||
hold: Boolean,
|
||||
uuid: String = "unused"
|
||||
): Unit = {
|
||||
|
||||
def broadcastEvent(voiceUserState: VoiceUserState): Unit = {
|
||||
@ -289,7 +318,9 @@ object VoiceApp extends SystemConfiguration {
|
||||
callingInto,
|
||||
System.currentTimeMillis(),
|
||||
floor = false,
|
||||
lastFloorTime = "0"
|
||||
lastFloorTime = "0",
|
||||
hold,
|
||||
uuid
|
||||
)
|
||||
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
|
||||
|
||||
@ -431,4 +462,108 @@ object VoiceApp extends SystemConfiguration {
|
||||
)
|
||||
outGW.send(deafEvent)
|
||||
}
|
||||
|
||||
def removeToggleListenOnlyTask(userId: String): Unit = {
|
||||
toggleListenOnlyTasks get userId match {
|
||||
case Some(task) =>
|
||||
task.cancel()
|
||||
toggleListenOnlyTasks = toggleListenOnlyTasks - userId
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
def toggleListenOnlyMode(
|
||||
liveMeeting: LiveMeeting,
|
||||
outGW: OutMsgRouter,
|
||||
userId: String,
|
||||
enabled: Boolean,
|
||||
delay: Int = 0
|
||||
)(implicit context: ActorContext): Unit = {
|
||||
implicit def executionContext = context.system.dispatcher
|
||||
def broacastEvent(): Unit = {
|
||||
val event = MsgBuilder.buildToggleListenOnlyModeSysMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
userId,
|
||||
enabled
|
||||
)
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
// Guarantee there are no other tasks for this channel
|
||||
removeToggleListenOnlyTask(userId)
|
||||
|
||||
if (enabled && delay > 0) {
|
||||
// If we are enabling listen only mode, we wait a bit before actually
|
||||
// dispatching the command - the idea is that recently muted users
|
||||
// are more likely to unmute themselves right after the action, so this
|
||||
// should make frequent mute-unmute transitions smoother.
|
||||
// This is just one of the heuristics we have to implement for this to
|
||||
// work seamlessly, but it's a start. - prlanzarin Aug 04 2023
|
||||
val newTask = context.system.scheduler.scheduleOnce(delay seconds) {
|
||||
broacastEvent()
|
||||
removeToggleListenOnlyTask(userId)
|
||||
}
|
||||
|
||||
toggleListenOnlyTasks = toggleListenOnlyTasks + (userId -> newTask)
|
||||
} else {
|
||||
// If we are disabling listen only mode, we can broadcast the event
|
||||
// right away
|
||||
broacastEvent()
|
||||
}
|
||||
}
|
||||
|
||||
def holdChannelInVoiceConf(
|
||||
liveMeeting: LiveMeeting,
|
||||
outGW: OutMsgRouter,
|
||||
uuid: String,
|
||||
hold: Boolean
|
||||
): Unit = {
|
||||
val event = MsgBuilder.buildHoldChannelInVoiceConfSysMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
uuid,
|
||||
hold
|
||||
)
|
||||
|
||||
outGW.send(event)
|
||||
}
|
||||
|
||||
def handleChannelHoldChanged(
|
||||
liveMeeting: LiveMeeting,
|
||||
outGW: OutMsgRouter,
|
||||
intId: String,
|
||||
uuid: String,
|
||||
hold: Boolean
|
||||
)(implicit context: ActorContext): Unit = {
|
||||
VoiceUsers.holdStateChanged(
|
||||
liveMeeting.voiceUsers,
|
||||
intId,
|
||||
uuid,
|
||||
hold
|
||||
) match {
|
||||
case Some(vu) =>
|
||||
// Mute vs hold state mismatch, enforce hold state again.
|
||||
// Mute state is the predominant one here.
|
||||
if (vu.muted != hold) {
|
||||
toggleListenOnlyMode(
|
||||
liveMeeting,
|
||||
outGW,
|
||||
intId,
|
||||
vu.muted
|
||||
)
|
||||
}
|
||||
|
||||
// User unmuted and channel is not on hold, broadcast user unmuted
|
||||
if (!vu.muted && !vu.hold) {
|
||||
broadcastUserMutedVoiceEvtMsg(
|
||||
liveMeeting.props.meetingProp.intId,
|
||||
vu,
|
||||
liveMeeting.props.voiceProp.voiceConf,
|
||||
outGW
|
||||
)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,9 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr
|
||||
with SyncGetVoiceUsersMsgHdlr
|
||||
with AudioFloorChangedVoiceConfEvtMsgHdlr
|
||||
with VoiceConfCallStateEvtMsgHdlr
|
||||
with UserStatusVoiceConfEvtMsgHdlr {
|
||||
with UserStatusVoiceConfEvtMsgHdlr
|
||||
with ChannelHoldChangedVoiceConfEvtMsgHdlr
|
||||
with ListenOnlyModeToggledInSfuEvtMsgHdlr {
|
||||
|
||||
this: MeetingActor =>
|
||||
}
|
||||
|
@ -74,19 +74,6 @@ object PresPageDAO {
|
||||
}
|
||||
}
|
||||
|
||||
def addSlidePosition(slideId: String, width: Double, height: Double,
|
||||
viewBoxWidth: Double, viewBoxHeight: Double) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[PresPageDbTableDef]
|
||||
.filter(_.pageId === slideId)
|
||||
.map(p => (p.width, p.height, p.viewBoxWidth, p.viewBoxHeight))
|
||||
.update((width, height, viewBoxWidth, viewBoxHeight))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) added slide position on PresPage table")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating slide position on PresPage: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updateSlidePosition(pageId: String, width: Double, height: Double, xOffset: Double, yOffset: Double,
|
||||
widthRatio: Double, heightRatio: Double) = {
|
||||
DatabaseConnection.db.run(
|
||||
|
@ -58,8 +58,8 @@ object PresPresentationDAO {
|
||||
yOffset = page._2.yOffset,
|
||||
widthRatio = page._2.widthRatio,
|
||||
heightRatio = page._2.heightRatio,
|
||||
width = 1,
|
||||
height = 1,
|
||||
width = page._2.width,
|
||||
height = page._2.height,
|
||||
viewBoxWidth = 1,
|
||||
viewBoxHeight = 1,
|
||||
maxImageWidth = 1440,
|
||||
|
@ -0,0 +1,60 @@
|
||||
package org.bigbluebutton.core.db
|
||||
import org.bigbluebutton.core.models.PadGroup
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class SharedNotesDbModel(
|
||||
meetingId: String,
|
||||
sharedNotesExtId: String,
|
||||
padId: String,
|
||||
model: String,
|
||||
name: String,
|
||||
pinned: Boolean
|
||||
)
|
||||
|
||||
class SharedNotesDbTableDef(tag: Tag) extends Table[SharedNotesDbModel](tag, None, "sharedNotes") {
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||
val padId = column[String]("padId")
|
||||
val model = column[String]("model")
|
||||
val name = column[String]("name")
|
||||
val pinned = column[Boolean]("pinned")
|
||||
val * = (
|
||||
meetingId, sharedNotesExtId, padId, model, name, pinned
|
||||
) <> (SharedNotesDbModel.tupled, SharedNotesDbModel.unapply)
|
||||
}
|
||||
|
||||
object SharedNotesDAO {
|
||||
def insert(meetingId: String, group: PadGroup, padId: String, name: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesDbTableDef].insertOrUpdate(
|
||||
SharedNotesDbModel(
|
||||
meetingId = meetingId,
|
||||
sharedNotesExtId = group.externalId,
|
||||
padId = padId,
|
||||
model = group.model,
|
||||
name = name,
|
||||
pinned = false
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotes table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotes: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def updatePinned(meetingId: String, sharedNotesExtId: String, pinned: Boolean) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.filter(_.sharedNotesExtId === sharedNotesExtId)
|
||||
.map(n => n.pinned)
|
||||
.update(pinned)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated pinned on SharedNotes table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating pinned SharedNotes: $e")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package org.bigbluebutton.core.db
|
||||
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class SharedNotesRevDbModel(
|
||||
meetingId: String,
|
||||
sharedNotesExtId: String,
|
||||
rev: Int,
|
||||
userId: String,
|
||||
changeset: String,
|
||||
start: Option[Int],
|
||||
end: Option[Int],
|
||||
diff: Option[String],
|
||||
createdAt: java.sql.Timestamp
|
||||
)
|
||||
|
||||
class SharedNotesRevDbTableDef(tag: Tag) extends Table[SharedNotesRevDbModel](tag, None, "sharedNotes_rev") {
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||
val rev = column[Int]("rev", O.PrimaryKey)
|
||||
val userId = column[String]("userId")
|
||||
val changeset = column[String]("changeset")
|
||||
val start = column[Option[Int]]("start")
|
||||
val end = column[Option[Int]]("end")
|
||||
val diff = column[Option[String]]("diff")
|
||||
val createdAt = column[java.sql.Timestamp]("createdAt")
|
||||
val * = (meetingId, sharedNotesExtId, rev, userId, changeset, start, end, diff, createdAt) <> (SharedNotesRevDbModel.tupled, SharedNotesRevDbModel.unapply)
|
||||
}
|
||||
|
||||
object SharedNotesRevDAO {
|
||||
def insert(meetingId: String, sharedNotesExtId: String, revId: Int, userId: String, changeset: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesRevDbTableDef].insertOrUpdate(
|
||||
SharedNotesRevDbModel(
|
||||
meetingId = meetingId,
|
||||
sharedNotesExtId = sharedNotesExtId,
|
||||
rev = revId,
|
||||
userId = userId,
|
||||
changeset = changeset,
|
||||
start = None,
|
||||
end = None,
|
||||
diff = None,
|
||||
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesRev table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesRev: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def update(meetingId: String, sharedNotesExtId: String, revId: Int, start: Int, end: Int, text: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesRevDbTableDef]
|
||||
.filter(_.meetingId === meetingId)
|
||||
.filter(_.sharedNotesExtId === sharedNotesExtId)
|
||||
.filter(_.rev === revId)
|
||||
.map(n => (n.start, n.end, n.diff))
|
||||
.update((Some(start), Some(end), Some(text)))
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated Rev on SharedNotes table!")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error updating Rev SharedNotes: $e")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.bigbluebutton.core.db
|
||||
import slick.jdbc.PostgresProfile.api._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.util.{ Failure, Success }
|
||||
|
||||
case class SharedNotesSessionDbModel(
|
||||
meetingId: String,
|
||||
sharedNotesExtId: String,
|
||||
userId: String,
|
||||
sessionId: String
|
||||
)
|
||||
|
||||
class SharedNotesSessionDbTableDef(tag: Tag) extends Table[SharedNotesSessionDbModel](tag, None, "sharedNotes_session") {
|
||||
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||
val userId = column[String]("userId", O.PrimaryKey)
|
||||
val sessionId = column[String]("sessionId")
|
||||
val * = (meetingId, sharedNotesExtId, userId, sessionId) <> (SharedNotesSessionDbModel.tupled, SharedNotesSessionDbModel.unapply)
|
||||
}
|
||||
|
||||
object SharedNotesSessionDAO {
|
||||
def insert(meetingId: String, sharedNotesExtId: String, userId: String, sessionId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesSessionDbTableDef].insertOrUpdate(
|
||||
SharedNotesSessionDbModel(
|
||||
meetingId = meetingId,
|
||||
sharedNotesExtId = sharedNotesExtId,
|
||||
userId = userId,
|
||||
sessionId = sessionId
|
||||
)
|
||||
)
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesSession table!")
|
||||
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesSession: $e")
|
||||
}
|
||||
}
|
||||
|
||||
def delete(intId: String, sessionId: String) = {
|
||||
DatabaseConnection.db.run(
|
||||
TableQuery[SharedNotesSessionDbTableDef]
|
||||
.filter(_.userId === intId)
|
||||
.filter(_.sessionId === sessionId)
|
||||
.delete
|
||||
).onComplete {
|
||||
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"SharedNotesSession ${sessionId} deleted")
|
||||
case Failure(e) => DatabaseConnection.logger.error(s"Error deleting SharedNotesSession ${sessionId}: $e")
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,9 @@ case class PresentationPage(
|
||||
xOffset: Double = 0,
|
||||
yOffset: Double = 0,
|
||||
widthRatio: Double = 100D,
|
||||
heightRatio: Double = 100D
|
||||
heightRatio: Double = 100D,
|
||||
width: Double = 1440D,
|
||||
height: Double = 1080D
|
||||
)
|
||||
|
||||
object PresentationInPod {
|
||||
|
@ -12,6 +12,10 @@ object VoiceUsers {
|
||||
users.toVector.find(u => u.intId == intId)
|
||||
}
|
||||
|
||||
def findWithIntIdAndUUID(users: VoiceUsers, intId: String, uuid: String): Option[VoiceUserState] = {
|
||||
users.toVector.find(u => u.uuid == uuid && u.intId == intId)
|
||||
}
|
||||
|
||||
def findAll(users: VoiceUsers): Vector[VoiceUserState] = users.toVector
|
||||
|
||||
def findAllNonListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == false)
|
||||
@ -100,6 +104,17 @@ object VoiceUsers {
|
||||
}
|
||||
}
|
||||
|
||||
def holdStateChanged(users: VoiceUsers, intId: String, uuid: String, hold: Boolean): Option[VoiceUserState] = {
|
||||
for {
|
||||
u <- findWithIntIdAndUUID(users, intId, uuid)
|
||||
} yield {
|
||||
val vu = u.modify(_.hold).setTo(hold)
|
||||
.modify(_.lastStatusUpdateOn).setTo(System.currentTimeMillis())
|
||||
users.save(vu)
|
||||
vu
|
||||
}
|
||||
}
|
||||
|
||||
def setLastStatusUpdate(users: VoiceUsers, user: VoiceUserState): VoiceUserState = {
|
||||
val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis())
|
||||
users.save(vu)
|
||||
@ -174,7 +189,9 @@ case class VoiceUserVO2x(
|
||||
callingWith: String,
|
||||
listenOnly: Boolean,
|
||||
floor: Boolean,
|
||||
lastFloorTime: String
|
||||
lastFloorTime: String,
|
||||
hold: Boolean,
|
||||
uuid: String
|
||||
)
|
||||
|
||||
case class VoiceUserState(
|
||||
@ -190,5 +207,7 @@ case class VoiceUserState(
|
||||
calledInto: String,
|
||||
lastStatusUpdateOn: Long,
|
||||
floor: Boolean,
|
||||
lastFloorTime: String
|
||||
lastFloorTime: String,
|
||||
hold: Boolean,
|
||||
uuid: String
|
||||
)
|
||||
|
@ -223,6 +223,10 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[GetGlobalAudioPermissionReqMsg](envelope, jsonNode)
|
||||
case GetMicrophonePermissionReqMsg.NAME =>
|
||||
routeGenericMsg[GetMicrophonePermissionReqMsg](envelope, jsonNode)
|
||||
case ChannelHoldChangedVoiceConfEvtMsg.NAME =>
|
||||
routeVoiceMsg[ChannelHoldChangedVoiceConfEvtMsg](envelope, jsonNode)
|
||||
case ListenOnlyModeToggledInSfuEvtMsg.NAME =>
|
||||
routeVoiceMsg[ListenOnlyModeToggledInSfuEvtMsg](envelope, jsonNode)
|
||||
|
||||
// Breakout rooms
|
||||
case BreakoutRoomsListMsg.NAME =>
|
||||
@ -292,8 +296,6 @@ class ReceivedJsonMsgHandlerActor(
|
||||
routeGenericMsg[SetCurrentPagePubMsg](envelope, jsonNode)
|
||||
case ResizeAndMovePagePubMsg.NAME =>
|
||||
routeGenericMsg[ResizeAndMovePagePubMsg](envelope, jsonNode)
|
||||
case AddSlidePositionsPubMsg.NAME =>
|
||||
routeGenericMsg[AddSlidePositionsPubMsg](envelope, jsonNode)
|
||||
case SlideResizedPubMsg.NAME =>
|
||||
routeGenericMsg[SlideResizedPubMsg](envelope, jsonNode)
|
||||
case RemovePresentationPubMsg.NAME =>
|
||||
|
@ -481,6 +481,10 @@ class MeetingActor(
|
||||
handleGetGlobalAudioPermissionReqMsg(m)
|
||||
case m: GetMicrophonePermissionReqMsg =>
|
||||
handleGetMicrophonePermissionReqMsg(m)
|
||||
case m: ChannelHoldChangedVoiceConfEvtMsg =>
|
||||
handleChannelHoldChangedVoiceConfEvtMsg(m)
|
||||
case m: ListenOnlyModeToggledInSfuEvtMsg =>
|
||||
handleListenOnlyModeToggledInSfuEvtMsg(m)
|
||||
|
||||
// Layout
|
||||
case m: GetCurrentLayoutReqMsg => handleGetCurrentLayoutReqMsg(m)
|
||||
@ -536,7 +540,6 @@ class MeetingActor(
|
||||
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationUploadTokenReqMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: ResizeAndMovePagePubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: AddSlidePositionsPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: SlideResizedPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationPageConvertedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
case m: PresentationPageConversionStartedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
|
||||
|
@ -102,6 +102,10 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
|
||||
logMessage(msg)
|
||||
case m: VoiceConfCallStateEvtMsg => logMessage(msg)
|
||||
case m: VoiceCallStateEvtMsg => logMessage(msg)
|
||||
case m: HoldChannelInVoiceConfSysMsg => logMessage(msg)
|
||||
case m: ChannelHoldChangedVoiceConfEvtMsg => logMessage(msg)
|
||||
case m: ToggleListenOnlyModeSysMsg => logMessage(msg)
|
||||
case m: ListenOnlyModeToggledInSfuEvtMsg => logMessage(msg)
|
||||
|
||||
// Breakout
|
||||
case m: BreakoutRoomEndedEvtMsg => logMessage(msg)
|
||||
|
@ -67,6 +67,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
|
||||
msgSender.send(toVoiceConfRedisChannel, json)
|
||||
case GetUsersStatusToVoiceConfSysMsg.NAME =>
|
||||
msgSender.send(toVoiceConfRedisChannel, json)
|
||||
case HoldChannelInVoiceConfSysMsg.NAME =>
|
||||
msgSender.send(toVoiceConfRedisChannel, json)
|
||||
|
||||
// Sent to SFU
|
||||
case EjectUserFromSfuSysMsg.NAME =>
|
||||
@ -75,6 +77,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
|
||||
msgSender.send(toSfuRedisChannel, json)
|
||||
case CamStreamUnsubscribeSysMsg.NAME =>
|
||||
msgSender.send(toSfuRedisChannel, json)
|
||||
case ToggleListenOnlyModeSysMsg.NAME =>
|
||||
msgSender.send(toSfuRedisChannel, json)
|
||||
|
||||
//==================================================================
|
||||
// Send chat, presentation, and whiteboard in different channels so as not to
|
||||
|
@ -45,7 +45,9 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
|
||||
dialInUser.color,
|
||||
MeetingStatus2x.isMeetingMuted(liveMeeting.status),
|
||||
false,
|
||||
"freeswitch"
|
||||
"freeswitch",
|
||||
false,
|
||||
"unused"
|
||||
)
|
||||
VoiceUsers.findWithIntId(
|
||||
liveMeeting.voiceUsers,
|
||||
|
@ -628,4 +628,34 @@ object MsgBuilder {
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildHoldChannelInVoiceConfSysMsg(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
uuid: String,
|
||||
hold: Boolean
|
||||
): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(HoldChannelInVoiceConfSysMsg.NAME, routing)
|
||||
val body = HoldChannelInVoiceConfSysMsgBody(voiceConf, uuid, hold)
|
||||
val header = BbbCoreHeaderWithMeetingId(HoldChannelInVoiceConfSysMsg.NAME, meetingId)
|
||||
val event = HoldChannelInVoiceConfSysMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
def buildToggleListenOnlyModeSysMsg(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
enabled: Boolean
|
||||
): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
|
||||
val envelope = BbbCoreEnvelope(ToggleListenOnlyModeSysMsg.NAME, routing)
|
||||
val body = ToggleListenOnlyModeSysMsgBody(voiceConf, userId, enabled)
|
||||
val header = BbbCoreHeaderWithMeetingId(ToggleListenOnlyModeSysMsg.NAME, meetingId)
|
||||
val event = ToggleListenOnlyModeSysMsg(header, body)
|
||||
|
||||
BbbCommonEnvCoreMsg(envelope, event)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -66,7 +66,9 @@ object FakeUserGenerator {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
val lastFloorTime = System.currentTimeMillis().toString();
|
||||
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
|
||||
callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
|
||||
callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime,
|
||||
false,
|
||||
"9b3f4504-275d-4315-9922-21174262d88c")
|
||||
}
|
||||
|
||||
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
|
||||
@ -76,7 +78,9 @@ object FakeUserGenerator {
|
||||
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
|
||||
val lastFloorTime = System.currentTimeMillis().toString();
|
||||
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
|
||||
callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
|
||||
callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime,
|
||||
false,
|
||||
"9b3f4504-275d-4315-9922-21174262d88c")
|
||||
}
|
||||
|
||||
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {
|
||||
|
@ -25,7 +25,9 @@ object TestDataGen {
|
||||
listenOnly: Boolean): VoiceUserState = {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
|
||||
callerNum = user.name, "#ff6242", muted, talking, listenOnly)
|
||||
callerNum = user.name, "#ff6242", muted, talking, listenOnly,
|
||||
false,
|
||||
"9b3f4504-275d-4315-9922-21174262d88c")
|
||||
}
|
||||
|
||||
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
|
||||
@ -33,7 +35,9 @@ object TestDataGen {
|
||||
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
|
||||
val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
|
||||
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
|
||||
callerNum = name, "#ff6242", muted, talking, listenOnly)
|
||||
callerNum = name, "#ff6242", muted, talking, listenOnly
|
||||
false,
|
||||
"9b3f4504-275d-4315-9922-21174262d88c")
|
||||
}
|
||||
|
||||
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {
|
||||
|
@ -107,6 +107,10 @@ voiceConf {
|
||||
# Path to the audio file being played when dial-in user is waiting for
|
||||
# approval. This can be relative to FreeSWITCH sounds folder
|
||||
dialInApprovalAudioPath = "ivr/ivr-please_hold_while_party_contacted.wav"
|
||||
|
||||
# Time (seconds) to wait before requesting an audio channel hold after
|
||||
# muting a user. Used in the experimental, transparent listen only mode.
|
||||
toggleListenOnlyAfterMuteTimer = 4
|
||||
}
|
||||
|
||||
recording {
|
||||
@ -117,4 +121,4 @@ recording {
|
||||
transcript {
|
||||
words = 8 # per line
|
||||
lines = 2
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +57,18 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
|
||||
if (event instanceof VoiceUserJoinedEvent) {
|
||||
VoiceUserJoinedEvent evt = (VoiceUserJoinedEvent) event;
|
||||
vcs.userJoinedVoiceConf(evt.getRoom(), evt.getVoiceUserId(), evt.getUserId(), evt.getCallerIdName(),
|
||||
evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith());
|
||||
} else if (event instanceof VoiceConfRunningEvent) {
|
||||
evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith(),
|
||||
evt.getHold(),
|
||||
evt.getUUID());
|
||||
} else if (event instanceof ChannelHoldChangedEvent) {
|
||||
ChannelHoldChangedEvent evt = (ChannelHoldChangedEvent) event;
|
||||
vcs.channelHoldChanged(
|
||||
evt.getRoom(),
|
||||
evt.getUserId(),
|
||||
evt.getUUID(),
|
||||
evt.isHeld()
|
||||
);
|
||||
} else if (event instanceof VoiceConfRunningEvent) {
|
||||
VoiceConfRunningEvent evt = (VoiceConfRunningEvent) event;
|
||||
vcs.voiceConfRunning(evt.getRoom(), evt.isRunning());
|
||||
} else if (event instanceof VoiceUserLeftEvent) {
|
||||
|
@ -22,7 +22,9 @@ public interface IVoiceConferenceService {
|
||||
String callerIdNum,
|
||||
Boolean muted,
|
||||
Boolean speaking,
|
||||
String avatarURL);
|
||||
String avatarURL,
|
||||
Boolean hold,
|
||||
String uuid);
|
||||
|
||||
void voiceUsersStatus(String voiceConfId,
|
||||
java.util.List<ConfMember> confMembers,
|
||||
@ -67,4 +69,9 @@ public interface IVoiceConferenceService {
|
||||
Long receivedResponseTimestamp);
|
||||
|
||||
void freeswitchHeartbeatEvent(Map<String, String> heartbeat);
|
||||
|
||||
void channelHoldChanged(String voiceConfId,
|
||||
String userId,
|
||||
String uuid,
|
||||
Boolean hold);
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.freeswitch.voice.events;
|
||||
|
||||
public class ChannelHoldChangedEvent extends VoiceConferenceEvent {
|
||||
|
||||
private final String userId;
|
||||
private final String uuid;
|
||||
private final boolean hold;
|
||||
|
||||
public ChannelHoldChangedEvent(
|
||||
String room,
|
||||
String userId,
|
||||
String uuid,
|
||||
boolean hold
|
||||
) {
|
||||
super(room);
|
||||
this.userId = userId;
|
||||
this.uuid = uuid;
|
||||
this.hold = hold;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public boolean isHeld() {
|
||||
return hold;
|
||||
}
|
||||
|
||||
}
|
@ -9,6 +9,8 @@ public class ConfMember {
|
||||
public final Boolean locked = false;
|
||||
public final String userId;
|
||||
public final String callingWith;
|
||||
public final Boolean hold;
|
||||
public final String uuid;
|
||||
|
||||
public ConfMember(String userId,
|
||||
String voiceUserId,
|
||||
@ -16,7 +18,9 @@ public class ConfMember {
|
||||
String callerIdName,
|
||||
Boolean muted,
|
||||
Boolean speaking,
|
||||
String callingWith) {
|
||||
String callingWith,
|
||||
Boolean hold,
|
||||
String uuid) {
|
||||
this.userId = userId;
|
||||
this.voiceUserId = voiceUserId;
|
||||
this.callerIdName = callerIdName;
|
||||
@ -24,5 +28,7 @@ public class ConfMember {
|
||||
this.muted = muted;
|
||||
this.speaking = speaking;
|
||||
this.callingWith = callingWith;
|
||||
this.hold = hold;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
||||
|
@ -28,10 +28,14 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
|
||||
private final Boolean locked = false;
|
||||
private final String userId;
|
||||
private final String callingWith;
|
||||
private final Boolean hold;
|
||||
private final String uuid;
|
||||
|
||||
public VoiceUserJoinedEvent(String userId, String voiceUserId, String room,
|
||||
String callerIdNum, String callerIdName,
|
||||
Boolean muted, Boolean speaking, String callingWith) {
|
||||
Boolean muted, Boolean speaking, String callingWith,
|
||||
Boolean hold,
|
||||
String uuid) {
|
||||
super(room);
|
||||
this.userId = userId;
|
||||
this.voiceUserId = voiceUserId;
|
||||
@ -40,6 +44,8 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
|
||||
this.muted = muted;
|
||||
this.speaking = speaking;
|
||||
this.callingWith = callingWith;
|
||||
this.hold = hold;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
@ -73,4 +79,12 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
|
||||
public String getCallingWith() {
|
||||
return callingWith;
|
||||
}
|
||||
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public Boolean getHold() {
|
||||
return hold;
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@ public class ConnectionManager {
|
||||
//c.addEventFilter(EVENT_NAME, "background_job");
|
||||
c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE");
|
||||
c.addEventFilter(EVENT_NAME, "CHANNEL_STATE");
|
||||
c.addEventFilter(EVENT_NAME, "CHANNEL_CALLSTATE");
|
||||
subscribed = true;
|
||||
} else {
|
||||
// Let's check for status every minute.
|
||||
@ -239,6 +240,13 @@ public class ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
public void holdChannel(HoldChannelCommand hcc) {
|
||||
Client c = manager.getESLClient();
|
||||
if (c.canSend()) {
|
||||
c.sendAsyncApiCommand(hcc.getCommand(), hcc.getCommandArgs());
|
||||
}
|
||||
}
|
||||
|
||||
public void eject(EjectUserCommand mpc) {
|
||||
Client c = manager.getESLClient();
|
||||
if (c.canSend()) {
|
||||
|
@ -26,6 +26,9 @@ public class ESLEventListener implements IEslEventListener {
|
||||
private static final String CONFERENCE_CREATED_EVENT = "conference-create";
|
||||
private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy";
|
||||
private static final String FLOOR_CHANGE_EVENT = "video-floor-change";
|
||||
private static final String CHANNEL_CALLSTATE_EVENT = "CHANNEL_CALLSTATE";
|
||||
private static final String CHANNEL_CALLSTATE_HELD = "HELD";
|
||||
private static final String CHANNEL_CALLSTATE_ACTIVE = "ACTIVE";
|
||||
|
||||
private final ConferenceEventListener conferenceEventListener;
|
||||
|
||||
@ -59,12 +62,14 @@ public class ESLEventListener implements IEslEventListener {
|
||||
@Override
|
||||
public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event) {
|
||||
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
Map<String, String> headers = event.getEventHeaders();
|
||||
String callerId = this.getCallerIdFromEvent(event);
|
||||
String callerId = this.getCallerId(event);
|
||||
String callerIdName = this.getCallerIdNameFromEvent(event);
|
||||
String channelCallState = this.getChannelCallState(headers);
|
||||
boolean muted = headers.get("Speak").equals("true") ? false : true; //Was inverted which was causing a State issue
|
||||
boolean speaking = headers.get("Talking").equals("true") ? true : false;
|
||||
boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD);
|
||||
|
||||
String voiceUserId = callerIdName;
|
||||
|
||||
@ -124,14 +129,16 @@ public class ESLEventListener implements IEslEventListener {
|
||||
callerIdName,
|
||||
muted,
|
||||
speaking,
|
||||
"none");
|
||||
"none",
|
||||
hold,
|
||||
callerUUID);
|
||||
conferenceEventListener.handleConferenceEvent(pj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event) {
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
String callerId = this.getCallerIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
String callerId = this.getCallerId(event);
|
||||
String callerIdName = this.getCallerIdNameFromEvent(event);
|
||||
|
||||
String callerUUID = this.getMemberUUIDFromEvent(event);
|
||||
@ -146,14 +153,14 @@ public class ESLEventListener implements IEslEventListener {
|
||||
|
||||
@Override
|
||||
public void conferenceEventMute(String uniqueId, String confName, int confSize, EslEvent event) {
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, true);
|
||||
conferenceEventListener.handleConferenceEvent(pm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void conferenceEventUnMute(String uniqueId, String confName, int confSize, EslEvent event) {
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, false);
|
||||
conferenceEventListener.handleConferenceEvent(pm);
|
||||
}
|
||||
@ -165,11 +172,11 @@ public class ESLEventListener implements IEslEventListener {
|
||||
}
|
||||
|
||||
if (action.equals(START_TALKING_EVENT)) {
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, true);
|
||||
conferenceEventListener.handleConferenceEvent(pt);
|
||||
} else if (action.equals(STOP_TALKING_EVENT)) {
|
||||
Integer memberId = this.getMemberIdFromEvent(event);
|
||||
Integer memberId = this.getMemberId(event);
|
||||
VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, false);
|
||||
conferenceEventListener.handleConferenceEvent(pt);
|
||||
} else if (action.equals(CONFERENCE_CREATED_EVENT)) {
|
||||
@ -437,16 +444,92 @@ public class ESLEventListener implements IEslEventListener {
|
||||
);
|
||||
conferenceEventListener.handleConferenceEvent(csEvent);
|
||||
}
|
||||
} else if (event.getEventName().equals(CHANNEL_CALLSTATE_EVENT)) {
|
||||
Map<String, String> eventHeaders = event.getEventHeaders();
|
||||
String channelCallState = this.getChannelCallState(eventHeaders);
|
||||
String originalChannelCallState = eventHeaders.get("Original-Channel-Call-State");
|
||||
if (channelCallState == null
|
||||
|| originalChannelCallState == null
|
||||
|| channelCallState.equals(originalChannelCallState)
|
||||
|| !(channelCallState.equals(CHANNEL_CALLSTATE_HELD) || channelCallState.equals(CHANNEL_CALLSTATE_ACTIVE))) {
|
||||
// No call state info, or no change in call state, or not a call state we care about
|
||||
return;
|
||||
}
|
||||
|
||||
String intId = this.getIntId(event);
|
||||
|
||||
if (intId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD);
|
||||
String uuid = this.getMemberUUIDFromEvent(event);
|
||||
String conference = eventHeaders.get("Caller-Destination-Number");
|
||||
Matcher callerDestNumberMatcher = ECHO_TEST_DEST_PATTERN.matcher(conference);
|
||||
|
||||
if (callerDestNumberMatcher.matches()) {
|
||||
conference = callerDestNumberMatcher.group(1).trim();
|
||||
}
|
||||
|
||||
ChannelHoldChangedEvent csEvent = new ChannelHoldChangedEvent(
|
||||
conference,
|
||||
intId,
|
||||
uuid,
|
||||
hold
|
||||
);
|
||||
conferenceEventListener.handleConferenceEvent(csEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String getIntId(EslEvent event) {
|
||||
return this.getIntId(event.getEventHeaders());
|
||||
}
|
||||
|
||||
private String getIntId(Map<String, String> eventHeaders) {
|
||||
String origCallerIdName = this.getCallerId(eventHeaders);
|
||||
Integer memberId = this.getMemberId(eventHeaders);
|
||||
Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
|
||||
Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
|
||||
if (callWithSess.matches()) {
|
||||
return callWithSess.group(1).trim();
|
||||
} else if (callerListenOnly.matches()) {
|
||||
return callerListenOnly.group(1).trim();
|
||||
} else if (memberId != null) {
|
||||
return "v_" + memberId.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getMemberIdFromEvent(EslEvent e) {
|
||||
return new Integer(e.getEventHeaders().get("Member-ID"));
|
||||
private Integer getMemberId(EslEvent event) {
|
||||
return this.getMemberId(event.getEventHeaders());
|
||||
}
|
||||
|
||||
private String getCallerIdFromEvent(EslEvent e) {
|
||||
return e.getEventHeaders().get("Caller-Caller-ID-Number");
|
||||
private Integer getMemberId(Map<String, String> eventHeaders) {
|
||||
String memberId = eventHeaders.get("Member-ID");
|
||||
|
||||
if (memberId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Integer.valueOf(memberId);
|
||||
}
|
||||
|
||||
private String getCallerId(EslEvent event) {
|
||||
return this.getCallerId(event.getEventHeaders());
|
||||
}
|
||||
|
||||
private String getCallerId(Map<String, String> eventHeaders) {
|
||||
return eventHeaders.get("Caller-Caller-ID-Number");
|
||||
}
|
||||
|
||||
private String getChannelCallState(EslEvent event) {
|
||||
return this.getChannelCallState(event.getEventHeaders());
|
||||
}
|
||||
|
||||
private String getChannelCallState(Map<String, String> eventHeaders) {
|
||||
return eventHeaders.get("Channel-Call-State");
|
||||
}
|
||||
|
||||
private String getMemberUUIDFromEvent(EslEvent e) {
|
||||
|
@ -34,6 +34,7 @@ import org.bigbluebutton.freeswitch.voice.freeswitch.actions.PlaySoundCommand;
|
||||
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.StopSoundCommand;
|
||||
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.RecordConferenceCommand;
|
||||
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.TransferUserToMeetingCommand;
|
||||
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.HoldChannelCommand;
|
||||
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -157,6 +158,11 @@ public class FreeswitchApplication implements IDelayedCommandListener{
|
||||
queueMessage(mpc);
|
||||
}
|
||||
|
||||
public void holdChannel(String voiceConfId, String uuid, Boolean hold) {
|
||||
HoldChannelCommand hcc = new HoldChannelCommand(voiceConfId, uuid, hold, USER);
|
||||
queueMessage(hcc);
|
||||
}
|
||||
|
||||
private Long genTimestamp() {
|
||||
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
|
||||
}
|
||||
@ -220,6 +226,10 @@ public class FreeswitchApplication implements IDelayedCommandListener{
|
||||
manager.forceEjectUser((ForceEjectUserCommand) command);
|
||||
} else if (command instanceof GetUsersStatusCommand) {
|
||||
manager.getUsersStatus((GetUsersStatusCommand) command);
|
||||
} else if (command instanceof HoldChannelCommand) {
|
||||
manager.holdChannel((HoldChannelCommand) command);
|
||||
} else {
|
||||
log.warn("Unknown command: " + command.getCommand());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
log.warn(e.getMessage());
|
||||
|
@ -108,7 +108,9 @@ public class GetAllUsersCommand extends FreeswitchCommand {
|
||||
}
|
||||
|
||||
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(),
|
||||
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none");
|
||||
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none",
|
||||
member.getHold(),
|
||||
uuid);
|
||||
eventListener.handleConferenceEvent(pj);
|
||||
} else if ("recording_node".equals(member.getMemberType())) {
|
||||
|
||||
|
@ -106,7 +106,9 @@ public class GetUsersStatusCommand extends FreeswitchCommand {
|
||||
callerId, callerIdName,
|
||||
member.getMuted(),
|
||||
member.getSpeaking(),
|
||||
"none");
|
||||
"none",
|
||||
member.getHold(),
|
||||
member.getUUID());
|
||||
confMembers.add(confMember);
|
||||
}
|
||||
} else if ("recording_node".equals(member.getMemberType())) {
|
||||
|
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||
*
|
||||
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the
|
||||
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License along
|
||||
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package org.bigbluebutton.freeswitch.voice.freeswitch.actions;
|
||||
|
||||
public class HoldChannelCommand extends FreeswitchCommand {
|
||||
private final String uuid;
|
||||
private final Boolean hold;
|
||||
|
||||
public HoldChannelCommand(String room, String uuid, Boolean hold, String requesterId) {
|
||||
super(room, requesterId);
|
||||
this.uuid = uuid;
|
||||
this.hold = hold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommand() {
|
||||
return "uuid_hold";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandArgs() {
|
||||
if (hold) {
|
||||
return "toggle" + SPACE + uuid;
|
||||
} else {
|
||||
return "off" + SPACE + uuid;
|
||||
}
|
||||
}
|
||||
}
|
@ -62,6 +62,10 @@ public class ConferenceMember {
|
||||
return flags.getIsSpeaking();
|
||||
}
|
||||
|
||||
public boolean getHold() {
|
||||
return flags.getHold();
|
||||
}
|
||||
|
||||
public void setFlags(ConferenceMemberFlags flags) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ public class ConferenceMemberFlags {
|
||||
//private boolean canHear = false;
|
||||
private boolean canSpeak = false;
|
||||
private boolean talking = false;
|
||||
private boolean hold = false;
|
||||
//private boolean hasVideo = false;
|
||||
//private boolean hasFloor = false;
|
||||
//private boolean isModerator = false;
|
||||
@ -51,4 +52,11 @@ public class ConferenceMemberFlags {
|
||||
talking = tempVal.equals("true") ? true : false;
|
||||
}
|
||||
|
||||
void setHold(String tempVal) {
|
||||
hold = tempVal.equals("true") ? true : false;
|
||||
}
|
||||
|
||||
boolean getHold() {
|
||||
return hold;
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +131,8 @@ public class XMLResponseConferenceListParser extends DefaultHandler {
|
||||
tempFlags.setCanSpeak(tempVal);
|
||||
}else if (qName.equalsIgnoreCase("talking")) {
|
||||
tempFlags.setTalking(tempVal);
|
||||
} else if (qName.equalsIgnoreCase("hold")) {
|
||||
tempFlags.setHold(tempVal);
|
||||
}
|
||||
}else if (qName.equalsIgnoreCase("id")) {
|
||||
try {
|
||||
|
@ -243,4 +243,21 @@ trait RxJsonMsgDeserializer {
|
||||
}
|
||||
}
|
||||
|
||||
def routeHoldChannelInVoiceConfMsg(envelope: BbbCoreEnvelope, jsonNode: JsonNode): Unit = {
|
||||
def deserialize(jsonNode: JsonNode): Option[HoldChannelInVoiceConfSysMsg] = {
|
||||
val (result, error) = JsonDeserializer.toBbbCommonMsg[HoldChannelInVoiceConfSysMsg](jsonNode)
|
||||
result match {
|
||||
case Some(msg) => Some(msg.asInstanceOf[HoldChannelInVoiceConfSysMsg])
|
||||
case None =>
|
||||
log.error("Failed to deserialize message: error: {} \n msg: {}", error, jsonNode)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
m <- deserialize(jsonNode)
|
||||
} yield {
|
||||
fsApp.holdChannel(m.body.voiceConf, m.body.uuid, m.body.hold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ class RxJsonMsgHdlrActor(val fsApp: FreeswitchApplication) extends Actor with Ac
|
||||
routeCheckRunningAndRecordingToVoiceConfSysMsg(envelope, jsonNode)
|
||||
case GetUsersStatusToVoiceConfSysMsg.NAME =>
|
||||
routeGetUsersStatusToVoiceConfSysMsg(envelope, jsonNode)
|
||||
case HoldChannelInVoiceConfSysMsg.NAME =>
|
||||
routeHoldChannelInVoiceConfMsg(envelope, jsonNode)
|
||||
case _ => // do nothing
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,9 @@ class VoiceConferenceService(healthz: HealthzService,
|
||||
cm.muted,
|
||||
cm.speaking,
|
||||
cm.callingWith,
|
||||
"freeswitch"
|
||||
"freeswitch",
|
||||
cm.hold,
|
||||
cm.uuid
|
||||
)
|
||||
}
|
||||
|
||||
@ -119,12 +121,16 @@ class VoiceConferenceService(healthz: HealthzService,
|
||||
callerIdNum: String,
|
||||
muted: java.lang.Boolean,
|
||||
talking: java.lang.Boolean,
|
||||
callingWith: String
|
||||
) {
|
||||
callingWith: String,
|
||||
hold: java.lang.Boolean,
|
||||
uuid: String
|
||||
): Unit = {
|
||||
|
||||
val header = BbbCoreVoiceConfHeader(UserJoinedVoiceConfEvtMsg.NAME, voiceConfId)
|
||||
val body = UserJoinedVoiceConfEvtMsgBody(voiceConfId, voiceUserId, userId, callerIdName, callerIdNum,
|
||||
muted.booleanValue(), talking.booleanValue(), callingWith)
|
||||
muted.booleanValue(), talking.booleanValue(), callingWith,
|
||||
hold,
|
||||
uuid);
|
||||
val envelope = BbbCoreEnvelope(UserJoinedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
|
||||
|
||||
val msg = new UserJoinedVoiceConfEvtMsg(header, body)
|
||||
@ -248,6 +254,28 @@ class VoiceConferenceService(healthz: HealthzService,
|
||||
sender.publish(fromVoiceConfRedisChannel, json)
|
||||
}
|
||||
|
||||
def channelHoldChanged(
|
||||
voiceConfId: String,
|
||||
voiceUserId: String,
|
||||
uuid: String,
|
||||
hold: java.lang.Boolean
|
||||
): Unit = {
|
||||
val header = BbbCoreVoiceConfHeader(ChannelHoldChangedVoiceConfEvtMsg.NAME, voiceConfId)
|
||||
val body = ChannelHoldChangedVoiceConfEvtMsgBody(
|
||||
voiceConfId,
|
||||
voiceUserId,
|
||||
uuid,
|
||||
hold
|
||||
);
|
||||
val envelope = BbbCoreEnvelope(ChannelHoldChangedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
|
||||
|
||||
val msg = new ChannelHoldChangedVoiceConfEvtMsg(header, body)
|
||||
val msgEvent = BbbCommonEnvCoreMsg(envelope, msg)
|
||||
|
||||
val json = JsonUtil.toJson(msgEvent)
|
||||
sender.publish(fromVoiceConfRedisChannel, json)
|
||||
}
|
||||
|
||||
def freeswitchStatusReplyEvent(
|
||||
sendCommandTimestamp: java.lang.Long,
|
||||
status: java.util.List[String],
|
||||
|
@ -6,7 +6,7 @@ case class PresentationVO(id: String, temporaryPresentationId: String, name: Str
|
||||
|
||||
case class PageVO(id: String, num: Int, thumbUri: String = "",
|
||||
txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0,
|
||||
yOffset: Double = 0, widthRatio: Double = 100D, heightRatio: Double = 100D)
|
||||
yOffset: Double = 0, widthRatio: Double = 100D, heightRatio: Double = 100D, width: Double = 1440D, height: Double = 1080D)
|
||||
|
||||
case class PresentationPodVO(id: String, currentPresenter: String,
|
||||
presentations: Vector[PresentationVO])
|
||||
@ -15,7 +15,9 @@ case class PresentationPageConvertedVO(
|
||||
id: String,
|
||||
num: Int,
|
||||
urls: Map[String, String],
|
||||
current: Boolean = false
|
||||
current: Boolean = false,
|
||||
width: Double = 1440D,
|
||||
height: Double = 1080D
|
||||
)
|
||||
|
||||
case class PresentationPageVO(
|
||||
|
@ -45,11 +45,6 @@ case class SlideResizedPubMsg(header: BbbClientMsgHeader, body: SlideResizedPubM
|
||||
case class SlideResizedPubMsgBody(pageId: String, width: Double, height: Double,
|
||||
xOffset: Double, yOffset: Double, widthRatio: Double, heightRatio: Double)
|
||||
|
||||
object AddSlidePositionsPubMsg { val NAME = "AddSlidePositionsPubMsg" }
|
||||
case class AddSlidePositionsPubMsg(header: BbbClientMsgHeader, body: AddSlidePositionsPubMsgBody) extends StandardMsg
|
||||
case class AddSlidePositionsPubMsgBody(slideId: String, width: Double,
|
||||
height: Double, viewBoxWidth: Double, viewBoxHeight: Double)
|
||||
|
||||
object SetCurrentPresentationPubMsg { val NAME = "SetCurrentPresentationPubMsg" }
|
||||
case class SetCurrentPresentationPubMsg(header: BbbClientMsgHeader, body: SetCurrentPresentationPubMsgBody) extends StandardMsg
|
||||
case class SetCurrentPresentationPubMsgBody(podId: String, presentationId: String)
|
||||
|
@ -387,8 +387,9 @@ case class UserStatusVoiceConfEvtMsgBody(voiceConf: String, confUsers: Vector[Co
|
||||
case class ConfVoiceUser(voiceUserId: String, intId: String,
|
||||
callerIdName: String, callerIdNum: String, muted: Boolean,
|
||||
talking: Boolean, callingWith: String,
|
||||
calledInto: String // freeswitch, kms
|
||||
)
|
||||
calledInto: String, // freeswitch, kms
|
||||
hold: Boolean,
|
||||
uuid: String)
|
||||
case class ConfVoiceRecording(recordPath: String, recordStartTime: Long)
|
||||
|
||||
/**
|
||||
@ -401,7 +402,9 @@ case class UserJoinedVoiceConfEvtMsg(
|
||||
) extends VoiceStandardMsg
|
||||
case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, intId: String,
|
||||
callerIdName: String, callerIdNum: String, muted: Boolean,
|
||||
talking: Boolean, callingWith: String)
|
||||
talking: Boolean, callingWith: String,
|
||||
hold: Boolean,
|
||||
uuid: String)
|
||||
|
||||
/**
|
||||
* Sent to client that a user has joined the voice conference.
|
||||
@ -639,3 +642,64 @@ case class GetMicrophonePermissionRespMsgBody(
|
||||
sfuSessionId: String,
|
||||
allowed: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Sent to FS to hold an audio channel
|
||||
*/
|
||||
object HoldChannelInVoiceConfSysMsg { val NAME = "HoldChannelInVoiceConfSysMsg" }
|
||||
case class HoldChannelInVoiceConfSysMsg(
|
||||
header: BbbCoreHeaderWithMeetingId,
|
||||
body: HoldChannelInVoiceConfSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class HoldChannelInVoiceConfSysMsgBody(
|
||||
voiceConf: String,
|
||||
uuid: String,
|
||||
hold: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Received from FS that the user channel hold state has changed
|
||||
*/
|
||||
object ChannelHoldChangedVoiceConfEvtMsg { val NAME = "ChannelHoldChangedVoiceConfEvtMsg" }
|
||||
case class ChannelHoldChangedVoiceConfEvtMsg(
|
||||
header: BbbCoreVoiceConfHeader,
|
||||
body: ChannelHoldChangedVoiceConfEvtMsgBody
|
||||
) extends VoiceStandardMsg
|
||||
case class ChannelHoldChangedVoiceConfEvtMsgBody(
|
||||
voiceConf: String,
|
||||
intId: String,
|
||||
uuid: String,
|
||||
hold: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Sent to bbb-webrtc-sfu to request for userId's microphone connection
|
||||
* to be toggled between bidirectional and unidirectional (listen only) modes
|
||||
* (enabled = unidirectional, listen only, !enabled = bidirectional);
|
||||
*/
|
||||
object ToggleListenOnlyModeSysMsg { val NAME = "ToggleListenOnlyModeSysMsg" }
|
||||
case class ToggleListenOnlyModeSysMsg(
|
||||
header: BbbCoreHeaderWithMeetingId,
|
||||
body: ToggleListenOnlyModeSysMsgBody
|
||||
) extends BbbCoreMsg
|
||||
case class ToggleListenOnlyModeSysMsgBody(
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
enabled: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Sent from bbb-webrtc-sfu to indicate that userId's microphone channel switched
|
||||
* modes (enabled = unidirectional, listen only, !enabled = bidirectional);
|
||||
*/
|
||||
object ListenOnlyModeToggledInSfuEvtMsg { val NAME = "ListenOnlyModeToggledInSfuEvtMsg" }
|
||||
case class ListenOnlyModeToggledInSfuEvtMsg(
|
||||
header: BbbCoreVoiceConfHeader,
|
||||
body: ListenOnlyModeToggledInSfuEvtMsgBody
|
||||
) extends VoiceStandardMsg
|
||||
case class ListenOnlyModeToggledInSfuEvtMsgBody(
|
||||
meetingId: String,
|
||||
voiceConf: String,
|
||||
userId: String,
|
||||
enabled: Boolean
|
||||
)
|
||||
|
@ -112,5 +112,6 @@ libraryDependencies ++= Seq(
|
||||
"com.zaxxer" % "HikariCP" % "4.0.3",
|
||||
"commons-validator" % "commons-validator" % "1.7",
|
||||
"org.apache.tika" % "tika-core" % "2.8.0",
|
||||
"org.apache.tika" % "tika-parsers-standard-package" % "2.8.0"
|
||||
"org.apache.tika" % "tika-parsers-standard-package" % "2.8.0",
|
||||
"org.scala-lang.modules" %% "scala-xml" % "2.2.0"
|
||||
)
|
||||
|
@ -12,14 +12,12 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
|
||||
import org.apache.http.impl.nio.client.HttpAsyncClients;
|
||||
@ -197,6 +195,7 @@ public class PresentationUrlDownloadService {
|
||||
conn.setReadTimeout(60000);
|
||||
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
|
||||
conn.addRequestProperty("User-Agent", "Mozilla");
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
|
||||
// normally, 3xx is redirect
|
||||
int status = conn.getResponseCode();
|
||||
@ -287,10 +286,21 @@ public class PresentationUrlDownloadService {
|
||||
String finalUrl = followRedirect(meetingId, urlString, 0, urlString);
|
||||
|
||||
if (finalUrl == null) return false;
|
||||
if(!finalUrl.equals(urlString)) {
|
||||
log.info("Redirected to Final URL [{}]", finalUrl);
|
||||
}
|
||||
|
||||
boolean success = false;
|
||||
|
||||
CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
|
||||
//Disable follow redirect since finalUrl already did it
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setRedirectsEnabled(false)
|
||||
.build();
|
||||
|
||||
CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom()
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.build();
|
||||
|
||||
try {
|
||||
httpclient.start();
|
||||
File download = new File(filename);
|
||||
|
@ -6,6 +6,11 @@ import org.bigbluebutton.common2.domain.{ DefaultProps, PageVO, PresentationPage
|
||||
import org.bigbluebutton.common2.msgs._
|
||||
import org.bigbluebutton.presentation.messages._
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import javax.imageio.ImageIO
|
||||
import scala.xml.XML
|
||||
|
||||
object MsgBuilder {
|
||||
def buildDestroyMeetingSysCmdMsg(msg: DestroyMeetingMessage): BbbCommonEnvCoreMsg = {
|
||||
val routing = collection.immutable.HashMap("sender" -> "bbb-web")
|
||||
@ -74,13 +79,35 @@ object MsgBuilder {
|
||||
val pngUrl = presBaseUrl + "/png/" + page
|
||||
|
||||
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
|
||||
|
||||
try {
|
||||
val imgUrl = new URL(svgUrl)
|
||||
val imgContent = XML.load(imgUrl)
|
||||
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
num = page,
|
||||
urls = urls,
|
||||
current = current
|
||||
)
|
||||
val w = (imgContent \ "@width").text.replaceAll("[^\\d]", "")
|
||||
val h = (imgContent \ "@height").text.replaceAll("[^\\d]", "")
|
||||
|
||||
val width = w.toInt
|
||||
val height = h.toInt
|
||||
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
num = page,
|
||||
urls = urls,
|
||||
current = current,
|
||||
width = width,
|
||||
height = height
|
||||
)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
e.printStackTrace()
|
||||
PresentationPageConvertedVO(
|
||||
id = id,
|
||||
num = page,
|
||||
urls = urls,
|
||||
current = current
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def buildPresentationPageConvertedSysMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = {
|
||||
|
@ -468,17 +468,22 @@ CREATE TABLE "user_voice" (
|
||||
"startTime" bigint
|
||||
);
|
||||
--CREATE INDEX "idx_user_voice_userId" ON "user_voice"("userId");
|
||||
ALTER TABLE "user_voice" ADD COLUMN "hideTalkingIndicatorAt" timestamp with time zone GENERATED ALWAYS AS (to_timestamp((COALESCE("endTime","startTime") + 6000) / 1000)) STORED;
|
||||
CREATE INDEX "idx_user_voice_userId_talking" ON "user_voice"("userId","hideTalkingIndicatorAt","startTime");
|
||||
ALTER TABLE "user_voice" ADD COLUMN "hideTalkingIndicatorAt" timestamp with time zone
|
||||
GENERATED ALWAYS AS (to_timestamp((COALESCE("endTime","startTime") + 6000) / 1000)) STORED;
|
||||
|
||||
CREATE INDEX "idx_user_voice_userId_talking" ON "user_voice"("userId","talking");
|
||||
CREATE INDEX "idx_user_voice_userId_hideTalkingIndicatorAt" ON "user_voice"("userId","hideTalkingIndicatorAt");
|
||||
|
||||
CREATE OR REPLACE VIEW "v_user_voice" AS
|
||||
SELECT
|
||||
u."meetingId",
|
||||
"user_voice" .*,
|
||||
greatest(coalesce(user_voice."startTime", 0), coalesce(user_voice."endTime", 0)) AS "lastSpeakChangedAt",
|
||||
case when "hideTalkingIndicatorAt" > current_timestamp then true else false end "showTalkingIndicator"
|
||||
user_talking."userId" IS NOT NULL "showTalkingIndicator"
|
||||
FROM "user" u
|
||||
JOIN "user_voice" ON u."userId" = "user_voice"."userId";
|
||||
JOIN "user_voice" ON "user_voice"."userId" = u."userId"
|
||||
LEFT JOIN "user_voice" user_talking ON (user_talking."userId" = u."userId" and user_talking."talking" IS TRUE)
|
||||
OR (user_talking."userId" = u."userId" and user_talking."hideTalkingIndicatorAt" > now());
|
||||
|
||||
CREATE TABLE "user_camera" (
|
||||
"streamId" varchar(100) PRIMARY KEY,
|
||||
@ -1276,6 +1281,53 @@ JOIN "v_meeting_breakoutPolicies"vmbp using("meetingId")
|
||||
JOIN "breakoutRoom" br ON br."parentMeetingId" = vmbp."parentId" AND br."externalId" = m."extId";
|
||||
|
||||
|
||||
------------------------------------
|
||||
----sharedNotes
|
||||
|
||||
create table "sharedNotes" (
|
||||
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"sharedNotesExtId" varchar(25),
|
||||
"padId" varchar(25),
|
||||
"model" varchar(25),
|
||||
"name" varchar(25),
|
||||
"pinned" boolean,
|
||||
constraint "pk_sharedNotes" primary key ("meetingId", "sharedNotesExtId")
|
||||
);
|
||||
|
||||
create table "sharedNotes_rev" (
|
||||
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"sharedNotesExtId" varchar(25),
|
||||
"rev" integer,
|
||||
"userId" varchar(50) references "user"("userId") ON DELETE SET NULL,
|
||||
"changeset" varchar(25),
|
||||
"start" integer,
|
||||
"end" integer,
|
||||
"diff" TEXT,
|
||||
"createdAt" timestamp with time zone,
|
||||
constraint "pk_sharedNotes_rev" primary key ("meetingId", "sharedNotesExtId", "rev")
|
||||
);
|
||||
--create view "v_sharedNotes_rev" as select * from "sharedNotes_rev";
|
||||
|
||||
create table "sharedNotes_session" (
|
||||
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
|
||||
"sharedNotesExtId" varchar(25),
|
||||
"userId" varchar(50) references "user"("userId") ON DELETE CASCADE,
|
||||
"sessionId" varchar(50),
|
||||
constraint "pk_sharedNotes_session" primary key ("meetingId", "sharedNotesExtId", "userId")
|
||||
);
|
||||
create index "sharedNotes_session_userId" on "sharedNotes_session"("userId");
|
||||
|
||||
create view "v_sharedNotes" as
|
||||
SELECT sn.*, max(snr.rev) "lastRev"
|
||||
FROM "sharedNotes" sn
|
||||
LEFT JOIN "sharedNotes_rev" snr ON snr."meetingId" = sn."meetingId" AND snr."sharedNotesExtId" = sn."sharedNotesExtId"
|
||||
GROUP BY sn."meetingId", sn."sharedNotesExtId";
|
||||
|
||||
create view "v_sharedNotes_session" as
|
||||
SELECT sns.*, sn."padId"
|
||||
FROM "sharedNotes_session" sns
|
||||
JOIN "sharedNotes" sn ON sn."meetingId" = sns."meetingId" AND sn."sharedNotesExtId" = sn."sharedNotesExtId";
|
||||
|
||||
----------------------
|
||||
|
||||
CREATE OR REPLACE VIEW "v_current_time" AS
|
||||
|
@ -0,0 +1,21 @@
|
||||
table:
|
||||
name: v_sharedNotes
|
||||
schema: public
|
||||
configuration:
|
||||
column_config: {}
|
||||
custom_column_names: {}
|
||||
custom_name: sharedNotes
|
||||
custom_root_fields: {}
|
||||
select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- lastRev
|
||||
- model
|
||||
- name
|
||||
- padId
|
||||
- pinned
|
||||
- sharedNotesExtId
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
@ -0,0 +1,29 @@
|
||||
table:
|
||||
name: v_sharedNotes_session
|
||||
schema: public
|
||||
configuration:
|
||||
column_config: {}
|
||||
custom_column_names: {}
|
||||
custom_name: sharedNotes_session
|
||||
custom_root_fields: {}
|
||||
object_relationships:
|
||||
- name: sharedNotes
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
meetingId: meetingId
|
||||
sharedNotesExtId: sharedNotesExtId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_sharedNotes
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: bbb_client
|
||||
permission:
|
||||
columns:
|
||||
- padId
|
||||
- sessionId
|
||||
- sharedNotesExtId
|
||||
filter:
|
||||
userId:
|
||||
_eq: X-Hasura-UserId
|
@ -25,3 +25,4 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
allow_aggregations: true
|
||||
|
@ -79,6 +79,15 @@ object_relationships:
|
||||
remote_table:
|
||||
name: v_user_reaction
|
||||
schema: public
|
||||
- name: sharedNotesSession
|
||||
using:
|
||||
manual_configuration:
|
||||
column_mapping:
|
||||
userId: userId
|
||||
insertion_order: null
|
||||
remote_table:
|
||||
name: v_sharedNotes_session
|
||||
schema: public
|
||||
- name: voice
|
||||
using:
|
||||
manual_configuration:
|
||||
|
@ -1,15 +1,13 @@
|
||||
- "!include public_v_chat_user.yaml"
|
||||
- "!include public_v_meeting.yaml"
|
||||
- "!include public_v_user_connectionStatus.yaml"
|
||||
- "!include public_v_user_localSettings.yaml"
|
||||
- "!include public_v_breakoutRoom.yaml"
|
||||
- "!include public_v_breakoutRoom_assignedUser.yaml"
|
||||
- "!include public_v_breakoutRoom_participant.yaml"
|
||||
- "!include public_v_chat.yaml"
|
||||
- "!include public_v_chat_message_private.yaml"
|
||||
- "!include public_v_chat_message_public.yaml"
|
||||
- "!include public_v_chat_user.yaml"
|
||||
- "!include public_v_current_time.yaml"
|
||||
- "!include public_v_external_video.yaml"
|
||||
- "!include public_v_meeting.yaml"
|
||||
- "!include public_v_meeting_breakoutPolicies.yaml"
|
||||
- "!include public_v_meeting_group.yaml"
|
||||
- "!include public_v_meeting_lockSettings.yaml"
|
||||
@ -30,14 +28,18 @@
|
||||
- "!include public_v_pres_page_writers.yaml"
|
||||
- "!include public_v_pres_presentation.yaml"
|
||||
- "!include public_v_screenshare.yaml"
|
||||
- "!include public_v_sharedNotes.yaml"
|
||||
- "!include public_v_sharedNotes_session.yaml"
|
||||
- "!include public_v_timer.yaml"
|
||||
- "!include public_v_user.yaml"
|
||||
- "!include public_v_user_breakoutRoom.yaml"
|
||||
- "!include public_v_user_camera.yaml"
|
||||
- "!include public_v_user_connectionStatus.yaml"
|
||||
- "!include public_v_user_connectionStatusReport.yaml"
|
||||
- "!include public_v_user_current.yaml"
|
||||
- "!include public_v_user_customParameter.yaml"
|
||||
- "!include public_v_user_guest.yaml"
|
||||
- "!include public_v_user_localSettings.yaml"
|
||||
- "!include public_v_user_reaction.yaml"
|
||||
- "!include public_v_user_reaction_current.yaml"
|
||||
- "!include public_v_user_ref.yaml"
|
||||
|
@ -34,4 +34,4 @@ if [ "$akka_apps_status" = "active" ]; then
|
||||
fi
|
||||
|
||||
echo "Applying new metadata to Hasura"
|
||||
/usr/local/bin/hasura metadata apply
|
||||
/usr/local/bin/hasura/hasura metadata apply
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v0.3.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
||||
git clone --branch v0.4.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
||||
|
@ -1 +1 @@
|
||||
git clone --branch v2.10.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
git clone --branch v2.11.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||
|
@ -366,6 +366,10 @@ start_bigbluebutton () {
|
||||
|
||||
echo "Starting BigBlueButton"
|
||||
|
||||
systemctl mask bbb-rap-resque-worker
|
||||
systemctl mask bbb-rap-starter
|
||||
systemctl mask bbb-rap-caption-inbox
|
||||
|
||||
systemctl restart bigbluebutton.target
|
||||
|
||||
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.16.6
|
||||
mongo@1.16.7
|
||||
reactive-var@1.0.12
|
||||
|
||||
standard-minifier-css@1.9.2
|
||||
|
@ -1 +1 @@
|
||||
METEOR@2.12
|
||||
METEOR@2.13
|
||||
|
@ -13,7 +13,7 @@ check@1.3.2
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-server@2.6.1
|
||||
ddp-server@2.6.2
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
ecmascript@0.16.7
|
||||
@ -33,7 +33,7 @@ inter-process-messaging@0.1.1
|
||||
launch-screen@1.3.0
|
||||
lmieulet:meteor-coverage@4.1.0
|
||||
logging@1.3.2
|
||||
meteor@1.11.2
|
||||
meteor@1.11.3
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.5
|
||||
meteortesting:mocha@2.0.3
|
||||
@ -46,7 +46,7 @@ mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.6
|
||||
mongo@1.16.7
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
getMappedFallbackStun,
|
||||
} from '/imports/utils/fetchStunTurnServers';
|
||||
import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
|
||||
import getFromUserSettings from '/imports/ui/services/users-settings';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import {
|
||||
getAudioSessionNumber,
|
||||
@ -26,6 +27,8 @@ const MEDIA = Meteor.settings.public.media;
|
||||
const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer;
|
||||
const RETRY_THROUGH_RELAY = MEDIA.audio.retryThroughRelay || false;
|
||||
const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering;
|
||||
const FULLAUDIO_OFFERING = MEDIA.fullAudioOffering;
|
||||
const TRANSPARENT_LISTEN_ONLY = MEDIA.transparentListenOnly;
|
||||
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
|
||||
const CONNECTION_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000;
|
||||
const { audio: NETWORK_PRIORITY } = MEDIA.networkPriorities || {};
|
||||
@ -71,7 +74,18 @@ const getMediaServerAdapter = (listenOnly = false) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isTransparentListenOnlyEnabled = () => getFromUserSettings(
|
||||
'bbb_transparent_listen_only',
|
||||
TRANSPARENT_LISTEN_ONLY,
|
||||
);
|
||||
|
||||
export default class SFUAudioBridge extends BaseAudioBridge {
|
||||
static getOfferingRole(isListenOnly) {
|
||||
return isListenOnly
|
||||
? LISTEN_ONLY_OFFERING
|
||||
: (!isTransparentListenOnlyEnabled() && FULLAUDIO_OFFERING);
|
||||
}
|
||||
|
||||
constructor(userData) {
|
||||
super();
|
||||
this.userId = userData.userId;
|
||||
@ -266,6 +280,11 @@ export default class SFUAudioBridge extends BaseAudioBridge {
|
||||
},
|
||||
}, 'SFU audio media play failed due to autoplay error');
|
||||
this.dispatchAutoplayHandlingEvent(mediaElement);
|
||||
// For connection purposes, this worked - the autoplay thing is a client
|
||||
// side soft issue to be handled at the UI/UX level, not WebRTC/negotiation
|
||||
// So: clear the connection timer
|
||||
this.clearConnectionTimeout();
|
||||
this.reconnecting = false;
|
||||
} else {
|
||||
const normalizedError = {
|
||||
errorCode: 1004,
|
||||
@ -320,12 +339,13 @@ export default class SFUAudioBridge extends BaseAudioBridge {
|
||||
constraints: getAudioConstraints({ deviceId: this.inputDeviceId }),
|
||||
forceRelay: _forceRelay || shouldForceRelay(),
|
||||
stream: (inputStream && inputStream.active) ? inputStream : undefined,
|
||||
offering: isListenOnly ? LISTEN_ONLY_OFFERING : true,
|
||||
offering: SFUAudioBridge.getOfferingRole(this.isListenOnly),
|
||||
signalCandidates: SIGNAL_CANDIDATES,
|
||||
traceLogs: TRACE_LOGS,
|
||||
networkPriority: NETWORK_PRIORITY,
|
||||
mediaStreamFactory: this.mediaStreamFactory,
|
||||
gatheringTimeout: GATHERING_TIMEOUT,
|
||||
transparentListenOnly: isTransparentListenOnlyEnabled(),
|
||||
};
|
||||
|
||||
this.broker = new AudioBroker(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Users from '/imports/api/users';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import RegexWebUrl from '/imports/utils/regex-weburl';
|
||||
import { BREAK_LINE } from '/imports/utils/lineEndings';
|
||||
|
||||
const MSG_DIRECT_TYPE = 'DIRECT';
|
||||
const NODE_USER = 'nodeJSapp';
|
||||
@ -25,9 +26,29 @@ export const parseMessage = (message) => {
|
||||
// Replace flash links to flash valid ones
|
||||
parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>");
|
||||
|
||||
// Replace flash links to html valid ones
|
||||
parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\'');
|
||||
parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="');
|
||||
|
||||
// Replace \r and \n to <br/>
|
||||
parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
|
||||
|
||||
return parsedMessage;
|
||||
};
|
||||
|
||||
export const textToMarkdown = (message) => {
|
||||
let parsedMessage = message || '';
|
||||
parsedMessage = parsedMessage.trim();
|
||||
|
||||
// replace url with markdown links
|
||||
const urlRegex = /(?<!\]\()https?:\/\/([\w-]+\.)+\w{1,6}([/?=&#.]?[\w-]+)*/gm;
|
||||
parsedMessage = parsedMessage.replace(urlRegex, '[$&]($&)');
|
||||
|
||||
// replace new lines with markdown new lines
|
||||
parsedMessage = parsedMessage.replace(/\n\r?/g, ' \n');
|
||||
|
||||
return parsedMessage;
|
||||
};
|
||||
|
||||
export const spokeTimeoutHandles = {};
|
||||
export const clearSpokeTimeout = (meetingId, userId) => {
|
||||
|
@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
|
||||
import { extractCredentials, parseMessage } from '/imports/api/common/server/helpers';
|
||||
import { extractCredentials, textToMarkdown } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default function sendGroupChatMsg(chatId, message) {
|
||||
@ -17,8 +17,7 @@ export default function sendGroupChatMsg(chatId, message) {
|
||||
check(requesterUserId, String);
|
||||
check(chatId, String);
|
||||
check(message, Object);
|
||||
const parsedMessage = parseMessage(message.message);
|
||||
message.message = parsedMessage;
|
||||
message.message = textToMarkdown(message.message);
|
||||
|
||||
const payload = {
|
||||
msg: message,
|
||||
|
@ -2,7 +2,7 @@ import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import flat from 'flat';
|
||||
import { parseMessage } from '/imports/api/common/server/helpers';
|
||||
import { parseMessage } from './addGroupChatMsg';
|
||||
|
||||
export default async function addBulkGroupChatMsgs(msgs) {
|
||||
if (!msgs.length) return;
|
||||
|
@ -47,6 +47,8 @@ export default async function addPresentation(meetingId, podId, presentation) {
|
||||
yOffset: Number,
|
||||
widthRatio: Number,
|
||||
heightRatio: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
},
|
||||
],
|
||||
downloadable: Boolean,
|
||||
@ -62,13 +64,14 @@ export default async function addPresentation(meetingId, podId, presentation) {
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: Object.assign({
|
||||
$set: {
|
||||
meetingId,
|
||||
podId,
|
||||
'conversion.done': true,
|
||||
'conversion.error': false,
|
||||
'exportation.status': null,
|
||||
}, flat(presentation, { safe: true })),
|
||||
...flat(presentation, { safe: true }),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -57,6 +57,8 @@ export default async function addSlide(meetingId, podId, presentationId, slide)
|
||||
yOffset: Number,
|
||||
widthRatio: Number,
|
||||
heightRatio: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
content: String,
|
||||
});
|
||||
|
||||
@ -79,15 +81,15 @@ export default async function addSlide(meetingId, podId, presentationId, slide)
|
||||
const imageUri = slide.svgUri || slide.pngUri;
|
||||
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
{ meetingId },
|
||||
{ podId },
|
||||
{ presentationId },
|
||||
{ id: slideId },
|
||||
{ imageUri },
|
||||
flat(restSlide),
|
||||
{ safe: true },
|
||||
),
|
||||
$set: {
|
||||
meetingId,
|
||||
podId,
|
||||
presentationId,
|
||||
id: slideId,
|
||||
imageUri,
|
||||
...flat(restSlide),
|
||||
safe: true,
|
||||
},
|
||||
};
|
||||
|
||||
const imageSizeUri = (loadSlidesFromHttpAlways ? imageUri.replace(/^https/i, 'http') : imageUri);
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { SlidePositions } from '/imports/api/slides';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { check } from 'meteor/check';
|
||||
import flat from 'flat';
|
||||
@ -12,10 +10,6 @@ export default async function addSlidePositions(
|
||||
slideId,
|
||||
slidePosition,
|
||||
) {
|
||||
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'AddSlidePositionsPubMsg';
|
||||
|
||||
check(meetingId, String);
|
||||
check(podId, String);
|
||||
check(presentationId, String);
|
||||
@ -56,21 +50,6 @@ export default async function addSlidePositions(
|
||||
} else {
|
||||
Logger.info(`Upserted slide position id=${slideId} pod=${podId} presentation=${presentationId}`);
|
||||
}
|
||||
|
||||
const {
|
||||
width, height, viewBoxWidth, viewBoxHeight,
|
||||
} = slidePosition;
|
||||
|
||||
const payload = {
|
||||
slideId,
|
||||
width,
|
||||
height,
|
||||
viewBoxWidth,
|
||||
viewBoxHeight,
|
||||
};
|
||||
|
||||
Logger.info('Sending slide position data to backen');
|
||||
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, '', payload);
|
||||
} catch (err) {
|
||||
Logger.error(`Adding slide position to collection: ${err}`);
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ const notifyExpiredReaction = (meetingId, userId) => {
|
||||
check(meetingId, String);
|
||||
|
||||
const payload = {
|
||||
emoji,
|
||||
userId,
|
||||
};
|
||||
|
||||
|
@ -35,6 +35,7 @@ const currentParameters = [
|
||||
'bbb_skip_check_audio',
|
||||
'bbb_skip_check_audio_on_first_join',
|
||||
'bbb_fullaudio_bridge',
|
||||
'bbb_transparent_listen_only',
|
||||
// BRANDING
|
||||
'bbb_display_branding_area',
|
||||
// SHORTCUTS
|
||||
|
@ -24,7 +24,7 @@ import { makeCall } from '/imports/ui/services/api';
|
||||
import BBBStorage from '/imports/ui/services/storage';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const USER_WAS_EJECTED = 'userWasEjected';
|
||||
|
||||
const HTML = document.getElementsByTagName('html')[0];
|
||||
|
@ -301,7 +301,12 @@ class ActionsDropdown extends PureComponent {
|
||||
}
|
||||
|
||||
makePresentationItems() {
|
||||
const { presentations, setPresentation, podIds } = this.props;
|
||||
const {
|
||||
presentations,
|
||||
setPresentation,
|
||||
podIds,
|
||||
setPresentationFitToWidth,
|
||||
} = this.props;
|
||||
|
||||
if (!podIds || podIds.length < 1) return [];
|
||||
|
||||
@ -314,18 +319,21 @@ class ActionsDropdown extends PureComponent {
|
||||
.map((p) => {
|
||||
const customStyles = { color: colorPrimary };
|
||||
|
||||
return {
|
||||
customStyles: p.current ? customStyles : null,
|
||||
icon: 'file',
|
||||
iconRight: p.current ? 'check' : null,
|
||||
selected: p.current ? true : false,
|
||||
label: p.name,
|
||||
description: 'uploaded presentation file',
|
||||
key: `uploaded-presentation-${p.id}`,
|
||||
onClick: () => {
|
||||
setPresentation(p.id, podId);
|
||||
},
|
||||
};
|
||||
return (
|
||||
{
|
||||
customStyles: p.current ? customStyles : null,
|
||||
icon: "file",
|
||||
iconRight: p.current ? 'check' : null,
|
||||
selected: p.current ? true : false,
|
||||
label: p.name,
|
||||
description: "uploaded presentation file",
|
||||
key: `uploaded-presentation-${p.id}`,
|
||||
onClick: () => {
|
||||
setPresentationFitToWidth(false);
|
||||
setPresentation(p.id, podId);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
return presentationItemElements;
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ class ActionsBar extends PureComponent {
|
||||
setMeetingLayout,
|
||||
showPushLayout,
|
||||
setPushLayout,
|
||||
setPresentationFitToWidth,
|
||||
} = this.props;
|
||||
|
||||
const { isCaptionsReaderMenuModalOpen } = this.state;
|
||||
@ -109,6 +110,7 @@ class ActionsBar extends PureComponent {
|
||||
presentationIsOpen,
|
||||
showPushLayout,
|
||||
hasCameraAsContent,
|
||||
setPresentationFitToWidth,
|
||||
}}
|
||||
/>
|
||||
{isCaptionsAvailable
|
||||
|
@ -51,8 +51,10 @@ const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButt
|
||||
const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
|
||||
|
||||
const isReactionsButtonEnabled = () => {
|
||||
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
|
||||
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled;
|
||||
return getFromUserSettings('enable-reactions-button', REACTIONS_BUTTON_ENABLED);
|
||||
|
||||
return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED;
|
||||
};
|
||||
|
||||
export default withTracker(() => ({
|
||||
|
@ -57,7 +57,7 @@ const FreeJoinLabel = styled.label`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${fontSizeSmall};
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0.2rem;
|
||||
|
||||
& > * {
|
||||
margin: 0 .5rem 0 0;
|
||||
@ -125,9 +125,9 @@ const RoomName = styled(BreakoutNameInput)`
|
||||
|
||||
const BreakoutSettings = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 2fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-gap: 4rem;
|
||||
grid-gap: 2rem;
|
||||
|
||||
@media ${smallOnly} {
|
||||
grid-template-columns: 1fr ;
|
||||
@ -235,7 +235,6 @@ const AssignBtns = styled(Button)`
|
||||
`;
|
||||
|
||||
const CheckBoxesContainer = styled(FlexRow)`
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: flex-end;
|
||||
|
@ -2,11 +2,11 @@ import React, { useState } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import ReactionsBar from '/imports/ui/components/emoji-picker/reactions-bar/component';
|
||||
import UserReactionService from '/imports/ui/components/user-reaction/service';
|
||||
import UserListService from '/imports/ui/components/user-list/service';
|
||||
import { Emoji } from 'emoji-mart';
|
||||
|
||||
import Styled from '../styles';
|
||||
import Styled from './styles';
|
||||
|
||||
const ReactionsButton = (props) => {
|
||||
const {
|
||||
@ -16,6 +16,7 @@ const ReactionsButton = (props) => {
|
||||
raiseHand,
|
||||
isMobile,
|
||||
currentUserReaction,
|
||||
autoCloseReactionsBar,
|
||||
} = props;
|
||||
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
@ -25,6 +26,14 @@ const ReactionsButton = (props) => {
|
||||
id: 'app.actionsBar.reactions.reactionsButtonLabel',
|
||||
description: 'reactions Label',
|
||||
},
|
||||
raiseHandLabel: {
|
||||
id: 'app.actionsBar.reactions.raiseHand',
|
||||
description: 'raise Hand Label',
|
||||
},
|
||||
notRaiseHandLabel: {
|
||||
id: 'app.actionsBar.reactions.lowHand',
|
||||
description: 'not Raise Hand Label',
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
@ -43,17 +52,80 @@ const ReactionsButton = (props) => {
|
||||
UserListService.setUserRaiseHand(userId, !raiseHand);
|
||||
};
|
||||
|
||||
const renderReactionsBar = () => (
|
||||
<Styled.Wrapper>
|
||||
<ReactionsBar
|
||||
{...props}
|
||||
onReactionSelect={handleReactionSelect}
|
||||
onRaiseHand={handleRaiseHandButtonClick}
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
);
|
||||
const RaiseHandButtonLabel = () => {
|
||||
if (isMobile) return null;
|
||||
|
||||
const customStyles = { top: '-1rem', borderRadius: '1.7rem' };
|
||||
return raiseHand
|
||||
? intl.formatMessage(intlMessages.notRaiseHandLabel)
|
||||
: intl.formatMessage(intlMessages.raiseHandLabel);
|
||||
};
|
||||
|
||||
const customStyles = {
|
||||
top: '-1rem',
|
||||
borderRadius: '1.7rem',
|
||||
};
|
||||
|
||||
const actionCustomStyles = {
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
paddingTop: isMobile ? '0' : '0.5rem',
|
||||
paddingBottom: isMobile ? '0' : '0.5rem',
|
||||
};
|
||||
|
||||
const emojiProps = {
|
||||
native: true,
|
||||
size: '1.5rem',
|
||||
padding: '4px',
|
||||
};
|
||||
|
||||
const reactions = [
|
||||
{
|
||||
id: 'smiley',
|
||||
native: '😃',
|
||||
},
|
||||
{
|
||||
id: 'neutral_face',
|
||||
native: '😐',
|
||||
},
|
||||
{
|
||||
id: 'slightly_frowning_face',
|
||||
native: '🙁',
|
||||
},
|
||||
{
|
||||
id: '+1',
|
||||
native: '👍',
|
||||
},
|
||||
{
|
||||
id: '-1',
|
||||
native: '👎',
|
||||
},
|
||||
{
|
||||
id: 'clap',
|
||||
native: '👏',
|
||||
},
|
||||
];
|
||||
|
||||
let actions = [];
|
||||
|
||||
reactions.forEach(({ id, native }) => {
|
||||
actions.push({
|
||||
label: <Styled.ButtonWrapper active={currentUserReaction === native}><Emoji key={id} emoji={{ id }} {...emojiProps} /></Styled.ButtonWrapper>,
|
||||
key: id,
|
||||
onClick: () => handleReactionSelect(native),
|
||||
customStyles: actionCustomStyles,
|
||||
});
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: <Styled.RaiseHandButtonWrapper isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
|
||||
key: 'hand',
|
||||
onClick: () => handleRaiseHandButtonClick(),
|
||||
customStyles: {...actionCustomStyles, width: 'auto'},
|
||||
});
|
||||
|
||||
const icon = currentUserReaction === 'none' ? 'hand' : null;
|
||||
const currentUserReactionEmoji = reactions.find(({ native }) => native === currentUserReaction);
|
||||
const customIcon = !icon ? <Emoji key={currentUserReactionEmoji?.id} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} /> : null;
|
||||
|
||||
return (
|
||||
<BBBMenu
|
||||
@ -61,26 +133,31 @@ const ReactionsButton = (props) => {
|
||||
<Styled.ReactionsDropdown>
|
||||
<Styled.RaiseHandButton
|
||||
data-test="reactionsButton"
|
||||
icon="hand"
|
||||
icon={icon}
|
||||
customIcon={customIcon}
|
||||
label={intl.formatMessage(intlMessages.reactionsLabel)}
|
||||
description="Reactions"
|
||||
ghost={!showEmojiPicker}
|
||||
ghost={!showEmojiPicker && !customIcon}
|
||||
onKeyPress={() => {}}
|
||||
onClick={() => setShowEmojiPicker(true)}
|
||||
color={showEmojiPicker ? 'primary' : 'default'}
|
||||
color={showEmojiPicker || customIcon ? 'primary' : 'default'}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
/>
|
||||
</Styled.ReactionsDropdown>
|
||||
)}
|
||||
renderOtherComponents={showEmojiPicker ? renderReactionsBar() : null}
|
||||
actions={actions}
|
||||
onCloseCallback={() => handleClose()}
|
||||
customAnchorEl={!isMobile ? actionsBarRef.current : null}
|
||||
customStyles={customStyles}
|
||||
open={showEmojiPicker}
|
||||
hasRoundedCorners
|
||||
overrideMobileStyles
|
||||
isHorizontal={!isMobile}
|
||||
isMobile={isMobile}
|
||||
roundButtons={true}
|
||||
keepOpen={!autoCloseReactionsBar}
|
||||
opts={{
|
||||
id: 'reactions-dropdown-menu',
|
||||
keepMounted: true,
|
||||
|
@ -6,6 +6,7 @@ import ReactionsButton from './component';
|
||||
import actionsBarService from '../service';
|
||||
import UserReactionService from '/imports/ui/components/user-reaction/service';
|
||||
import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums';
|
||||
import SettingsService from '/imports/ui/services/settings';
|
||||
|
||||
const ReactionsButtonContainer = ({ ...props }) => {
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
@ -32,6 +33,7 @@ export default injectIntl(withTracker(() => {
|
||||
emoji: currentUser.emoji,
|
||||
currentUserReaction: currentUserReaction.reaction,
|
||||
raiseHand: currentUser.raiseHand,
|
||||
autoCloseReactionsBar: SettingsService?.application?.autoCloseReactionsBar,
|
||||
};
|
||||
})(ReactionsButtonContainer));
|
||||
|
||||
|
@ -0,0 +1,88 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
import {
|
||||
colorGrayDark,
|
||||
colorGrayLightest,
|
||||
btnPrimaryColor,
|
||||
btnPrimaryActiveBg,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const RaiseHandButton = styled(Button)`
|
||||
${({ ghost }) => ghost && `
|
||||
& > span {
|
||||
box-shadow: none;
|
||||
background-color: transparent !important;
|
||||
border-color: ${colorWhite} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const ReactionsDropdown = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled.div`
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
margin: 0 .5rem;
|
||||
|
||||
&:focus {
|
||||
background-color: ${colorGrayDark};
|
||||
}
|
||||
|
||||
& > button {
|
||||
cursor: pointer;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
& > * > span {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
${({ active }) => active && `
|
||||
color: ${btnPrimaryColor};
|
||||
background-color: ${btnPrimaryActiveBg};
|
||||
|
||||
&:hover{
|
||||
filter: brightness(90%);
|
||||
color: ${btnPrimaryColor};
|
||||
background-color: ${btnPrimaryActiveBg} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const RaiseHandButtonWrapper = styled(ButtonWrapper)`
|
||||
width: 2.5rem;
|
||||
border-radius: 1.7rem;
|
||||
|
||||
|
||||
${({ isMobile }) => !isMobile && `
|
||||
border: 1px solid ${colorGrayLightest};
|
||||
padding: 1rem 0.5rem;
|
||||
width: auto;
|
||||
`}
|
||||
|
||||
${({ active }) => active && `
|
||||
color: ${btnPrimaryColor};
|
||||
background-color: ${btnPrimaryActiveBg};
|
||||
|
||||
&:hover{
|
||||
filter: brightness(90%);
|
||||
color: ${btnPrimaryColor};
|
||||
background-color: ${btnPrimaryActiveBg} !important;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
RaiseHandButton,
|
||||
ReactionsDropdown,
|
||||
ButtonWrapper,
|
||||
RaiseHandButtonWrapper,
|
||||
};
|
@ -23,4 +23,4 @@ const TimeSync: React.FC = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default TimeSync;
|
||||
export default TimeSync;
|
||||
|
@ -450,6 +450,7 @@ class App extends Component {
|
||||
setMeetingLayout={setMeetingLayout}
|
||||
showPushLayout={showPushLayoutButton && selectedLayout === 'custom'}
|
||||
presentationIsOpen={presentationIsOpen}
|
||||
setPresentationFitToWidth={this.setPresentationFitToWidth}
|
||||
/>
|
||||
</Styled.ActionsBar>
|
||||
);
|
||||
@ -582,6 +583,7 @@ class App extends Component {
|
||||
isAudioModalOpen,
|
||||
isRandomUserSelectModalOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
presentationFitToWidth,
|
||||
allPluginsLoaded,
|
||||
} = this.state;
|
||||
return (
|
||||
@ -612,46 +614,40 @@ class App extends Component {
|
||||
<NavBarContainer main="new" />
|
||||
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
|
||||
<Styled.TextMeasure id="text-measure" />
|
||||
{shouldShowPresentation ? (
|
||||
<PresentationAreaContainer
|
||||
darkTheme={darkTheme}
|
||||
presentationIsOpen={presentationIsOpen}
|
||||
layoutType={selectedLayout}
|
||||
/>
|
||||
) : null}
|
||||
{shouldShowScreenshare ? (
|
||||
<ScreenshareContainer isLayoutSwapped={!presentationIsOpen} />
|
||||
) : null}
|
||||
{shouldShowExternalVideo ? (
|
||||
<ExternalVideoContainer
|
||||
isLayoutSwapped={!presentationIsOpen}
|
||||
isPresenter={isPresenter}
|
||||
/>
|
||||
) : null}
|
||||
{shouldShowSharedNotes ? (
|
||||
<NotesContainer area="media" layoutType={selectedLayout} />
|
||||
) : null}
|
||||
{shouldShowPresentation ? <PresentationAreaContainer setPresentationFitToWidth={this.setPresentationFitToWidth} fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} layoutType={selectedLayout} /> : null}
|
||||
{shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} /> : null}
|
||||
{
|
||||
shouldShowExternalVideo
|
||||
? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
|
||||
: null
|
||||
}
|
||||
{shouldShowSharedNotes
|
||||
? (
|
||||
<NotesContainer
|
||||
area="media"
|
||||
layoutType={selectedLayout}
|
||||
/>
|
||||
) : null}
|
||||
{this.renderCaptions()}
|
||||
<AudioCaptionsSpeechContainer />
|
||||
{this.renderAudioCaptions()}
|
||||
<UploaderContainer />
|
||||
<CaptionsSpeechContainer />
|
||||
<BreakoutRoomInvitation />
|
||||
<AudioContainer
|
||||
{...{
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen: this.setAudioModalIsOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen,
|
||||
}}
|
||||
/>
|
||||
<AudioContainer {...{
|
||||
isAudioModalOpen,
|
||||
setAudioModalIsOpen: this.setAudioModalIsOpen,
|
||||
isVideoPreviewModalOpen,
|
||||
setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen,
|
||||
}} />
|
||||
<ToastContainer rtl />
|
||||
{(audioAlertEnabled || pushAlertEnabled) && (
|
||||
<ChatAlertContainer
|
||||
audioAlertEnabled={audioAlertEnabled}
|
||||
pushAlertEnabled={pushAlertEnabled}
|
||||
/>
|
||||
)}
|
||||
{(audioAlertEnabled || pushAlertEnabled)
|
||||
&& (
|
||||
<ChatAlertContainer
|
||||
audioAlertEnabled={audioAlertEnabled}
|
||||
pushAlertEnabled={pushAlertEnabled}
|
||||
/>
|
||||
)}
|
||||
<RaiseHandNotifier />
|
||||
<ManyWebcamsNotifier />
|
||||
<PollingContainer />
|
||||
@ -659,23 +655,15 @@ class App extends Component {
|
||||
<WakeLockContainer />
|
||||
{this.renderActionsBar()}
|
||||
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
|
||||
{customStyle ? (
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`}
|
||||
/>
|
||||
) : null}
|
||||
{isRandomUserSelectModalOpen ? (
|
||||
<RandomUserSelectContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
|
||||
priority: 'low',
|
||||
setIsOpen: this.setRandomUserSelectModalIsOpen,
|
||||
isOpen: isRandomUserSelectModalOpen,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
|
||||
{isRandomUserSelectModalOpen ? <RandomUserSelectContainer
|
||||
{...{
|
||||
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
|
||||
priority: "low",
|
||||
setIsOpen: this.setRandomUserSelectModalIsOpen,
|
||||
isOpen: isRandomUserSelectModalOpen,
|
||||
}}
|
||||
/> : null}
|
||||
</Styled.Layout>
|
||||
</>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { debounce } from 'radash';
|
||||
import { debounce } from '/imports/utils/debounce';
|
||||
import AudioManager from '/imports/ui/services/audio-manager';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
@ -123,7 +123,7 @@ export default {
|
||||
joinListenOnly: () => AudioManager.joinListenOnly(),
|
||||
joinMicrophone: () => AudioManager.joinMicrophone(),
|
||||
joinEchoTest: () => AudioManager.joinEchoTest(),
|
||||
toggleMuteMicrophone: debounce({ delay: 500 }, toggleMuteMicrophone),
|
||||
toggleMuteMicrophone: debounce(toggleMuteMicrophone, 500, { leading: true, trailing: false }),
|
||||
changeInputDevice: (inputDeviceId) => AudioManager.changeInputDevice(inputDeviceId),
|
||||
changeInputStream: (newInputStream) => { AudioManager.inputStream = newInputStream; },
|
||||
liveChangeInputDevice: (inputDeviceId) => AudioManager.liveChangeInputDevice(inputDeviceId),
|
||||
|
@ -3,8 +3,8 @@ import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { isChatEnabled } from '/imports/ui/services/features';
|
||||
import ClickOutside from '/imports/ui/components/click-outside/component';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import Styled from './styles';
|
||||
import { escapeHtml } from '/imports/utils/string-utils';
|
||||
import { checkText } from 'smile2emoji';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
|
||||
@ -19,7 +19,6 @@ import { Layout } from '../../../layout/layoutTypes';
|
||||
import { useMeeting } from '/imports/ui/core/hooks/useMeeting';
|
||||
import Events from '/imports/ui/core/events/events';
|
||||
import ChatOfflineIndicator from './chat-offline-indicator/component';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
|
||||
interface ChatMessageFormProps {
|
||||
minMessageLength: number,
|
||||
@ -31,7 +30,9 @@ interface ChatMessageFormProps {
|
||||
locked: boolean,
|
||||
partnerIsLoggedOut: boolean,
|
||||
title: string,
|
||||
handleClickOutside: Function,
|
||||
handleEmojiSelect: (emojiObject: () => void,
|
||||
{ native: string }) => void;
|
||||
handleClickOutside: () => void,
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -187,7 +188,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
handleSendMessage(escapeHtml(msg), chatId);
|
||||
handleSendMessage(msg, chatId);
|
||||
setMessage('');
|
||||
updateUnreadMessages(chatId, '');
|
||||
setHasErrors(false);
|
||||
@ -195,11 +196,11 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
if (ENABLE_TYPING_INDICATOR) stopUserTyping();
|
||||
const sentMessageEvent = new CustomEvent(Events.SENT_MESSAGE);
|
||||
window.dispatchEvent(sentMessageEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmojiSelect = (emojiObject: { native: string }) => {
|
||||
const handleEmojiSelect = (emojiObject: { native: string }): void => {
|
||||
const txtArea = textAreaRef?.current?.textarea;
|
||||
if(!txtArea) return;
|
||||
if (!txtArea) return;
|
||||
const cursor = txtArea.selectionStart;
|
||||
|
||||
setMessage(
|
||||
@ -212,7 +213,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
setTimeout(() => txtArea.setSelectionRange(newCursor, newCursor), 10);
|
||||
}
|
||||
|
||||
const handleMessageChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleMessageChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
let newMessage = null;
|
||||
let newError = null;
|
||||
if (AUTO_CONVERT_EMOJI) {
|
||||
@ -229,17 +230,17 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
newMessage = newMessage.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
const handleUserTyping = (hasError?: boolean) => {
|
||||
if (hasError || !ENABLE_TYPING_INDICATOR) return;
|
||||
startUserTyping(chatId);
|
||||
};
|
||||
|
||||
setMessage(newMessage);
|
||||
setError(newError);
|
||||
handleUserTyping(newError!=null)
|
||||
}
|
||||
handleUserTyping(newError != null);
|
||||
};
|
||||
|
||||
const handleUserTyping = (hasError?: boolean) => {
|
||||
if (hasError || !ENABLE_TYPING_INDICATOR) return;
|
||||
startUserTyping(chatId);
|
||||
}
|
||||
|
||||
const handleMessageKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleMessageKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// TODO Prevent send message pressing enter on mobile and/or virtual keyboard
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@ -251,10 +252,10 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
|
||||
handleSubmit(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
const formRef = useRef();
|
||||
const formRef = useRef<HTMLFormElement | null >(null);
|
||||
|
||||
return (
|
||||
<Styled.Form
|
||||
@ -362,16 +363,13 @@ const ChatMessageFormContainer: React.FC = ({
|
||||
? intl.formatMessage(messages.titlePrivate, { 0: chat?.participant?.name })
|
||||
: intl.formatMessage(messages.titlePublic);
|
||||
|
||||
|
||||
const meeting = useMeeting((m) => {
|
||||
return {
|
||||
lockSettings: {
|
||||
hasActiveLockSetting: m?.lockSettings?.hasActiveLockSetting,
|
||||
disablePublicChat: m?.lockSettings?.disablePublicChat,
|
||||
disablePrivateChat: m?.lockSettings?.disablePrivateChat,
|
||||
}
|
||||
};
|
||||
});
|
||||
const meeting = useMeeting((m) => ({
|
||||
lockSettings: {
|
||||
hasActiveLockSetting: m?.lockSettings?.hasActiveLockSetting,
|
||||
disablePublicChat: m?.lockSettings?.disablePublicChat,
|
||||
disablePrivateChat: m?.lockSettings?.disablePrivateChat,
|
||||
},
|
||||
}));
|
||||
|
||||
const locked = chat?.public
|
||||
? meeting?.lockSettings?.disablePublicChat
|
||||
@ -387,7 +385,8 @@ const ChatMessageFormContainer: React.FC = ({
|
||||
return <ChatOfflineIndicator participantName={chat.participant.name} />;
|
||||
}
|
||||
|
||||
return <ChatMessageForm
|
||||
return (
|
||||
<ChatMessageForm
|
||||
{...{
|
||||
minMessageLength: CHAT_CONFIG.min_message_length,
|
||||
maxMessageLength: CHAT_CONFIG.max_message_length,
|
||||
@ -401,7 +400,8 @@ const ChatMessageFormContainer: React.FC = ({
|
||||
partnerIsLoggedOut: chat?.participant ? !chat?.participant?.isOnline : false,
|
||||
locked: locked ?? false,
|
||||
}}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessageFormContainer;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState, useMemo } from "react";
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import { makeVar, useMutation } from "@apollo/client";
|
||||
import { LAST_SEEN_MUTATION } from "./queries";
|
||||
@ -6,6 +6,7 @@ import {
|
||||
ButtonLoadMore,
|
||||
MessageList,
|
||||
MessageListWrapper,
|
||||
UnreadButton,
|
||||
} from "./styles";
|
||||
import { layoutSelect } from "../../../layout/context";
|
||||
import ChatListPage from "./page/component";
|
||||
@ -30,6 +31,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.chat.loadMoreButtonLabel',
|
||||
description: 'Label for load more button',
|
||||
},
|
||||
moreMessages: {
|
||||
id: 'app.chat.moreMessages',
|
||||
description: 'Chat message when the user has unread messages below the scroll',
|
||||
},
|
||||
});
|
||||
|
||||
interface ChatListProps {
|
||||
@ -97,15 +102,18 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
chatId,
|
||||
setMessageAsSeenMutation,
|
||||
lastSeenAt,
|
||||
totalUnread,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const messageListRef = React.useRef<HTMLDivElement>();
|
||||
const contentRef = React.useRef<HTMLDivElement>();
|
||||
// I used a ref here because I don't want to re-render the component when the last sender changes
|
||||
const lastSenderPerPage = React.useRef<Map<number, string>>(new Map());
|
||||
const messagesEndRef = React.useRef<HTMLDivElement>();
|
||||
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
|
||||
const [lastMessageCreatedTime, setLastMessageCreatedTime] = useState<number>(0);
|
||||
const [followingTail, setFollowingTail] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setter({
|
||||
...setter(),
|
||||
@ -167,6 +175,24 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderUnreadNotification = useMemo(() => {
|
||||
if (totalUnread && !followingTail) {
|
||||
return (
|
||||
<UnreadButton
|
||||
aria-hidden="true"
|
||||
color="primary"
|
||||
size="sm"
|
||||
key="unread-messages"
|
||||
label={intl.formatMessage(intlMessages.moreMessages)}
|
||||
onClick={() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [totalUnread, followingTail]);
|
||||
|
||||
useEffect(() => {
|
||||
const setScrollToTailEventHandler = () => {
|
||||
if (scrollObserver && contentRef.current) {
|
||||
@ -209,66 +235,70 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
? userLoadedBackUntilPage : Math.max(totalPages - 2, 0);
|
||||
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
|
||||
return (
|
||||
<MessageListWrapper>
|
||||
<MessageList
|
||||
ref={messageListRef}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY < 0) {
|
||||
if (isElement(contentRef.current) && followingTail) {
|
||||
toggleFollowingTail(false)
|
||||
[
|
||||
<MessageListWrapper>
|
||||
<MessageList
|
||||
ref={messageListRef}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY < 0) {
|
||||
if (isElement(contentRef.current) && followingTail) {
|
||||
toggleFollowingTail(false)
|
||||
}
|
||||
} else if (e.deltaY > 0) {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}
|
||||
} else if (e.deltaY > 0) {
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{
|
||||
(userLoadedBackUntilPage)
|
||||
? (
|
||||
<ButtonLoadMore
|
||||
onClick={() => {
|
||||
if (followingTail) {
|
||||
toggleFollowingTail(false);
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{
|
||||
(userLoadedBackUntilPage)
|
||||
? (
|
||||
<ButtonLoadMore
|
||||
onClick={() => {
|
||||
if (followingTail) {
|
||||
toggleFollowingTail(false);
|
||||
}
|
||||
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
|
||||
}
|
||||
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
|
||||
}
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
|
||||
</ButtonLoadMore>
|
||||
) : null
|
||||
}
|
||||
</span>
|
||||
<div id="contentRef" ref={contentRef}>
|
||||
<ChatPopupContainer />
|
||||
{
|
||||
// @ts-ignore
|
||||
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
|
||||
return (
|
||||
<ChatListPage
|
||||
key={`page-${page}`}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
setLastSender={setLastSender(lastSenderPerPage.current)}
|
||||
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
|
||||
chatId={chatId}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={messageListRef}
|
||||
lastSeenAt={lastSeenAt}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</MessageList>
|
||||
</MessageListWrapper >
|
||||
}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
|
||||
</ButtonLoadMore>
|
||||
) : null
|
||||
}
|
||||
</span>
|
||||
<div id="contentRef" ref={contentRef}>
|
||||
<ChatPopupContainer />
|
||||
{
|
||||
// @ts-ignore
|
||||
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
|
||||
return (
|
||||
<ChatListPage
|
||||
key={`page-${page}`}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
setLastSender={setLastSender(lastSenderPerPage.current)}
|
||||
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
|
||||
chatId={chatId}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={messageListRef}
|
||||
lastSeenAt={lastSeenAt}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</MessageList>
|
||||
</MessageListWrapper >,
|
||||
renderUnreadNotification,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { Message } from '/imports/ui/Types/message';
|
||||
import {
|
||||
ChatWrapper,
|
||||
ChatContent,
|
||||
ChatAvatar,
|
||||
} from "./styles";
|
||||
import ChatMessageHeader from "./message-header/component";
|
||||
import ChatMessageTextContent from "./message-content/text-content/component";
|
||||
@ -148,16 +149,24 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
sameSender={sameSender}
|
||||
ref={messageRef}
|
||||
>
|
||||
{(!message?.user || !sameSender)
|
||||
&& (
|
||||
<ChatAvatar
|
||||
avatar={message.user?.avatar}
|
||||
color={messageContent.color}
|
||||
moderator={messageContent.isModerator}
|
||||
>
|
||||
{messageContent.name.toLowerCase().slice(0, 2) || " "}
|
||||
</ChatAvatar>
|
||||
)
|
||||
}
|
||||
<ChatContent sameSender={message?.user ? sameSender : false}>
|
||||
<ChatMessageHeader
|
||||
sameSender={message?.user ? sameSender : false}
|
||||
name={messageContent.name}
|
||||
color={messageContent.color}
|
||||
isModerator={messageContent.isModerator}
|
||||
isOnline={message.user?.isOnline ?? true}
|
||||
avatar={message.user?.avatar}
|
||||
dateTime={dateTime}
|
||||
/>
|
||||
<ChatContent>
|
||||
{
|
||||
messageContent.component
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Styled from './styles';
|
||||
interface ChatMessageTextContentProps {
|
||||
text: string;
|
||||
@ -9,9 +10,18 @@ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
|
||||
text,
|
||||
emphasizedMessage,
|
||||
}) => {
|
||||
// @ts-ignore - temporary, while meteor exists in the project
|
||||
const { allowedElements } = Meteor.settings.public.chat;
|
||||
|
||||
return (
|
||||
<Styled.ChatMessage emphasizedMessage={emphasizedMessage}>
|
||||
{text}
|
||||
<ReactMarkdown
|
||||
linkTarget="_blank"
|
||||
allowedElements={allowedElements}
|
||||
unwrapDisallowed={true}
|
||||
>
|
||||
{text}
|
||||
</ReactMarkdown>
|
||||
</Styled.ChatMessage>
|
||||
);
|
||||
};
|
||||
|
@ -5,14 +5,23 @@ export const ChatMessage = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex-direction: column;
|
||||
color: ${colorText};
|
||||
word-break: break-word;
|
||||
margin-left: 2.75rem;
|
||||
${({ emphasizedMessage }) =>
|
||||
emphasizedMessage &&
|
||||
`
|
||||
font-weight: bold;
|
||||
`}
|
||||
|
||||
& img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default {
|
||||
|
@ -13,9 +13,6 @@ const intlMessages = defineMessages({
|
||||
|
||||
interface ChatMessageHeaderProps {
|
||||
name: string;
|
||||
avatar: string;
|
||||
color: string;
|
||||
isModerator: boolean;
|
||||
isOnline: boolean;
|
||||
dateTime: Date;
|
||||
sameSender: boolean;
|
||||
@ -24,9 +21,6 @@ interface ChatMessageHeaderProps {
|
||||
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
||||
sameSender,
|
||||
name,
|
||||
color,
|
||||
isModerator,
|
||||
avatar,
|
||||
isOnline,
|
||||
dateTime,
|
||||
}) => {
|
||||
@ -35,15 +29,8 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
||||
|
||||
return (
|
||||
<Styled.HeaderContent>
|
||||
<Styled.ChatAvatar
|
||||
avatar={avatar}
|
||||
color={color}
|
||||
moderator={isModerator}
|
||||
>
|
||||
{name.toLowerCase().slice(0, 2) || " "}
|
||||
</Styled.ChatAvatar>
|
||||
<Styled.ChatHeaderText>
|
||||
<Styled.ChatUserName>
|
||||
<Styled.ChatUserName isOnline={isOnline}>
|
||||
{name}
|
||||
</Styled.ChatUserName>
|
||||
{
|
||||
|
@ -1,25 +1,23 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
userIndicatorsOffset,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import {
|
||||
colorWhite,
|
||||
userListBg,
|
||||
colorSuccess,
|
||||
colorHeading,
|
||||
palettePlaceholderText,
|
||||
colorGrayLight,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
interface ChatUserNameProps {
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
||||
export const HeaderContent = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ChatUserName = styled.div`
|
||||
export const ChatUserName = styled.div<ChatUserNameProps>`
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
@ -83,91 +81,6 @@ export const ChatTime = styled.time`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatAvatar = styled.div`
|
||||
flex: 0 0 2.25rem;
|
||||
margin: 0px calc(0.5rem) 0px 0px;
|
||||
box-flex: 0;
|
||||
position: relative;
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
font-size: .85rem;
|
||||
border: 2px solid transparent;
|
||||
user-select: none;
|
||||
${({ color }) => css`
|
||||
background-color: ${color};
|
||||
`}
|
||||
}
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding-top: .5rem;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
padding-bottom: 0;
|
||||
color: inherit;
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: ${userIndicatorsOffset};
|
||||
right: ${userIndicatorsOffset};
|
||||
border: 1.5px solid ${userListBg};
|
||||
border-radius: 50%;
|
||||
background-color: ${colorSuccess};
|
||||
color: ${colorWhite};
|
||||
opacity: 0;
|
||||
font-family: 'bbb-icons';
|
||||
font-size: .65rem;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
letter-spacing: -.65rem;
|
||||
z-index: 1;
|
||||
|
||||
[dir="rtl"] & {
|
||||
left: ${userIndicatorsOffset};
|
||||
right: auto;
|
||||
padding-right: .65rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
${({ moderator }) =>
|
||||
moderator &&
|
||||
`
|
||||
border-radius: 5px;
|
||||
`}
|
||||
|
||||
// ================ image ================
|
||||
${({ avatar, emoji }) =>
|
||||
avatar?.length !== 0 &&
|
||||
!emoji &&
|
||||
css`
|
||||
background-image: url(${avatar});
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
`}
|
||||
// ================ image ================
|
||||
|
||||
// ================ content ================
|
||||
color: ${colorWhite};
|
||||
font-size: 110%;
|
||||
text-transform: capitalize;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items:center;
|
||||
// ================ content ================
|
||||
|
||||
& .react-loading-skeleton {
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatHeaderText = styled.div`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@ -176,7 +89,6 @@ export const ChatHeaderText = styled.div`
|
||||
|
||||
export default {
|
||||
HeaderContent,
|
||||
ChatAvatar,
|
||||
ChatTime,
|
||||
ChatUserOffline,
|
||||
ChatUserName,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user