Merge branch 'develop' of github.com:bigbluebutton/bigbluebutton into akka-pekko-migration

This commit is contained in:
Paul Trudel 2023-08-29 12:06:56 +00:00
commit 225b2f2d74
190 changed files with 5387 additions and 2946 deletions

View 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

View File

@ -15,318 +15,96 @@ on:
- '**/*.md' - '**/*.md'
permissions: permissions:
contents: read contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build-bbb-apps-akka: build-bbb-apps-akka:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-apps-akka
with: cache-files-list: akka-bbb-apps bbb-common-message
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
build-bbb-config: build-bbb-config:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-config
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh cache-files-list: bigbluebutton-config
- 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
build-bbb-export-annotations: build-bbb-export-annotations:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-export-annotations
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh cache-files-list: bbb-export-annotations
- 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
build-bbb-learning-dashboard: build-bbb-learning-dashboard:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-learning-dashboard
with: cache-files-list: bbb-learning-dashboard
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
build-bbb-playback-record: build-bbb-playback-record:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-playback-record
- run: ./build/get_external_dependencies.sh build-list: bbb-playback bbb-playback-notes bbb-playback-podcast bbb-playback-presentation bbb-playback-screenshare bbb-playback-video bbb-record-core
- 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
build-bbb-graphql-server: build-bbb-graphql-server:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-graphql-server
- run: ./build/get_external_dependencies.sh build-list: bbb-graphql-server bbb-graphql-middleware
- 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
build-bbb-etherpad: build-bbb-etherpad:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-etherpad
with: cache-files-list: bbb-etherpad.placeholder.sh build/packages-template/bbb-etherpad
fetch-depth: 0 # Fetch all history 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
- 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
build-bbb-bbb-web: build-bbb-bbb-web:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-web
with: cache-files-list: bigbluebutton-web bbb-common-message bbb-common-web
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
build-bbb-fsesl-akka: build-bbb-fsesl-akka:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-fsesl-akka
with: cache-files-list: akka-bbb-fsesl bbb-common-message
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
build-bbb-html5: build-bbb-html5:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-html5
with: build-list: bbb-html5-nodejs bbb-html5
fetch-depth: 0 # Fetch all history cache-files-list: bigbluebutton-html5
- 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
build-bbb-freeswitch: build-bbb-freeswitch:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: bbb-freeswitch
with: build-list: bbb-freeswitch-core bbb-freeswitch-sounds
fetch-depth: 0 # Fetch all history cache-files-list: freeswitch.placeholder.sh build/packages-template/bbb-freeswitch-core build/packages-template/bbb-freeswitch-sounds
- run: echo "CACHE_FREESWITCH_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-core)" >> $GITHUB_ENV cache-urls-list: http://bigbluebutton.org/downloads/sounds.tar.gz
- run: echo "CACHE_FREESWITCH_SOUNDS_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-sounds)" >> $GITHUB_ENV build-bbb-webrtc:
- run: echo "CACHE_SOUNDS_KEY=$(curl -Is http://bigbluebutton.org/downloads/sounds.tar.gz | grep "Last-Modified" | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV with:
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh build-name: bbb-webrtc
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh build-list: bbb-webrtc-sfu bbb-webrtc-recorder
- name: Handle cache 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
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
build-others: build-others:
runs-on: ubuntu-22.04 uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
steps: with:
- uses: actions/checkout@v3 build-name: others
- run: ./build/get_external_dependencies.sh build-list: bbb-mkclean bbb-pads bbb-libreoffice-docker bbb-transcription-controller bigbluebutton
- 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
install-and-run-tests: 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 runs-on: ubuntu-22.04
steps: 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 - run: ./build/get_external_dependencies.sh
- name: Download artifacts_bbb-apps-akka - name: Download artifacts_bbb-apps-akka
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@ -368,6 +146,11 @@ jobs:
with: with:
name: artifacts_bbb-freeswitch.tar name: artifacts_bbb-freeswitch.tar
- run: tar xf artifacts.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 - name: Download artifacts_bbb-web
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
@ -386,7 +169,7 @@ jobs:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: artifacts.tar name: artifacts_others.tar
- run: tar xf artifacts.tar - run: tar xf artifacts.tar
- name: Extracting files .tar - name: Extracting files .tar
run: | run: |
@ -461,7 +244,7 @@ jobs:
run: | run: |
sudo -i <<EOF sudo -i <<EOF
set -e 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/ 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 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 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

View 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

View File

@ -41,6 +41,7 @@ trait SystemConfiguration {
lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43) lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43)
lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true) 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 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) lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.{ SharedNotesRevDAO }
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -9,7 +10,6 @@ trait PadContentSysMsgHdlr {
this: PadsApp2x => this: PadsApp2x =>
def handle(msg: PadContentSysMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { def handle(msg: PadContentSysMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(externalId: String, padId: String, rev: String, start: Int, end: Int, text: String): 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 routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(PadContentEvtMsg.NAME, routing) val envelope = BbbCoreEnvelope(PadContentEvtMsg.NAME, routing)
@ -22,8 +22,18 @@ trait PadContentSysMsgHdlr {
} }
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match { 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 Some(group) => {
case _ => 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 _ =>
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.SharedNotesDAO
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -22,8 +23,11 @@ trait PadCreatedEvtMsgHdlr {
} }
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match { Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
case Some(group) => broadcastEvent(group.externalId, group.userId, msg.body.padId, msg.body.name) case Some(group) => {
case _ => 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 _ =>
} }
} }
} }

View File

@ -3,6 +3,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait } import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.SharedNotesDAO
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -29,8 +30,11 @@ trait PadPinnedReqMsgHdlr extends RightsManagementTrait {
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)
} else { } else {
Pads.getGroup(liveMeeting.pads, msg.body.externalId) match { Pads.getGroup(liveMeeting.pads, msg.body.externalId) match {
case Some(group) => broadcastEvent(group.externalId, msg.body.pinned) case Some(group) => {
case _ => SharedNotesDAO.updatePinned(liveMeeting.props.meetingProp.intId, msg.body.externalId, msg.body.pinned)
broadcastEvent(group.externalId, msg.body.pinned)
}
case _ =>
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.SharedNotesSessionDAO
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -22,8 +23,11 @@ trait PadSessionCreatedEvtMsgHdlr {
} }
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match { Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
case Some(group) => broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId) case Some(group) => {
case _ => SharedNotesSessionDAO.insert(liveMeeting.props.meetingProp.intId, group.externalId, msg.body.userId, msg.body.sessionId)
broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
}
case _ =>
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.SharedNotesSessionDAO
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -22,8 +23,11 @@ trait PadSessionDeletedSysMsgHdlr {
} }
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match { Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match {
case Some(group) => broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId) case Some(group) => {
case _ => SharedNotesSessionDAO.delete(msg.body.userId, msg.body.sessionId)
broadcastEvent(group.externalId, msg.body.userId, msg.body.sessionId)
}
case _ =>
} }
} }
} }

View File

@ -2,6 +2,7 @@ package org.bigbluebutton.core.apps.pads
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.bus.MessageBus import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.{ SharedNotesRevDAO }
import org.bigbluebutton.core.models.Pads import org.bigbluebutton.core.models.Pads
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
@ -22,8 +23,11 @@ trait PadUpdatedSysMsgHdlr {
} }
Pads.getGroupById(liveMeeting.pads, msg.body.groupId) match { 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 Some(group) => {
case _ => 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 _ =>
} }
} }
} }

View File

@ -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
}
}

View File

@ -166,7 +166,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename) 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) val event = buildNewPresFileAvailable(fileURI, presId, m.body.typeOfExport)
handle(event, liveMeeting, bus) handle(event, liveMeeting, bus)

View File

@ -54,7 +54,9 @@ trait PresentationPageConvertedSysMsgHdlr {
msg.body.page.id, msg.body.page.id,
msg.body.page.num, msg.body.page.num,
msg.body.page.urls, 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 { val newState = for {

View File

@ -21,7 +21,6 @@ class PresentationPodHdlrs(implicit val context: ActorContext)
with PresentationUploadTokenReqMsgHdlr with PresentationUploadTokenReqMsgHdlr
with MakePresentationDownloadReqMsgHdlr with MakePresentationDownloadReqMsgHdlr
with ResizeAndMovePagePubMsgHdlr with ResizeAndMovePagePubMsgHdlr
with AddSlidePositionsPubMsgHdlr
with SlideResizedPubMsgHdlr with SlideResizedPubMsgHdlr
with SyncGetPresentationPodsMsgHdlr with SyncGetPresentationPodsMsgHdlr
with RemovePresentationPodPubMsgHdlr with RemovePresentationPodPubMsgHdlr

View File

@ -86,7 +86,9 @@ object PresentationPodsApp {
xOffset = page.xOffset, xOffset = page.xOffset,
yOffset = page.yOffset, yOffset = page.yOffset,
widthRatio = page.widthRatio, 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, PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable,

View File

@ -9,7 +9,7 @@ trait UserConnectedToGlobalAudioMsgHdlr {
val outGW: OutMsgRouter val outGW: OutMsgRouter
def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg) { def handleUserConnectedToGlobalAudioMsg(msg: UserConnectedToGlobalAudioMsg): Unit = {
log.info("Handling UserConnectedToGlobalAudio: meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId) log.info("Handling UserConnectedToGlobalAudio: meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId)
def broadcastEvent(vu: VoiceUserState): Unit = { def broadcastEvent(vu: VoiceUserState): Unit = {
@ -44,6 +44,8 @@ trait UserConnectedToGlobalAudioMsgHdlr {
System.currentTimeMillis(), System.currentTimeMillis(),
floor = false, floor = false,
lastFloorTime = "0", lastFloorTime = "0",
hold = false,
uuid = "unused"
) )
VoiceUsers.add(liveMeeting.voiceUsers, vu) VoiceUsers.add(liveMeeting.voiceUsers, vu)

View File

@ -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
)
}
}

View File

@ -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
)
}
}
}

View File

@ -93,7 +93,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
userColor, userColor,
msg.body.muted, msg.body.muted,
msg.body.talking, msg.body.talking,
"freeswitch" "freeswitch",
msg.body.hold,
msg.body.uuid
) )
} }

View File

@ -1,5 +1,6 @@
package org.bigbluebutton.core.apps.voice package org.bigbluebutton.core.apps.voice
import akka.actor.{ ActorContext, ActorSystem, Cancellable }
import org.bigbluebutton.SystemConfiguration import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.LockSettingsUtil import org.bigbluebutton.LockSettingsUtil
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers 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.apps.users.UsersApp
import org.bigbluebutton.core.util.ColorPicker import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core.util.TimeUtil import org.bigbluebutton.core.util.TimeUtil
import scala.collection.immutable.Map
import scala.concurrent.duration._
object VoiceApp extends SystemConfiguration { object VoiceApp extends SystemConfiguration {
// Key is userId
var toggleListenOnlyTasks: Map[String, Cancellable] = Map()
def genRecordPath( def genRecordPath(
recordDir: String, recordDir: String,
meetingId: String, meetingId: String,
@ -104,7 +110,7 @@ object VoiceApp extends SystemConfiguration {
outGW: OutMsgRouter, outGW: OutMsgRouter,
voiceUserId: String, voiceUserId: String,
muted: Boolean muted: Boolean
): Unit = { )(implicit context: ActorContext): Unit = {
for { for {
mutedUser <- VoiceUsers.userMuted(liveMeeting.voiceUsers, voiceUserId, muted) mutedUser <- VoiceUsers.userMuted(liveMeeting.voiceUsers, voiceUserId, muted)
} yield { } yield {
@ -117,13 +123,32 @@ object VoiceApp extends SystemConfiguration {
) )
} }
broadcastUserMutedVoiceEvtMsg( // Ask for the audio channel to be switched to listen only mode
liveMeeting.props.meetingProp.intId, // if the user is muted, otherwise switch back to normal mode
mutedUser, // This is only effective if the "transparent listen only" mode is active
liveMeeting.props.voiceProp.voiceConf, // for the target user.
outGW 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, outGW: OutMsgRouter,
eventBus: InternalEventBus, eventBus: InternalEventBus,
users: Vector[ConfVoiceUser] users: Vector[ConfVoiceUser]
): Unit = { )(implicit context: ActorContext): Unit = {
users foreach { cvu => users foreach { cvu =>
VoiceUsers.findWithVoiceUserId( VoiceUsers.findWithVoiceUserId(
liveMeeting.voiceUsers, liveMeeting.voiceUsers,
@ -179,7 +204,9 @@ object VoiceApp extends SystemConfiguration {
ColorPicker.nextColor(liveMeeting.props.meetingProp.intId), ColorPicker.nextColor(liveMeeting.props.meetingProp.intId),
cvu.muted, cvu.muted,
cvu.talking, cvu.talking,
cvu.calledInto cvu.calledInto,
cvu.hold,
cvu.uuid,
) )
} }
} }
@ -229,7 +256,9 @@ object VoiceApp extends SystemConfiguration {
color: String, color: String,
muted: Boolean, muted: Boolean,
talking: Boolean, talking: Boolean,
callingInto: String callingInto: String,
hold: Boolean,
uuid: String = "unused"
): Unit = { ): Unit = {
def broadcastEvent(voiceUserState: VoiceUserState): Unit = { def broadcastEvent(voiceUserState: VoiceUserState): Unit = {
@ -289,7 +318,9 @@ object VoiceApp extends SystemConfiguration {
callingInto, callingInto,
System.currentTimeMillis(), System.currentTimeMillis(),
floor = false, floor = false,
lastFloorTime = "0" lastFloorTime = "0",
hold,
uuid
) )
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState) VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
@ -431,4 +462,108 @@ object VoiceApp extends SystemConfiguration {
) )
outGW.send(deafEvent) 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 _ =>
}
}
} }

View File

@ -20,7 +20,9 @@ trait VoiceApp2x extends UserJoinedVoiceConfEvtMsgHdlr
with SyncGetVoiceUsersMsgHdlr with SyncGetVoiceUsersMsgHdlr
with AudioFloorChangedVoiceConfEvtMsgHdlr with AudioFloorChangedVoiceConfEvtMsgHdlr
with VoiceConfCallStateEvtMsgHdlr with VoiceConfCallStateEvtMsgHdlr
with UserStatusVoiceConfEvtMsgHdlr { with UserStatusVoiceConfEvtMsgHdlr
with ChannelHoldChangedVoiceConfEvtMsgHdlr
with ListenOnlyModeToggledInSfuEvtMsgHdlr {
this: MeetingActor => this: MeetingActor =>
} }

View File

@ -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, def updateSlidePosition(pageId: String, width: Double, height: Double, xOffset: Double, yOffset: Double,
widthRatio: Double, heightRatio: Double) = { widthRatio: Double, heightRatio: Double) = {
DatabaseConnection.db.run( DatabaseConnection.db.run(

View File

@ -58,8 +58,8 @@ object PresPresentationDAO {
yOffset = page._2.yOffset, yOffset = page._2.yOffset,
widthRatio = page._2.widthRatio, widthRatio = page._2.widthRatio,
heightRatio = page._2.heightRatio, heightRatio = page._2.heightRatio,
width = 1, width = page._2.width,
height = 1, height = page._2.height,
viewBoxWidth = 1, viewBoxWidth = 1,
viewBoxHeight = 1, viewBoxHeight = 1,
maxImageWidth = 1440, maxImageWidth = 1440,

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -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")
}
}
}

View File

@ -29,7 +29,9 @@ case class PresentationPage(
xOffset: Double = 0, xOffset: Double = 0,
yOffset: Double = 0, yOffset: Double = 0,
widthRatio: Double = 100D, widthRatio: Double = 100D,
heightRatio: Double = 100D heightRatio: Double = 100D,
width: Double = 1440D,
height: Double = 1080D
) )
object PresentationInPod { object PresentationInPod {

View File

@ -12,6 +12,10 @@ object VoiceUsers {
users.toVector.find(u => u.intId == intId) 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 findAll(users: VoiceUsers): Vector[VoiceUserState] = users.toVector
def findAllNonListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == false) 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 = { def setLastStatusUpdate(users: VoiceUsers, user: VoiceUserState): VoiceUserState = {
val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis()) val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis())
users.save(vu) users.save(vu)
@ -174,7 +189,9 @@ case class VoiceUserVO2x(
callingWith: String, callingWith: String,
listenOnly: Boolean, listenOnly: Boolean,
floor: Boolean, floor: Boolean,
lastFloorTime: String lastFloorTime: String,
hold: Boolean,
uuid: String
) )
case class VoiceUserState( case class VoiceUserState(
@ -190,5 +207,7 @@ case class VoiceUserState(
calledInto: String, calledInto: String,
lastStatusUpdateOn: Long, lastStatusUpdateOn: Long,
floor: Boolean, floor: Boolean,
lastFloorTime: String lastFloorTime: String,
hold: Boolean,
uuid: String
) )

View File

@ -223,6 +223,10 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[GetGlobalAudioPermissionReqMsg](envelope, jsonNode) routeGenericMsg[GetGlobalAudioPermissionReqMsg](envelope, jsonNode)
case GetMicrophonePermissionReqMsg.NAME => case GetMicrophonePermissionReqMsg.NAME =>
routeGenericMsg[GetMicrophonePermissionReqMsg](envelope, jsonNode) routeGenericMsg[GetMicrophonePermissionReqMsg](envelope, jsonNode)
case ChannelHoldChangedVoiceConfEvtMsg.NAME =>
routeVoiceMsg[ChannelHoldChangedVoiceConfEvtMsg](envelope, jsonNode)
case ListenOnlyModeToggledInSfuEvtMsg.NAME =>
routeVoiceMsg[ListenOnlyModeToggledInSfuEvtMsg](envelope, jsonNode)
// Breakout rooms // Breakout rooms
case BreakoutRoomsListMsg.NAME => case BreakoutRoomsListMsg.NAME =>
@ -292,8 +296,6 @@ class ReceivedJsonMsgHandlerActor(
routeGenericMsg[SetCurrentPagePubMsg](envelope, jsonNode) routeGenericMsg[SetCurrentPagePubMsg](envelope, jsonNode)
case ResizeAndMovePagePubMsg.NAME => case ResizeAndMovePagePubMsg.NAME =>
routeGenericMsg[ResizeAndMovePagePubMsg](envelope, jsonNode) routeGenericMsg[ResizeAndMovePagePubMsg](envelope, jsonNode)
case AddSlidePositionsPubMsg.NAME =>
routeGenericMsg[AddSlidePositionsPubMsg](envelope, jsonNode)
case SlideResizedPubMsg.NAME => case SlideResizedPubMsg.NAME =>
routeGenericMsg[SlideResizedPubMsg](envelope, jsonNode) routeGenericMsg[SlideResizedPubMsg](envelope, jsonNode)
case RemovePresentationPubMsg.NAME => case RemovePresentationPubMsg.NAME =>

View File

@ -481,6 +481,10 @@ class MeetingActor(
handleGetGlobalAudioPermissionReqMsg(m) handleGetGlobalAudioPermissionReqMsg(m)
case m: GetMicrophonePermissionReqMsg => case m: GetMicrophonePermissionReqMsg =>
handleGetMicrophonePermissionReqMsg(m) handleGetMicrophonePermissionReqMsg(m)
case m: ChannelHoldChangedVoiceConfEvtMsg =>
handleChannelHoldChangedVoiceConfEvtMsg(m)
case m: ListenOnlyModeToggledInSfuEvtMsg =>
handleListenOnlyModeToggledInSfuEvtMsg(m)
// Layout // Layout
case m: GetCurrentLayoutReqMsg => handleGetCurrentLayoutReqMsg(m) case m: GetCurrentLayoutReqMsg => handleGetCurrentLayoutReqMsg(m)
@ -536,7 +540,6 @@ class MeetingActor(
case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus) case m: PresentationPageCountErrorSysPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationUploadTokenReqMsg => 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: 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: SlideResizedPubMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)
case m: PresentationPageConvertedSysMsg => 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) case m: PresentationPageConversionStartedSysMsg => state = presentationPodsApp.handle(m, state, liveMeeting, msgBus)

View File

@ -102,6 +102,10 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
logMessage(msg) logMessage(msg)
case m: VoiceConfCallStateEvtMsg => logMessage(msg) case m: VoiceConfCallStateEvtMsg => logMessage(msg)
case m: VoiceCallStateEvtMsg => 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 // Breakout
case m: BreakoutRoomEndedEvtMsg => logMessage(msg) case m: BreakoutRoomEndedEvtMsg => logMessage(msg)

View File

@ -67,6 +67,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
msgSender.send(toVoiceConfRedisChannel, json) msgSender.send(toVoiceConfRedisChannel, json)
case GetUsersStatusToVoiceConfSysMsg.NAME => case GetUsersStatusToVoiceConfSysMsg.NAME =>
msgSender.send(toVoiceConfRedisChannel, json) msgSender.send(toVoiceConfRedisChannel, json)
case HoldChannelInVoiceConfSysMsg.NAME =>
msgSender.send(toVoiceConfRedisChannel, json)
// Sent to SFU // Sent to SFU
case EjectUserFromSfuSysMsg.NAME => case EjectUserFromSfuSysMsg.NAME =>
@ -75,6 +77,8 @@ class FromAkkaAppsMsgSenderActor(msgSender: MessageSender)
msgSender.send(toSfuRedisChannel, json) msgSender.send(toSfuRedisChannel, json)
case CamStreamUnsubscribeSysMsg.NAME => case CamStreamUnsubscribeSysMsg.NAME =>
msgSender.send(toSfuRedisChannel, json) msgSender.send(toSfuRedisChannel, json)
case ToggleListenOnlyModeSysMsg.NAME =>
msgSender.send(toSfuRedisChannel, json)
//================================================================== //==================================================================
// Send chat, presentation, and whiteboard in different channels so as not to // Send chat, presentation, and whiteboard in different channels so as not to

View File

@ -45,7 +45,9 @@ trait GuestsWaitingApprovedMsgHdlr extends HandlerHelpers with RightsManagementT
dialInUser.color, dialInUser.color,
MeetingStatus2x.isMeetingMuted(liveMeeting.status), MeetingStatus2x.isMeetingMuted(liveMeeting.status),
false, false,
"freeswitch" "freeswitch",
false,
"unused"
) )
VoiceUsers.findWithIntId( VoiceUsers.findWithIntId(
liveMeeting.voiceUsers, liveMeeting.voiceUsers,

View File

@ -628,4 +628,34 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event) 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)
}
} }

View File

@ -66,7 +66,9 @@ object FakeUserGenerator {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val lastFloorTime = System.currentTimeMillis().toString(); val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, 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, def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -76,7 +78,9 @@ object FakeUserGenerator {
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random) val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
val lastFloorTime = System.currentTimeMillis().toString(); val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, 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 = { def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {

View File

@ -25,7 +25,9 @@ object TestDataGen {
listenOnly: Boolean): VoiceUserState = { listenOnly: Boolean): VoiceUserState = {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name, 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, def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -33,7 +35,9 @@ object TestDataGen {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8) val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16) val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name, 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 = { def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {

View File

@ -107,6 +107,10 @@ voiceConf {
# Path to the audio file being played when dial-in user is waiting for # Path to the audio file being played when dial-in user is waiting for
# approval. This can be relative to FreeSWITCH sounds folder # approval. This can be relative to FreeSWITCH sounds folder
dialInApprovalAudioPath = "ivr/ivr-please_hold_while_party_contacted.wav" 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 { recording {

View File

@ -57,8 +57,18 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
if (event instanceof VoiceUserJoinedEvent) { if (event instanceof VoiceUserJoinedEvent) {
VoiceUserJoinedEvent evt = (VoiceUserJoinedEvent) event; VoiceUserJoinedEvent evt = (VoiceUserJoinedEvent) event;
vcs.userJoinedVoiceConf(evt.getRoom(), evt.getVoiceUserId(), evt.getUserId(), evt.getCallerIdName(), vcs.userJoinedVoiceConf(evt.getRoom(), evt.getVoiceUserId(), evt.getUserId(), evt.getCallerIdName(),
evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith()); evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith(),
} else if (event instanceof VoiceConfRunningEvent) { 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; VoiceConfRunningEvent evt = (VoiceConfRunningEvent) event;
vcs.voiceConfRunning(evt.getRoom(), evt.isRunning()); vcs.voiceConfRunning(evt.getRoom(), evt.isRunning());
} else if (event instanceof VoiceUserLeftEvent) { } else if (event instanceof VoiceUserLeftEvent) {

View File

@ -22,7 +22,9 @@ public interface IVoiceConferenceService {
String callerIdNum, String callerIdNum,
Boolean muted, Boolean muted,
Boolean speaking, Boolean speaking,
String avatarURL); String avatarURL,
Boolean hold,
String uuid);
void voiceUsersStatus(String voiceConfId, void voiceUsersStatus(String voiceConfId,
java.util.List<ConfMember> confMembers, java.util.List<ConfMember> confMembers,
@ -67,4 +69,9 @@ public interface IVoiceConferenceService {
Long receivedResponseTimestamp); Long receivedResponseTimestamp);
void freeswitchHeartbeatEvent(Map<String, String> heartbeat); void freeswitchHeartbeatEvent(Map<String, String> heartbeat);
void channelHoldChanged(String voiceConfId,
String userId,
String uuid,
Boolean hold);
} }

View File

@ -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;
}
}

View File

@ -9,6 +9,8 @@ public class ConfMember {
public final Boolean locked = false; public final Boolean locked = false;
public final String userId; public final String userId;
public final String callingWith; public final String callingWith;
public final Boolean hold;
public final String uuid;
public ConfMember(String userId, public ConfMember(String userId,
String voiceUserId, String voiceUserId,
@ -16,7 +18,9 @@ public class ConfMember {
String callerIdName, String callerIdName,
Boolean muted, Boolean muted,
Boolean speaking, Boolean speaking,
String callingWith) { String callingWith,
Boolean hold,
String uuid) {
this.userId = userId; this.userId = userId;
this.voiceUserId = voiceUserId; this.voiceUserId = voiceUserId;
this.callerIdName = callerIdName; this.callerIdName = callerIdName;
@ -24,5 +28,7 @@ public class ConfMember {
this.muted = muted; this.muted = muted;
this.speaking = speaking; this.speaking = speaking;
this.callingWith = callingWith; this.callingWith = callingWith;
this.hold = hold;
this.uuid = uuid;
} }
} }

View File

@ -28,10 +28,14 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
private final Boolean locked = false; private final Boolean locked = false;
private final String userId; private final String userId;
private final String callingWith; private final String callingWith;
private final Boolean hold;
private final String uuid;
public VoiceUserJoinedEvent(String userId, String voiceUserId, String room, public VoiceUserJoinedEvent(String userId, String voiceUserId, String room,
String callerIdNum, String callerIdName, String callerIdNum, String callerIdName,
Boolean muted, Boolean speaking, String callingWith) { Boolean muted, Boolean speaking, String callingWith,
Boolean hold,
String uuid) {
super(room); super(room);
this.userId = userId; this.userId = userId;
this.voiceUserId = voiceUserId; this.voiceUserId = voiceUserId;
@ -40,6 +44,8 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
this.muted = muted; this.muted = muted;
this.speaking = speaking; this.speaking = speaking;
this.callingWith = callingWith; this.callingWith = callingWith;
this.hold = hold;
this.uuid = uuid;
} }
public String getUserId() { public String getUserId() {
@ -73,4 +79,12 @@ public class VoiceUserJoinedEvent extends VoiceConferenceEvent {
public String getCallingWith() { public String getCallingWith() {
return callingWith; return callingWith;
} }
public String getUUID() {
return uuid;
}
public Boolean getHold() {
return hold;
}
} }

View File

@ -94,6 +94,7 @@ public class ConnectionManager {
//c.addEventFilter(EVENT_NAME, "background_job"); //c.addEventFilter(EVENT_NAME, "background_job");
c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE"); c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE");
c.addEventFilter(EVENT_NAME, "CHANNEL_STATE"); c.addEventFilter(EVENT_NAME, "CHANNEL_STATE");
c.addEventFilter(EVENT_NAME, "CHANNEL_CALLSTATE");
subscribed = true; subscribed = true;
} else { } else {
// Let's check for status every minute. // 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) { public void eject(EjectUserCommand mpc) {
Client c = manager.getESLClient(); Client c = manager.getESLClient();
if (c.canSend()) { if (c.canSend()) {

View File

@ -26,6 +26,9 @@ public class ESLEventListener implements IEslEventListener {
private static final String CONFERENCE_CREATED_EVENT = "conference-create"; private static final String CONFERENCE_CREATED_EVENT = "conference-create";
private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy"; private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy";
private static final String FLOOR_CHANGE_EVENT = "video-floor-change"; 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; private final ConferenceEventListener conferenceEventListener;
@ -59,12 +62,14 @@ public class ESLEventListener implements IEslEventListener {
@Override @Override
public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event) { 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(); Map<String, String> headers = event.getEventHeaders();
String callerId = this.getCallerIdFromEvent(event); String callerId = this.getCallerId(event);
String callerIdName = this.getCallerIdNameFromEvent(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 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 speaking = headers.get("Talking").equals("true") ? true : false;
boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD);
String voiceUserId = callerIdName; String voiceUserId = callerIdName;
@ -124,14 +129,16 @@ public class ESLEventListener implements IEslEventListener {
callerIdName, callerIdName,
muted, muted,
speaking, speaking,
"none"); "none",
hold,
callerUUID);
conferenceEventListener.handleConferenceEvent(pj); conferenceEventListener.handleConferenceEvent(pj);
} }
@Override @Override
public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event) { public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event) {
Integer memberId = this.getMemberIdFromEvent(event); Integer memberId = this.getMemberId(event);
String callerId = this.getCallerIdFromEvent(event); String callerId = this.getCallerId(event);
String callerIdName = this.getCallerIdNameFromEvent(event); String callerIdName = this.getCallerIdNameFromEvent(event);
String callerUUID = this.getMemberUUIDFromEvent(event); String callerUUID = this.getMemberUUIDFromEvent(event);
@ -146,14 +153,14 @@ public class ESLEventListener implements IEslEventListener {
@Override @Override
public void conferenceEventMute(String uniqueId, String confName, int confSize, EslEvent event) { 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); VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, true);
conferenceEventListener.handleConferenceEvent(pm); conferenceEventListener.handleConferenceEvent(pm);
} }
@Override @Override
public void conferenceEventUnMute(String uniqueId, String confName, int confSize, EslEvent event) { 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); VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, false);
conferenceEventListener.handleConferenceEvent(pm); conferenceEventListener.handleConferenceEvent(pm);
} }
@ -165,11 +172,11 @@ public class ESLEventListener implements IEslEventListener {
} }
if (action.equals(START_TALKING_EVENT)) { if (action.equals(START_TALKING_EVENT)) {
Integer memberId = this.getMemberIdFromEvent(event); Integer memberId = this.getMemberId(event);
VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, true); VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, true);
conferenceEventListener.handleConferenceEvent(pt); conferenceEventListener.handleConferenceEvent(pt);
} else if (action.equals(STOP_TALKING_EVENT)) { } 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); VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, false);
conferenceEventListener.handleConferenceEvent(pt); conferenceEventListener.handleConferenceEvent(pt);
} else if (action.equals(CONFERENCE_CREATED_EVENT)) { } else if (action.equals(CONFERENCE_CREATED_EVENT)) {
@ -437,16 +444,92 @@ public class ESLEventListener implements IEslEventListener {
); );
conferenceEventListener.handleConferenceEvent(csEvent); 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) { private Integer getMemberId(EslEvent event) {
return new Integer(e.getEventHeaders().get("Member-ID")); return this.getMemberId(event.getEventHeaders());
} }
private String getCallerIdFromEvent(EslEvent e) { private Integer getMemberId(Map<String, String> eventHeaders) {
return e.getEventHeaders().get("Caller-Caller-ID-Number"); 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) { private String getMemberUUIDFromEvent(EslEvent e) {

View File

@ -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.StopSoundCommand;
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.RecordConferenceCommand; import org.bigbluebutton.freeswitch.voice.freeswitch.actions.RecordConferenceCommand;
import org.bigbluebutton.freeswitch.voice.freeswitch.actions.TransferUserToMeetingCommand; 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.bigbluebutton.freeswitch.voice.freeswitch.actions.*;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -157,6 +158,11 @@ public class FreeswitchApplication implements IDelayedCommandListener{
queueMessage(mpc); queueMessage(mpc);
} }
public void holdChannel(String voiceConfId, String uuid, Boolean hold) {
HoldChannelCommand hcc = new HoldChannelCommand(voiceConfId, uuid, hold, USER);
queueMessage(hcc);
}
private Long genTimestamp() { private Long genTimestamp() {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
} }
@ -220,6 +226,10 @@ public class FreeswitchApplication implements IDelayedCommandListener{
manager.forceEjectUser((ForceEjectUserCommand) command); manager.forceEjectUser((ForceEjectUserCommand) command);
} else if (command instanceof GetUsersStatusCommand) { } else if (command instanceof GetUsersStatusCommand) {
manager.getUsersStatus((GetUsersStatusCommand) command); manager.getUsersStatus((GetUsersStatusCommand) command);
} else if (command instanceof HoldChannelCommand) {
manager.holdChannel((HoldChannelCommand) command);
} else {
log.warn("Unknown command: " + command.getCommand());
} }
} catch (RuntimeException e) { } catch (RuntimeException e) {
log.warn(e.getMessage()); log.warn(e.getMessage());

View File

@ -108,7 +108,9 @@ public class GetAllUsersCommand extends FreeswitchCommand {
} }
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(), 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); eventListener.handleConferenceEvent(pj);
} else if ("recording_node".equals(member.getMemberType())) { } else if ("recording_node".equals(member.getMemberType())) {

View File

@ -106,7 +106,9 @@ public class GetUsersStatusCommand extends FreeswitchCommand {
callerId, callerIdName, callerId, callerIdName,
member.getMuted(), member.getMuted(),
member.getSpeaking(), member.getSpeaking(),
"none"); "none",
member.getHold(),
member.getUUID());
confMembers.add(confMember); confMembers.add(confMember);
} }
} else if ("recording_node".equals(member.getMemberType())) { } else if ("recording_node".equals(member.getMemberType())) {

View File

@ -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;
}
}
}

View File

@ -62,6 +62,10 @@ public class ConferenceMember {
return flags.getIsSpeaking(); return flags.getIsSpeaking();
} }
public boolean getHold() {
return flags.getHold();
}
public void setFlags(ConferenceMemberFlags flags) { public void setFlags(ConferenceMemberFlags flags) {
this.flags = flags; this.flags = flags;
} }

View File

@ -27,6 +27,7 @@ public class ConferenceMemberFlags {
//private boolean canHear = false; //private boolean canHear = false;
private boolean canSpeak = false; private boolean canSpeak = false;
private boolean talking = false; private boolean talking = false;
private boolean hold = false;
//private boolean hasVideo = false; //private boolean hasVideo = false;
//private boolean hasFloor = false; //private boolean hasFloor = false;
//private boolean isModerator = false; //private boolean isModerator = false;
@ -51,4 +52,11 @@ public class ConferenceMemberFlags {
talking = tempVal.equals("true") ? true : false; talking = tempVal.equals("true") ? true : false;
} }
void setHold(String tempVal) {
hold = tempVal.equals("true") ? true : false;
}
boolean getHold() {
return hold;
}
} }

View File

@ -131,6 +131,8 @@ public class XMLResponseConferenceListParser extends DefaultHandler {
tempFlags.setCanSpeak(tempVal); tempFlags.setCanSpeak(tempVal);
}else if (qName.equalsIgnoreCase("talking")) { }else if (qName.equalsIgnoreCase("talking")) {
tempFlags.setTalking(tempVal); tempFlags.setTalking(tempVal);
} else if (qName.equalsIgnoreCase("hold")) {
tempFlags.setHold(tempVal);
} }
}else if (qName.equalsIgnoreCase("id")) { }else if (qName.equalsIgnoreCase("id")) {
try { try {

View File

@ -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)
}
}
} }

View File

@ -60,6 +60,8 @@ class RxJsonMsgHdlrActor(val fsApp: FreeswitchApplication) extends Actor with Ac
routeCheckRunningAndRecordingToVoiceConfSysMsg(envelope, jsonNode) routeCheckRunningAndRecordingToVoiceConfSysMsg(envelope, jsonNode)
case GetUsersStatusToVoiceConfSysMsg.NAME => case GetUsersStatusToVoiceConfSysMsg.NAME =>
routeGetUsersStatusToVoiceConfSysMsg(envelope, jsonNode) routeGetUsersStatusToVoiceConfSysMsg(envelope, jsonNode)
case HoldChannelInVoiceConfSysMsg.NAME =>
routeHoldChannelInVoiceConfMsg(envelope, jsonNode)
case _ => // do nothing case _ => // do nothing
} }
} }

View File

@ -90,7 +90,9 @@ class VoiceConferenceService(healthz: HealthzService,
cm.muted, cm.muted,
cm.speaking, cm.speaking,
cm.callingWith, cm.callingWith,
"freeswitch" "freeswitch",
cm.hold,
cm.uuid
) )
} }
@ -119,12 +121,16 @@ class VoiceConferenceService(healthz: HealthzService,
callerIdNum: String, callerIdNum: String,
muted: java.lang.Boolean, muted: java.lang.Boolean,
talking: 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 header = BbbCoreVoiceConfHeader(UserJoinedVoiceConfEvtMsg.NAME, voiceConfId)
val body = UserJoinedVoiceConfEvtMsgBody(voiceConfId, voiceUserId, userId, callerIdName, callerIdNum, 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 envelope = BbbCoreEnvelope(UserJoinedVoiceConfEvtMsg.NAME, Map("voiceConf" -> voiceConfId))
val msg = new UserJoinedVoiceConfEvtMsg(header, body) val msg = new UserJoinedVoiceConfEvtMsg(header, body)
@ -248,6 +254,28 @@ class VoiceConferenceService(healthz: HealthzService,
sender.publish(fromVoiceConfRedisChannel, json) 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( def freeswitchStatusReplyEvent(
sendCommandTimestamp: java.lang.Long, sendCommandTimestamp: java.lang.Long,
status: java.util.List[String], status: java.util.List[String],

View File

@ -6,7 +6,7 @@ case class PresentationVO(id: String, temporaryPresentationId: String, name: Str
case class PageVO(id: String, num: Int, thumbUri: String = "", case class PageVO(id: String, num: Int, thumbUri: String = "",
txtUri: String, svgUri: String, current: Boolean = false, xOffset: Double = 0, 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, case class PresentationPodVO(id: String, currentPresenter: String,
presentations: Vector[PresentationVO]) presentations: Vector[PresentationVO])
@ -15,7 +15,9 @@ case class PresentationPageConvertedVO(
id: String, id: String,
num: Int, num: Int,
urls: Map[String, String], urls: Map[String, String],
current: Boolean = false current: Boolean = false,
width: Double = 1440D,
height: Double = 1080D
) )
case class PresentationPageVO( case class PresentationPageVO(

View File

@ -45,11 +45,6 @@ case class SlideResizedPubMsg(header: BbbClientMsgHeader, body: SlideResizedPubM
case class SlideResizedPubMsgBody(pageId: String, width: Double, height: Double, case class SlideResizedPubMsgBody(pageId: String, width: Double, height: Double,
xOffset: Double, yOffset: Double, widthRatio: Double, heightRatio: 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" } object SetCurrentPresentationPubMsg { val NAME = "SetCurrentPresentationPubMsg" }
case class SetCurrentPresentationPubMsg(header: BbbClientMsgHeader, body: SetCurrentPresentationPubMsgBody) extends StandardMsg case class SetCurrentPresentationPubMsg(header: BbbClientMsgHeader, body: SetCurrentPresentationPubMsgBody) extends StandardMsg
case class SetCurrentPresentationPubMsgBody(podId: String, presentationId: String) case class SetCurrentPresentationPubMsgBody(podId: String, presentationId: String)

View File

@ -387,8 +387,9 @@ case class UserStatusVoiceConfEvtMsgBody(voiceConf: String, confUsers: Vector[Co
case class ConfVoiceUser(voiceUserId: String, intId: String, case class ConfVoiceUser(voiceUserId: String, intId: String,
callerIdName: String, callerIdNum: String, muted: Boolean, callerIdName: String, callerIdNum: String, muted: Boolean,
talking: Boolean, callingWith: String, talking: Boolean, callingWith: String,
calledInto: String // freeswitch, kms calledInto: String, // freeswitch, kms
) hold: Boolean,
uuid: String)
case class ConfVoiceRecording(recordPath: String, recordStartTime: Long) case class ConfVoiceRecording(recordPath: String, recordStartTime: Long)
/** /**
@ -401,7 +402,9 @@ case class UserJoinedVoiceConfEvtMsg(
) extends VoiceStandardMsg ) extends VoiceStandardMsg
case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, intId: String, case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, intId: String,
callerIdName: String, callerIdNum: String, muted: Boolean, 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. * Sent to client that a user has joined the voice conference.
@ -639,3 +642,64 @@ case class GetMicrophonePermissionRespMsgBody(
sfuSessionId: String, sfuSessionId: String,
allowed: Boolean 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
)

View File

@ -112,5 +112,6 @@ libraryDependencies ++= Seq(
"com.zaxxer" % "HikariCP" % "4.0.3", "com.zaxxer" % "HikariCP" % "4.0.3",
"commons-validator" % "commons-validator" % "1.7", "commons-validator" % "commons-validator" % "1.7",
"org.apache.tika" % "tika-core" % "2.8.0", "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"
) )

View File

@ -12,14 +12,12 @@ import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; 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.FileUtils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException; import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.entity.ContentType; import org.apache.http.entity.ContentType;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients; import org.apache.http.impl.nio.client.HttpAsyncClients;
@ -197,6 +195,7 @@ public class PresentationUrlDownloadService {
conn.setReadTimeout(60000); conn.setReadTimeout(60000);
conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8"); conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
conn.addRequestProperty("User-Agent", "Mozilla"); conn.addRequestProperty("User-Agent", "Mozilla");
conn.setInstanceFollowRedirects(false);
// normally, 3xx is redirect // normally, 3xx is redirect
int status = conn.getResponseCode(); int status = conn.getResponseCode();
@ -287,10 +286,21 @@ public class PresentationUrlDownloadService {
String finalUrl = followRedirect(meetingId, urlString, 0, urlString); String finalUrl = followRedirect(meetingId, urlString, 0, urlString);
if (finalUrl == null) return false; if (finalUrl == null) return false;
if(!finalUrl.equals(urlString)) {
log.info("Redirected to Final URL [{}]", finalUrl);
}
boolean success = false; 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 { try {
httpclient.start(); httpclient.start();
File download = new File(filename); File download = new File(filename);

View File

@ -6,6 +6,11 @@ import org.bigbluebutton.common2.domain.{ DefaultProps, PageVO, PresentationPage
import org.bigbluebutton.common2.msgs._ import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.presentation.messages._ import org.bigbluebutton.presentation.messages._
import java.io.IOException
import java.net.URL
import javax.imageio.ImageIO
import scala.xml.XML
object MsgBuilder { object MsgBuilder {
def buildDestroyMeetingSysCmdMsg(msg: DestroyMeetingMessage): BbbCommonEnvCoreMsg = { def buildDestroyMeetingSysCmdMsg(msg: DestroyMeetingMessage): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-web") val routing = collection.immutable.HashMap("sender" -> "bbb-web")
@ -75,12 +80,34 @@ object MsgBuilder {
val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl) val urls = Map("thumb" -> thumbUrl, "text" -> txtUrl, "svg" -> svgUrl, "png" -> pngUrl)
PresentationPageConvertedVO( try {
id = id, val imgUrl = new URL(svgUrl)
num = page, val imgContent = XML.load(imgUrl)
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 = { def buildPresentationPageConvertedSysMsg(msg: DocPageGeneratedProgress): BbbCommonEnvCoreMsg = {

View File

@ -468,17 +468,22 @@ CREATE TABLE "user_voice" (
"startTime" bigint "startTime" bigint
); );
--CREATE INDEX "idx_user_voice_userId" ON "user_voice"("userId"); --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; ALTER TABLE "user_voice" ADD COLUMN "hideTalkingIndicatorAt" timestamp with time zone
CREATE INDEX "idx_user_voice_userId_talking" ON "user_voice"("userId","hideTalkingIndicatorAt","startTime"); 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 CREATE OR REPLACE VIEW "v_user_voice" AS
SELECT SELECT
u."meetingId", u."meetingId",
"user_voice" .*, "user_voice" .*,
greatest(coalesce(user_voice."startTime", 0), coalesce(user_voice."endTime", 0)) AS "lastSpeakChangedAt", 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 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" ( CREATE TABLE "user_camera" (
"streamId" varchar(100) PRIMARY KEY, "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"; 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 CREATE OR REPLACE VIEW "v_current_time" AS

View File

@ -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

View File

@ -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

View File

@ -25,3 +25,4 @@ select_permissions:
filter: filter:
meetingId: meetingId:
_eq: X-Hasura-MeetingId _eq: X-Hasura-MeetingId
allow_aggregations: true

View File

@ -79,6 +79,15 @@ object_relationships:
remote_table: remote_table:
name: v_user_reaction name: v_user_reaction
schema: public schema: public
- name: sharedNotesSession
using:
manual_configuration:
column_mapping:
userId: userId
insertion_order: null
remote_table:
name: v_sharedNotes_session
schema: public
- name: voice - name: voice
using: using:
manual_configuration: manual_configuration:

View File

@ -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.yaml"
- "!include public_v_breakoutRoom_assignedUser.yaml" - "!include public_v_breakoutRoom_assignedUser.yaml"
- "!include public_v_breakoutRoom_participant.yaml" - "!include public_v_breakoutRoom_participant.yaml"
- "!include public_v_chat.yaml" - "!include public_v_chat.yaml"
- "!include public_v_chat_message_private.yaml" - "!include public_v_chat_message_private.yaml"
- "!include public_v_chat_message_public.yaml" - "!include public_v_chat_message_public.yaml"
- "!include public_v_chat_user.yaml"
- "!include public_v_current_time.yaml" - "!include public_v_current_time.yaml"
- "!include public_v_external_video.yaml" - "!include public_v_external_video.yaml"
- "!include public_v_meeting.yaml"
- "!include public_v_meeting_breakoutPolicies.yaml" - "!include public_v_meeting_breakoutPolicies.yaml"
- "!include public_v_meeting_group.yaml" - "!include public_v_meeting_group.yaml"
- "!include public_v_meeting_lockSettings.yaml" - "!include public_v_meeting_lockSettings.yaml"
@ -30,14 +28,18 @@
- "!include public_v_pres_page_writers.yaml" - "!include public_v_pres_page_writers.yaml"
- "!include public_v_pres_presentation.yaml" - "!include public_v_pres_presentation.yaml"
- "!include public_v_screenshare.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_timer.yaml"
- "!include public_v_user.yaml" - "!include public_v_user.yaml"
- "!include public_v_user_breakoutRoom.yaml" - "!include public_v_user_breakoutRoom.yaml"
- "!include public_v_user_camera.yaml" - "!include public_v_user_camera.yaml"
- "!include public_v_user_connectionStatus.yaml"
- "!include public_v_user_connectionStatusReport.yaml" - "!include public_v_user_connectionStatusReport.yaml"
- "!include public_v_user_current.yaml" - "!include public_v_user_current.yaml"
- "!include public_v_user_customParameter.yaml" - "!include public_v_user_customParameter.yaml"
- "!include public_v_user_guest.yaml" - "!include public_v_user_guest.yaml"
- "!include public_v_user_localSettings.yaml"
- "!include public_v_user_reaction.yaml" - "!include public_v_user_reaction.yaml"
- "!include public_v_user_reaction_current.yaml" - "!include public_v_user_reaction_current.yaml"
- "!include public_v_user_ref.yaml" - "!include public_v_user_ref.yaml"

View File

@ -34,4 +34,4 @@ if [ "$akka_apps_status" = "active" ]; then
fi fi
echo "Applying new metadata to Hasura" echo "Applying new metadata to Hasura"
/usr/local/bin/hasura metadata apply /usr/local/bin/hasura/hasura metadata apply

View File

@ -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

View File

@ -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

View File

@ -366,6 +366,10 @@ start_bigbluebutton () {
echo "Starting 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 systemctl restart bigbluebutton.target
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then

View File

@ -5,7 +5,7 @@
meteor-base@1.5.1 meteor-base@1.5.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.16.6 mongo@1.16.7
reactive-var@1.0.12 reactive-var@1.0.12
standard-minifier-css@1.9.2 standard-minifier-css@1.9.2

View File

@ -1 +1 @@
METEOR@2.12 METEOR@2.13

View File

@ -13,7 +13,7 @@ check@1.3.2
ddp@1.4.1 ddp@1.4.1
ddp-client@2.6.1 ddp-client@2.6.1
ddp-common@1.4.0 ddp-common@1.4.0
ddp-server@2.6.1 ddp-server@2.6.2
diff-sequence@1.1.2 diff-sequence@1.1.2
dynamic-import@0.7.3 dynamic-import@0.7.3
ecmascript@0.16.7 ecmascript@0.16.7
@ -33,7 +33,7 @@ inter-process-messaging@0.1.1
launch-screen@1.3.0 launch-screen@1.3.0
lmieulet:meteor-coverage@4.1.0 lmieulet:meteor-coverage@4.1.0
logging@1.3.2 logging@1.3.2
meteor@1.11.2 meteor@1.11.3
meteor-base@1.5.1 meteor-base@1.5.1
meteortesting:browser-tests@1.3.5 meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3 meteortesting:mocha@2.0.3
@ -46,7 +46,7 @@ mobile-status-bar@1.1.0
modern-browsers@0.1.9 modern-browsers@0.1.9
modules@0.19.0 modules@0.19.0
modules-runtime@0.13.1 modules-runtime@0.13.1
mongo@1.16.6 mongo@1.16.7
mongo-decimal@0.1.3 mongo-decimal@0.1.3
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.8

View File

@ -8,6 +8,7 @@ import {
getMappedFallbackStun, getMappedFallbackStun,
} from '/imports/utils/fetchStunTurnServers'; } from '/imports/utils/fetchStunTurnServers';
import getFromMeetingSettings from '/imports/ui/services/meeting-settings'; import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import { import {
getAudioSessionNumber, getAudioSessionNumber,
@ -26,6 +27,8 @@ const MEDIA = Meteor.settings.public.media;
const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer; const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer;
const RETRY_THROUGH_RELAY = MEDIA.audio.retryThroughRelay || false; const RETRY_THROUGH_RELAY = MEDIA.audio.retryThroughRelay || false;
const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering; 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 MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
const CONNECTION_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000; const CONNECTION_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000;
const { audio: NETWORK_PRIORITY } = MEDIA.networkPriorities || {}; 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 { export default class SFUAudioBridge extends BaseAudioBridge {
static getOfferingRole(isListenOnly) {
return isListenOnly
? LISTEN_ONLY_OFFERING
: (!isTransparentListenOnlyEnabled() && FULLAUDIO_OFFERING);
}
constructor(userData) { constructor(userData) {
super(); super();
this.userId = userData.userId; this.userId = userData.userId;
@ -266,6 +280,11 @@ export default class SFUAudioBridge extends BaseAudioBridge {
}, },
}, 'SFU audio media play failed due to autoplay error'); }, 'SFU audio media play failed due to autoplay error');
this.dispatchAutoplayHandlingEvent(mediaElement); 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 { } else {
const normalizedError = { const normalizedError = {
errorCode: 1004, errorCode: 1004,
@ -320,12 +339,13 @@ export default class SFUAudioBridge extends BaseAudioBridge {
constraints: getAudioConstraints({ deviceId: this.inputDeviceId }), constraints: getAudioConstraints({ deviceId: this.inputDeviceId }),
forceRelay: _forceRelay || shouldForceRelay(), forceRelay: _forceRelay || shouldForceRelay(),
stream: (inputStream && inputStream.active) ? inputStream : undefined, stream: (inputStream && inputStream.active) ? inputStream : undefined,
offering: isListenOnly ? LISTEN_ONLY_OFFERING : true, offering: SFUAudioBridge.getOfferingRole(this.isListenOnly),
signalCandidates: SIGNAL_CANDIDATES, signalCandidates: SIGNAL_CANDIDATES,
traceLogs: TRACE_LOGS, traceLogs: TRACE_LOGS,
networkPriority: NETWORK_PRIORITY, networkPriority: NETWORK_PRIORITY,
mediaStreamFactory: this.mediaStreamFactory, mediaStreamFactory: this.mediaStreamFactory,
gatheringTimeout: GATHERING_TIMEOUT, gatheringTimeout: GATHERING_TIMEOUT,
transparentListenOnly: isTransparentListenOnlyEnabled(),
}; };
this.broker = new AudioBroker( this.broker = new AudioBroker(

View File

@ -1,6 +1,7 @@
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import RegexWebUrl from '/imports/utils/regex-weburl'; import RegexWebUrl from '/imports/utils/regex-weburl';
import { BREAK_LINE } from '/imports/utils/lineEndings';
const MSG_DIRECT_TYPE = 'DIRECT'; const MSG_DIRECT_TYPE = 'DIRECT';
const NODE_USER = 'nodeJSapp'; const NODE_USER = 'nodeJSapp';
@ -25,9 +26,29 @@ export const parseMessage = (message) => {
// Replace flash links to flash valid ones // Replace flash links to flash valid ones
parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>"); 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; 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 spokeTimeoutHandles = {};
export const clearSpokeTimeout = (meetingId, userId) => { export const clearSpokeTimeout = (meetingId, userId) => {

View File

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis'; 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'; import Logger from '/imports/startup/server/logger';
export default function sendGroupChatMsg(chatId, message) { export default function sendGroupChatMsg(chatId, message) {
@ -17,8 +17,7 @@ export default function sendGroupChatMsg(chatId, message) {
check(requesterUserId, String); check(requesterUserId, String);
check(chatId, String); check(chatId, String);
check(message, Object); check(message, Object);
const parsedMessage = parseMessage(message.message); message.message = textToMarkdown(message.message);
message.message = parsedMessage;
const payload = { const payload = {
msg: message, msg: message,

View File

@ -2,7 +2,7 @@ import { GroupChatMsg } from '/imports/api/group-chat-msg';
import GroupChat from '/imports/api/group-chat'; import GroupChat from '/imports/api/group-chat';
import Logger from '/imports/startup/server/logger'; import Logger from '/imports/startup/server/logger';
import flat from 'flat'; import flat from 'flat';
import { parseMessage } from '/imports/api/common/server/helpers'; import { parseMessage } from './addGroupChatMsg';
export default async function addBulkGroupChatMsgs(msgs) { export default async function addBulkGroupChatMsgs(msgs) {
if (!msgs.length) return; if (!msgs.length) return;

View File

@ -47,6 +47,8 @@ export default async function addPresentation(meetingId, podId, presentation) {
yOffset: Number, yOffset: Number,
widthRatio: Number, widthRatio: Number,
heightRatio: Number, heightRatio: Number,
width: Number,
height: Number,
}, },
], ],
downloadable: Boolean, downloadable: Boolean,
@ -62,13 +64,14 @@ export default async function addPresentation(meetingId, podId, presentation) {
}; };
const modifier = { const modifier = {
$set: Object.assign({ $set: {
meetingId, meetingId,
podId, podId,
'conversion.done': true, 'conversion.done': true,
'conversion.error': false, 'conversion.error': false,
'exportation.status': null, 'exportation.status': null,
}, flat(presentation, { safe: true })), ...flat(presentation, { safe: true }),
},
}; };
try { try {

View File

@ -57,6 +57,8 @@ export default async function addSlide(meetingId, podId, presentationId, slide)
yOffset: Number, yOffset: Number,
widthRatio: Number, widthRatio: Number,
heightRatio: Number, heightRatio: Number,
width: Number,
height: Number,
content: String, content: String,
}); });
@ -79,15 +81,15 @@ export default async function addSlide(meetingId, podId, presentationId, slide)
const imageUri = slide.svgUri || slide.pngUri; const imageUri = slide.svgUri || slide.pngUri;
const modifier = { const modifier = {
$set: Object.assign( $set: {
{ meetingId }, meetingId,
{ podId }, podId,
{ presentationId }, presentationId,
{ id: slideId }, id: slideId,
{ imageUri }, imageUri,
flat(restSlide), ...flat(restSlide),
{ safe: true }, safe: true,
), },
}; };
const imageSizeUri = (loadSlidesFromHttpAlways ? imageUri.replace(/^https/i, 'http') : imageUri); const imageSizeUri = (loadSlidesFromHttpAlways ? imageUri.replace(/^https/i, 'http') : imageUri);

View File

@ -1,6 +1,4 @@
import { SlidePositions } from '/imports/api/slides'; 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 Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import flat from 'flat'; import flat from 'flat';
@ -12,10 +10,6 @@ export default async function addSlidePositions(
slideId, slideId,
slidePosition, slidePosition,
) { ) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'AddSlidePositionsPubMsg';
check(meetingId, String); check(meetingId, String);
check(podId, String); check(podId, String);
check(presentationId, String); check(presentationId, String);
@ -56,21 +50,6 @@ export default async function addSlidePositions(
} else { } else {
Logger.info(`Upserted slide position id=${slideId} pod=${podId} presentation=${presentationId}`); 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) { } catch (err) {
Logger.error(`Adding slide position to collection: ${err}`); Logger.error(`Adding slide position to collection: ${err}`);
} }

View File

@ -16,7 +16,6 @@ const notifyExpiredReaction = (meetingId, userId) => {
check(meetingId, String); check(meetingId, String);
const payload = { const payload = {
emoji,
userId, userId,
}; };

View File

@ -35,6 +35,7 @@ const currentParameters = [
'bbb_skip_check_audio', 'bbb_skip_check_audio',
'bbb_skip_check_audio_on_first_join', 'bbb_skip_check_audio_on_first_join',
'bbb_fullaudio_bridge', 'bbb_fullaudio_bridge',
'bbb_transparent_listen_only',
// BRANDING // BRANDING
'bbb_display_branding_area', 'bbb_display_branding_area',
// SHORTCUTS // SHORTCUTS

View File

@ -24,7 +24,7 @@ import { makeCall } from '/imports/ui/services/api';
import BBBStorage from '/imports/ui/services/storage'; import BBBStorage from '/imports/ui/services/storage';
const CHAT_CONFIG = Meteor.settings.public.chat; 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 USER_WAS_EJECTED = 'userWasEjected';
const HTML = document.getElementsByTagName('html')[0]; const HTML = document.getElementsByTagName('html')[0];

View File

@ -301,7 +301,12 @@ class ActionsDropdown extends PureComponent {
} }
makePresentationItems() { makePresentationItems() {
const { presentations, setPresentation, podIds } = this.props; const {
presentations,
setPresentation,
podIds,
setPresentationFitToWidth,
} = this.props;
if (!podIds || podIds.length < 1) return []; if (!podIds || podIds.length < 1) return [];
@ -314,18 +319,21 @@ class ActionsDropdown extends PureComponent {
.map((p) => { .map((p) => {
const customStyles = { color: colorPrimary }; const customStyles = { color: colorPrimary };
return { return (
customStyles: p.current ? customStyles : null, {
icon: 'file', customStyles: p.current ? customStyles : null,
iconRight: p.current ? 'check' : null, icon: "file",
selected: p.current ? true : false, iconRight: p.current ? 'check' : null,
label: p.name, selected: p.current ? true : false,
description: 'uploaded presentation file', label: p.name,
key: `uploaded-presentation-${p.id}`, description: "uploaded presentation file",
onClick: () => { key: `uploaded-presentation-${p.id}`,
setPresentation(p.id, podId); onClick: () => {
}, setPresentationFitToWidth(false);
}; setPresentation(p.id, podId);
},
}
);
}); });
return presentationItemElements; return presentationItemElements;
} }

View File

@ -75,6 +75,7 @@ class ActionsBar extends PureComponent {
setMeetingLayout, setMeetingLayout,
showPushLayout, showPushLayout,
setPushLayout, setPushLayout,
setPresentationFitToWidth,
} = this.props; } = this.props;
const { isCaptionsReaderMenuModalOpen } = this.state; const { isCaptionsReaderMenuModalOpen } = this.state;
@ -109,6 +110,7 @@ class ActionsBar extends PureComponent {
presentationIsOpen, presentationIsOpen,
showPushLayout, showPushLayout,
hasCameraAsContent, hasCameraAsContent,
setPresentationFitToWidth,
}} }}
/> />
{isCaptionsAvailable {isCaptionsAvailable

View File

@ -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 RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
const isReactionsButtonEnabled = () => { const isReactionsButtonEnabled = () => {
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.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(() => ({ export default withTracker(() => ({

View File

@ -57,7 +57,7 @@ const FreeJoinLabel = styled.label`
display: flex; display: flex;
align-items: center; align-items: center;
font-size: ${fontSizeSmall}; font-size: ${fontSizeSmall};
margin-bottom: 0; margin-bottom: 0.2rem;
& > * { & > * {
margin: 0 .5rem 0 0; margin: 0 .5rem 0 0;
@ -125,9 +125,9 @@ const RoomName = styled(BreakoutNameInput)`
const BreakoutSettings = styled.div` const BreakoutSettings = styled.div`
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 2fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-gap: 4rem; grid-gap: 2rem;
@media ${smallOnly} { @media ${smallOnly} {
grid-template-columns: 1fr ; grid-template-columns: 1fr ;
@ -235,7 +235,6 @@ const AssignBtns = styled(Button)`
`; `;
const CheckBoxesContainer = styled(FlexRow)` const CheckBoxesContainer = styled(FlexRow)`
white-space: nowrap;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: flex-end; justify-content: flex-end;

View File

@ -2,11 +2,11 @@ import React, { useState } from 'react';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import BBBMenu from '/imports/ui/components/common/menu/component'; 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 UserReactionService from '/imports/ui/components/user-reaction/service';
import UserListService from '/imports/ui/components/user-list/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 ReactionsButton = (props) => {
const { const {
@ -16,6 +16,7 @@ const ReactionsButton = (props) => {
raiseHand, raiseHand,
isMobile, isMobile,
currentUserReaction, currentUserReaction,
autoCloseReactionsBar,
} = props; } = props;
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@ -25,6 +26,14 @@ const ReactionsButton = (props) => {
id: 'app.actionsBar.reactions.reactionsButtonLabel', id: 'app.actionsBar.reactions.reactionsButtonLabel',
description: 'reactions Label', 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 = () => { const handleClose = () => {
@ -43,17 +52,80 @@ const ReactionsButton = (props) => {
UserListService.setUserRaiseHand(userId, !raiseHand); UserListService.setUserRaiseHand(userId, !raiseHand);
}; };
const renderReactionsBar = () => ( const RaiseHandButtonLabel = () => {
<Styled.Wrapper> if (isMobile) return null;
<ReactionsBar
{...props}
onReactionSelect={handleReactionSelect}
onRaiseHand={handleRaiseHandButtonClick}
/>
</Styled.Wrapper>
);
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 ( return (
<BBBMenu <BBBMenu
@ -61,26 +133,31 @@ const ReactionsButton = (props) => {
<Styled.ReactionsDropdown> <Styled.ReactionsDropdown>
<Styled.RaiseHandButton <Styled.RaiseHandButton
data-test="reactionsButton" data-test="reactionsButton"
icon="hand" icon={icon}
customIcon={customIcon}
label={intl.formatMessage(intlMessages.reactionsLabel)} label={intl.formatMessage(intlMessages.reactionsLabel)}
description="Reactions" description="Reactions"
ghost={!showEmojiPicker} ghost={!showEmojiPicker && !customIcon}
onKeyPress={() => {}} onKeyPress={() => {}}
onClick={() => setShowEmojiPicker(true)} onClick={() => setShowEmojiPicker(true)}
color={showEmojiPicker ? 'primary' : 'default'} color={showEmojiPicker || customIcon ? 'primary' : 'default'}
hideLabel hideLabel
circle circle
size="lg" size="lg"
/> />
</Styled.ReactionsDropdown> </Styled.ReactionsDropdown>
)} )}
renderOtherComponents={showEmojiPicker ? renderReactionsBar() : null} actions={actions}
onCloseCallback={() => handleClose()} onCloseCallback={() => handleClose()}
customAnchorEl={!isMobile ? actionsBarRef.current : null} customAnchorEl={!isMobile ? actionsBarRef.current : null}
customStyles={customStyles} customStyles={customStyles}
open={showEmojiPicker} open={showEmojiPicker}
hasRoundedCorners hasRoundedCorners
overrideMobileStyles overrideMobileStyles
isHorizontal={!isMobile}
isMobile={isMobile}
roundButtons={true}
keepOpen={!autoCloseReactionsBar}
opts={{ opts={{
id: 'reactions-dropdown-menu', id: 'reactions-dropdown-menu',
keepMounted: true, keepMounted: true,

View File

@ -6,6 +6,7 @@ import ReactionsButton from './component';
import actionsBarService from '../service'; import actionsBarService from '../service';
import UserReactionService from '/imports/ui/components/user-reaction/service'; import UserReactionService from '/imports/ui/components/user-reaction/service';
import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums'; import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums';
import SettingsService from '/imports/ui/services/settings';
const ReactionsButtonContainer = ({ ...props }) => { const ReactionsButtonContainer = ({ ...props }) => {
const layoutContextDispatch = layoutDispatch(); const layoutContextDispatch = layoutDispatch();
@ -32,6 +33,7 @@ export default injectIntl(withTracker(() => {
emoji: currentUser.emoji, emoji: currentUser.emoji,
currentUserReaction: currentUserReaction.reaction, currentUserReaction: currentUserReaction.reaction,
raiseHand: currentUser.raiseHand, raiseHand: currentUser.raiseHand,
autoCloseReactionsBar: SettingsService?.application?.autoCloseReactionsBar,
}; };
})(ReactionsButtonContainer)); })(ReactionsButtonContainer));

View File

@ -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,
};

View File

@ -450,6 +450,7 @@ class App extends Component {
setMeetingLayout={setMeetingLayout} setMeetingLayout={setMeetingLayout}
showPushLayout={showPushLayoutButton && selectedLayout === 'custom'} showPushLayout={showPushLayoutButton && selectedLayout === 'custom'}
presentationIsOpen={presentationIsOpen} presentationIsOpen={presentationIsOpen}
setPresentationFitToWidth={this.setPresentationFitToWidth}
/> />
</Styled.ActionsBar> </Styled.ActionsBar>
); );
@ -582,6 +583,7 @@ class App extends Component {
isAudioModalOpen, isAudioModalOpen,
isRandomUserSelectModalOpen, isRandomUserSelectModalOpen,
isVideoPreviewModalOpen, isVideoPreviewModalOpen,
presentationFitToWidth,
allPluginsLoaded, allPluginsLoaded,
} = this.state; } = this.state;
return ( return (
@ -612,46 +614,40 @@ class App extends Component {
<NavBarContainer main="new" /> <NavBarContainer main="new" />
<WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} /> <WebcamContainer isLayoutSwapped={!presentationIsOpen} layoutType={selectedLayout} />
<Styled.TextMeasure id="text-measure" /> <Styled.TextMeasure id="text-measure" />
{shouldShowPresentation ? ( {shouldShowPresentation ? <PresentationAreaContainer setPresentationFitToWidth={this.setPresentationFitToWidth} fitToWidth={presentationFitToWidth} darkTheme={darkTheme} presentationIsOpen={presentationIsOpen} layoutType={selectedLayout} /> : null}
<PresentationAreaContainer {shouldShowScreenshare ? <ScreenshareContainer isLayoutSwapped={!presentationIsOpen} /> : null}
darkTheme={darkTheme} {
presentationIsOpen={presentationIsOpen} shouldShowExternalVideo
layoutType={selectedLayout} ? <ExternalVideoContainer isLayoutSwapped={!presentationIsOpen} isPresenter={isPresenter} />
/> : null
) : null} }
{shouldShowScreenshare ? ( {shouldShowSharedNotes
<ScreenshareContainer isLayoutSwapped={!presentationIsOpen} /> ? (
) : null} <NotesContainer
{shouldShowExternalVideo ? ( area="media"
<ExternalVideoContainer layoutType={selectedLayout}
isLayoutSwapped={!presentationIsOpen} />
isPresenter={isPresenter} ) : null}
/>
) : null}
{shouldShowSharedNotes ? (
<NotesContainer area="media" layoutType={selectedLayout} />
) : null}
{this.renderCaptions()} {this.renderCaptions()}
<AudioCaptionsSpeechContainer /> <AudioCaptionsSpeechContainer />
{this.renderAudioCaptions()} {this.renderAudioCaptions()}
<UploaderContainer /> <UploaderContainer />
<CaptionsSpeechContainer /> <CaptionsSpeechContainer />
<BreakoutRoomInvitation /> <BreakoutRoomInvitation />
<AudioContainer <AudioContainer {...{
{...{ isAudioModalOpen,
isAudioModalOpen, setAudioModalIsOpen: this.setAudioModalIsOpen,
setAudioModalIsOpen: this.setAudioModalIsOpen, isVideoPreviewModalOpen,
isVideoPreviewModalOpen, setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen,
setVideoPreviewModalIsOpen: this.setVideoPreviewModalIsOpen, }} />
}}
/>
<ToastContainer rtl /> <ToastContainer rtl />
{(audioAlertEnabled || pushAlertEnabled) && ( {(audioAlertEnabled || pushAlertEnabled)
<ChatAlertContainer && (
audioAlertEnabled={audioAlertEnabled} <ChatAlertContainer
pushAlertEnabled={pushAlertEnabled} audioAlertEnabled={audioAlertEnabled}
/> pushAlertEnabled={pushAlertEnabled}
)} />
)}
<RaiseHandNotifier /> <RaiseHandNotifier />
<ManyWebcamsNotifier /> <ManyWebcamsNotifier />
<PollingContainer /> <PollingContainer />
@ -659,23 +655,15 @@ class App extends Component {
<WakeLockContainer /> <WakeLockContainer />
{this.renderActionsBar()} {this.renderActionsBar()}
{customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null} {customStyleUrl ? <link rel="stylesheet" type="text/css" href={customStyleUrl} /> : null}
{customStyle ? ( {customStyle ? <link rel="stylesheet" type="text/css" href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} /> : null}
<link {isRandomUserSelectModalOpen ? <RandomUserSelectContainer
rel="stylesheet" {...{
type="text/css" onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
href={`data:text/css;charset=UTF-8,${encodeURIComponent(customStyle)}`} priority: "low",
/> setIsOpen: this.setRandomUserSelectModalIsOpen,
) : null} isOpen: isRandomUserSelectModalOpen,
{isRandomUserSelectModalOpen ? ( }}
<RandomUserSelectContainer /> : null}
{...{
onRequestClose: () => this.setRandomUserSelectModalIsOpen(false),
priority: 'low',
setIsOpen: this.setRandomUserSelectModalIsOpen,
isOpen: isRandomUserSelectModalOpen,
}}
/>
) : null}
</Styled.Layout> </Styled.Layout>
</> </>
); );

View File

@ -1,7 +1,7 @@
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import { throttle } from '/imports/utils/throttle'; 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 AudioManager from '/imports/ui/services/audio-manager';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
@ -123,7 +123,7 @@ export default {
joinListenOnly: () => AudioManager.joinListenOnly(), joinListenOnly: () => AudioManager.joinListenOnly(),
joinMicrophone: () => AudioManager.joinMicrophone(), joinMicrophone: () => AudioManager.joinMicrophone(),
joinEchoTest: () => AudioManager.joinEchoTest(), joinEchoTest: () => AudioManager.joinEchoTest(),
toggleMuteMicrophone: debounce({ delay: 500 }, toggleMuteMicrophone), toggleMuteMicrophone: debounce(toggleMuteMicrophone, 500, { leading: true, trailing: false }),
changeInputDevice: (inputDeviceId) => AudioManager.changeInputDevice(inputDeviceId), changeInputDevice: (inputDeviceId) => AudioManager.changeInputDevice(inputDeviceId),
changeInputStream: (newInputStream) => { AudioManager.inputStream = newInputStream; }, changeInputStream: (newInputStream) => { AudioManager.inputStream = newInputStream; },
liveChangeInputDevice: (inputDeviceId) => AudioManager.liveChangeInputDevice(inputDeviceId), liveChangeInputDevice: (inputDeviceId) => AudioManager.liveChangeInputDevice(inputDeviceId),

View File

@ -3,8 +3,8 @@ import { layoutSelect } from '/imports/ui/components/layout/context';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { isChatEnabled } from '/imports/ui/services/features'; import { isChatEnabled } from '/imports/ui/services/features';
import ClickOutside from '/imports/ui/components/click-outside/component'; import ClickOutside from '/imports/ui/components/click-outside/component';
import TextareaAutosize from 'react-autosize-textarea';
import Styled from './styles'; import Styled from './styles';
import { escapeHtml } from '/imports/utils/string-utils';
import { checkText } from 'smile2emoji'; import { checkText } from 'smile2emoji';
import deviceInfo from '/imports/utils/deviceInfo'; import deviceInfo from '/imports/utils/deviceInfo';
import { usePreviousValue } from '/imports/ui/components/utils/hooks'; 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 { useMeeting } from '/imports/ui/core/hooks/useMeeting';
import Events from '/imports/ui/core/events/events'; import Events from '/imports/ui/core/events/events';
import ChatOfflineIndicator from './chat-offline-indicator/component'; import ChatOfflineIndicator from './chat-offline-indicator/component';
import TextareaAutosize from 'react-autosize-textarea';
interface ChatMessageFormProps { interface ChatMessageFormProps {
minMessageLength: number, minMessageLength: number,
@ -31,7 +30,9 @@ interface ChatMessageFormProps {
locked: boolean, locked: boolean,
partnerIsLoggedOut: boolean, partnerIsLoggedOut: boolean,
title: string, title: string,
handleClickOutside: Function, handleEmojiSelect: (emojiObject: () => void,
{ native: string }) => void;
handleClickOutside: () => void,
} }
const messages = defineMessages({ const messages = defineMessages({
@ -187,7 +188,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
return; return;
} }
handleSendMessage(escapeHtml(msg), chatId); handleSendMessage(msg, chatId);
setMessage(''); setMessage('');
updateUnreadMessages(chatId, ''); updateUnreadMessages(chatId, '');
setHasErrors(false); setHasErrors(false);
@ -195,11 +196,11 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
if (ENABLE_TYPING_INDICATOR) stopUserTyping(); if (ENABLE_TYPING_INDICATOR) stopUserTyping();
const sentMessageEvent = new CustomEvent(Events.SENT_MESSAGE); const sentMessageEvent = new CustomEvent(Events.SENT_MESSAGE);
window.dispatchEvent(sentMessageEvent); window.dispatchEvent(sentMessageEvent);
} };
const handleEmojiSelect = (emojiObject: { native: string }) => { const handleEmojiSelect = (emojiObject: { native: string }): void => {
const txtArea = textAreaRef?.current?.textarea; const txtArea = textAreaRef?.current?.textarea;
if(!txtArea) return; if (!txtArea) return;
const cursor = txtArea.selectionStart; const cursor = txtArea.selectionStart;
setMessage( setMessage(
@ -212,7 +213,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
setTimeout(() => txtArea.setSelectionRange(newCursor, newCursor), 10); setTimeout(() => txtArea.setSelectionRange(newCursor, newCursor), 10);
} }
const handleMessageChange = (e: ChangeEvent<HTMLInputElement>) => { const handleMessageChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
let newMessage = null; let newMessage = null;
let newError = null; let newError = null;
if (AUTO_CONVERT_EMOJI) { if (AUTO_CONVERT_EMOJI) {
@ -229,17 +230,17 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
newMessage = newMessage.substring(0, maxMessageLength); newMessage = newMessage.substring(0, maxMessageLength);
} }
const handleUserTyping = (hasError?: boolean) => {
if (hasError || !ENABLE_TYPING_INDICATOR) return;
startUserTyping(chatId);
};
setMessage(newMessage); setMessage(newMessage);
setError(newError); setError(newError);
handleUserTyping(newError!=null) handleUserTyping(newError != null);
} };
const handleUserTyping = (hasError?: boolean) => { const handleMessageKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (hasError || !ENABLE_TYPING_INDICATOR) return;
startUserTyping(chatId);
}
const handleMessageKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// TODO Prevent send message pressing enter on mobile and/or virtual keyboard // TODO Prevent send message pressing enter on mobile and/or virtual keyboard
if (e.keyCode === 13 && !e.shiftKey) { if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
@ -251,10 +252,10 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
handleSubmit(event); handleSubmit(event);
} }
} };
const renderForm = () => { const renderForm = () => {
const formRef = useRef(); const formRef = useRef<HTMLFormElement | null >(null);
return ( return (
<Styled.Form <Styled.Form
@ -362,16 +363,13 @@ const ChatMessageFormContainer: React.FC = ({
? intl.formatMessage(messages.titlePrivate, { 0: chat?.participant?.name }) ? intl.formatMessage(messages.titlePrivate, { 0: chat?.participant?.name })
: intl.formatMessage(messages.titlePublic); : intl.formatMessage(messages.titlePublic);
const meeting = useMeeting((m) => ({
const meeting = useMeeting((m) => { lockSettings: {
return { hasActiveLockSetting: m?.lockSettings?.hasActiveLockSetting,
lockSettings: { disablePublicChat: m?.lockSettings?.disablePublicChat,
hasActiveLockSetting: m?.lockSettings?.hasActiveLockSetting, disablePrivateChat: m?.lockSettings?.disablePrivateChat,
disablePublicChat: m?.lockSettings?.disablePublicChat, },
disablePrivateChat: m?.lockSettings?.disablePrivateChat, }));
}
};
});
const locked = chat?.public const locked = chat?.public
? meeting?.lockSettings?.disablePublicChat ? meeting?.lockSettings?.disablePublicChat
@ -387,7 +385,8 @@ const ChatMessageFormContainer: React.FC = ({
return <ChatOfflineIndicator participantName={chat.participant.name} />; return <ChatOfflineIndicator participantName={chat.participant.name} />;
} }
return <ChatMessageForm return (
<ChatMessageForm
{...{ {...{
minMessageLength: CHAT_CONFIG.min_message_length, minMessageLength: CHAT_CONFIG.min_message_length,
maxMessageLength: CHAT_CONFIG.max_message_length, maxMessageLength: CHAT_CONFIG.max_message_length,
@ -401,7 +400,8 @@ const ChatMessageFormContainer: React.FC = ({
partnerIsLoggedOut: chat?.participant ? !chat?.participant?.isOnline : false, partnerIsLoggedOut: chat?.participant ? !chat?.participant?.isOnline : false,
locked: locked ?? false, locked: locked ?? false,
}} }}
/>; />
);
}; };
export default ChatMessageFormContainer; export default ChatMessageFormContainer;

View File

@ -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 { Meteor } from "meteor/meteor";
import { makeVar, useMutation } from "@apollo/client"; import { makeVar, useMutation } from "@apollo/client";
import { LAST_SEEN_MUTATION } from "./queries"; import { LAST_SEEN_MUTATION } from "./queries";
@ -6,6 +6,7 @@ import {
ButtonLoadMore, ButtonLoadMore,
MessageList, MessageList,
MessageListWrapper, MessageListWrapper,
UnreadButton,
} from "./styles"; } from "./styles";
import { layoutSelect } from "../../../layout/context"; import { layoutSelect } from "../../../layout/context";
import ChatListPage from "./page/component"; import ChatListPage from "./page/component";
@ -30,6 +31,10 @@ const intlMessages = defineMessages({
id: 'app.chat.loadMoreButtonLabel', id: 'app.chat.loadMoreButtonLabel',
description: 'Label for load more button', 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 { interface ChatListProps {
@ -97,15 +102,18 @@ const ChatMessageList: React.FC<ChatListProps> = ({
chatId, chatId,
setMessageAsSeenMutation, setMessageAsSeenMutation,
lastSeenAt, lastSeenAt,
totalUnread,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const messageListRef = React.useRef<HTMLDivElement>(); const messageListRef = React.useRef<HTMLDivElement>();
const contentRef = 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 // 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 lastSenderPerPage = React.useRef<Map<number, string>>(new Map());
const messagesEndRef = React.useRef<HTMLDivElement>();
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null); const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
const [lastMessageCreatedTime, setLastMessageCreatedTime] = useState<number>(0); const [lastMessageCreatedTime, setLastMessageCreatedTime] = useState<number>(0);
const [followingTail, setFollowingTail] = React.useState(true); const [followingTail, setFollowingTail] = React.useState(true);
useEffect(() => { useEffect(() => {
setter({ setter({
...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(() => { useEffect(() => {
const setScrollToTailEventHandler = () => { const setScrollToTailEventHandler = () => {
if (scrollObserver && contentRef.current) { if (scrollObserver && contentRef.current) {
@ -209,66 +235,70 @@ const ChatMessageList: React.FC<ChatListProps> = ({
? userLoadedBackUntilPage : Math.max(totalPages - 2, 0); ? userLoadedBackUntilPage : Math.max(totalPages - 2, 0);
const pagesToLoad = (totalPages - firstPageToLoad) || 1; const pagesToLoad = (totalPages - firstPageToLoad) || 1;
return ( return (
<MessageListWrapper> [
<MessageList <MessageListWrapper>
ref={messageListRef} <MessageList
onWheel={(e) => { ref={messageListRef}
if (e.deltaY < 0) { onWheel={(e) => {
if (isElement(contentRef.current) && followingTail) { if (e.deltaY < 0) {
toggleFollowingTail(false) 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); setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
} }}
}} onTouchEnd={() => {
onMouseUp={() => { setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement); }}
}} >
onTouchEnd={() => { <span>
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement); {
}} (userLoadedBackUntilPage)
> ? (
<span> <ButtonLoadMore
{ onClick={() => {
(userLoadedBackUntilPage) if (followingTail) {
? ( toggleFollowingTail(false);
<ButtonLoadMore }
onClick={() => { setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
if (followingTail) {
toggleFollowingTail(false);
} }
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1); }
} >
} {intl.formatMessage(intlMessages.loadMoreButtonLabel)}
> </ButtonLoadMore>
{intl.formatMessage(intlMessages.loadMoreButtonLabel)} ) : null
</ButtonLoadMore> }
) : null </span>
} <div id="contentRef" ref={contentRef}>
</span> <ChatPopupContainer />
<div id="contentRef" ref={contentRef}> {
<ChatPopupContainer /> // @ts-ignore
{ Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
// @ts-ignore return (
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => { <ChatListPage
return ( key={`page-${page}`}
<ChatListPage page={page}
key={`page-${page}`} pageSize={PAGE_SIZE}
page={page} setLastSender={setLastSender(lastSenderPerPage.current)}
pageSize={PAGE_SIZE} lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
setLastSender={setLastSender(lastSenderPerPage.current)} chatId={chatId}
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined} markMessageAsSeen={markMessageAsSeen}
chatId={chatId} scrollRef={messageListRef}
markMessageAsSeen={markMessageAsSeen} lastSeenAt={lastSeenAt}
scrollRef={messageListRef} />
lastSeenAt={lastSeenAt} )
/> })
) }
}) </div>
} <div ref={messagesEndRef} />
</div> </MessageList>
</MessageList> </MessageListWrapper >,
</MessageListWrapper > renderUnreadNotification,
]
); );
} }

View File

@ -3,6 +3,7 @@ import { Message } from '/imports/ui/Types/message';
import { import {
ChatWrapper, ChatWrapper,
ChatContent, ChatContent,
ChatAvatar,
} from "./styles"; } from "./styles";
import ChatMessageHeader from "./message-header/component"; import ChatMessageHeader from "./message-header/component";
import ChatMessageTextContent from "./message-content/text-content/component"; import ChatMessageTextContent from "./message-content/text-content/component";
@ -148,16 +149,24 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
sameSender={sameSender} sameSender={sameSender}
ref={messageRef} 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 <ChatMessageHeader
sameSender={message?.user ? sameSender : false} sameSender={message?.user ? sameSender : false}
name={messageContent.name} name={messageContent.name}
color={messageContent.color}
isModerator={messageContent.isModerator}
isOnline={message.user?.isOnline ?? true} isOnline={message.user?.isOnline ?? true}
avatar={message.user?.avatar}
dateTime={dateTime} dateTime={dateTime}
/> />
<ChatContent>
{ {
messageContent.component messageContent.component
} }

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import ReactMarkdown from 'react-markdown';
import Styled from './styles'; import Styled from './styles';
interface ChatMessageTextContentProps { interface ChatMessageTextContentProps {
text: string; text: string;
@ -9,9 +10,18 @@ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
text, text,
emphasizedMessage, emphasizedMessage,
}) => { }) => {
// @ts-ignore - temporary, while meteor exists in the project
const { allowedElements } = Meteor.settings.public.chat;
return ( return (
<Styled.ChatMessage emphasizedMessage={emphasizedMessage}> <Styled.ChatMessage emphasizedMessage={emphasizedMessage}>
{text} <ReactMarkdown
linkTarget="_blank"
allowedElements={allowedElements}
unwrapDisallowed={true}
>
{text}
</ReactMarkdown>
</Styled.ChatMessage> </Styled.ChatMessage>
); );
}; };

View File

@ -5,14 +5,23 @@ export const ChatMessage = styled.div`
flex: 1; flex: 1;
display: flex; display: flex;
flex-flow: row; flex-flow: row;
flex-direction: column;
color: ${colorText}; color: ${colorText};
word-break: break-word; word-break: break-word;
margin-left: 2.75rem;
${({ emphasizedMessage }) => ${({ emphasizedMessage }) =>
emphasizedMessage && emphasizedMessage &&
` `
font-weight: bold; font-weight: bold;
`} `}
& img {
max-width: 100%;
max-height: 100%;
}
& p {
margin: 0;
}
`; `;
export default { export default {

View File

@ -13,9 +13,6 @@ const intlMessages = defineMessages({
interface ChatMessageHeaderProps { interface ChatMessageHeaderProps {
name: string; name: string;
avatar: string;
color: string;
isModerator: boolean;
isOnline: boolean; isOnline: boolean;
dateTime: Date; dateTime: Date;
sameSender: boolean; sameSender: boolean;
@ -24,9 +21,6 @@ interface ChatMessageHeaderProps {
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
sameSender, sameSender,
name, name,
color,
isModerator,
avatar,
isOnline, isOnline,
dateTime, dateTime,
}) => { }) => {
@ -35,15 +29,8 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
return ( return (
<Styled.HeaderContent> <Styled.HeaderContent>
<Styled.ChatAvatar
avatar={avatar}
color={color}
moderator={isModerator}
>
{name.toLowerCase().slice(0, 2) || " "}
</Styled.ChatAvatar>
<Styled.ChatHeaderText> <Styled.ChatHeaderText>
<Styled.ChatUserName> <Styled.ChatUserName isOnline={isOnline}>
{name} {name}
</Styled.ChatUserName> </Styled.ChatUserName>
{ {

View File

@ -1,25 +1,23 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
import { import {
userIndicatorsOffset,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorWhite,
userListBg,
colorSuccess,
colorHeading, colorHeading,
palettePlaceholderText, palettePlaceholderText,
colorGrayLight, colorGrayLight,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography'; import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
interface ChatUserNameProps {
isOnline: boolean;
}
export const HeaderContent = styled.div` export const HeaderContent = styled.div`
display: flex; display: flex;
flex-flow: row; flex-flow: row;
width: 100%; width: 100%;
`; `;
export const ChatUserName = styled.div` export const ChatUserName = styled.div<ChatUserNameProps>`
display: flex; display: flex;
min-width: 0; min-width: 0;
font-weight: 600; 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` export const ChatHeaderText = styled.div`
display: flex; display: flex;
align-items: baseline; align-items: baseline;
@ -176,7 +89,6 @@ export const ChatHeaderText = styled.div`
export default { export default {
HeaderContent, HeaderContent,
ChatAvatar,
ChatTime, ChatTime,
ChatUserOffline, ChatUserOffline,
ChatUserName, ChatUserName,

View File

@ -1,20 +1,36 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
borderSize, borderSize,
userIndicatorsOffset,
} from '/imports/ui/stylesheets/styled-components/general'; } from '/imports/ui/stylesheets/styled-components/general';
import { import {
lineHeightComputed, lineHeightComputed,
fontSizeBase, fontSizeBase,
} from '/imports/ui/stylesheets/styled-components/typography'; } from '/imports/ui/stylesheets/styled-components/typography';
export const ChatWrapper = styled.div` import {
colorWhite,
userListBg,
colorSuccess,
} from '/imports/ui/stylesheets/styled-components/palette';
interface ChatWrapperProps {
sameSender: boolean;
}
interface ChatContentProps {
sameSender: boolean;
}
export const ChatWrapper = styled.div<ChatWrapperProps>`
pointer-events: auto; pointer-events: auto;
[dir='rtl'] & { [dir='rtl'] & {
direction: rtl; direction: rtl;
} }
display: flex; display: flex;
flex-flow: column; flex-flow: row;
position: relative; position: relative;
${({ sameSender }) => ${({ sameSender }) =>
sameSender && sameSender &&
@ -34,8 +50,100 @@ export const ChatWrapper = styled.div`
font-size: ${fontSizeBase}; font-size: ${fontSizeBase};
`; `;
export const ChatContent = styled.div` export const ChatContent = styled.div<ChatContentProps>`
display: flex; display: flex;
flex-flow: column; flex-flow: column;
width: 100%; width: 100%;
${({ sameSender }) =>
sameSender &&
`
margin-left: 2.6rem;
`}
`;
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;
}
`; `;

Some files were not shown because too many files have changed in this diff Show More