Merge branch 'develop' of github.com:bigbluebutton/bigbluebutton into akka-pekko-migration
This commit is contained in:
commit
225b2f2d74
55
.github/workflows/automated-tests-build-package-job.yml
vendored
Normal file
55
.github/workflows/automated-tests-build-package-job.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
name: Build service and cache
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
build-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
build-list:
|
||||||
|
type: string
|
||||||
|
cache-files-list:
|
||||||
|
type: string
|
||||||
|
cache-urls-list:
|
||||||
|
type: string
|
||||||
|
jobs:
|
||||||
|
b:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.base.ref || '' }}
|
||||||
|
fetch-depth: 0 # Fetch all history
|
||||||
|
- name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }}
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
git config user.name "BBB Automated Tests"
|
||||||
|
git config user.email "tests@bigbluebutton.org"
|
||||||
|
git config pull.rebase false
|
||||||
|
git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }}
|
||||||
|
- name: Set cache-key vars
|
||||||
|
run: |
|
||||||
|
echo "CACHE_KEY_FILES=$(echo '${{ inputs.cache-files-list }} .gitlab-ci.yml build/deb-helper.sh' | xargs -n1 git log -1 --format=%h -- | tr '\n' '-' | sed 's/-$//')" >> $GITHUB_ENV
|
||||||
|
echo "CACHE_KEY_URLS=$(echo '${{ inputs.cache-urls-list }}' | xargs -r -n 1 curl -Is | grep -i 'Last-Modified' | md5sum | cut -c1-10)" >> $GITHUB_ENV
|
||||||
|
cat bigbluebutton-config/bigbluebutton-release >> $GITHUB_ENV
|
||||||
|
echo "RUNNER_OS=$(lsb_release -d | cut -f2 | tr -d ' ')" >> $GITHUB_ENV
|
||||||
|
echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
|
||||||
|
echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
|
||||||
|
- name: Handle cache
|
||||||
|
if: inputs.cache-files-list != ''
|
||||||
|
id: cache-action
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: artifacts.tar
|
||||||
|
key: ${{ env.RUNNER_OS }}-${{ inputs.build-name }}-${{ env.BIGBLUEBUTTON_RELEASE }}-commits-${{ env.CACHE_KEY_FILES }}-urls-${{ env.CACHE_KEY_URLS }}
|
||||||
|
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
|
||||||
|
name: Generate artifacts
|
||||||
|
run: |
|
||||||
|
./build/get_external_dependencies.sh
|
||||||
|
echo "${{ inputs.build-list || inputs.build-name }}" | xargs -n 1 ./build/setup.sh
|
||||||
|
tar cvf artifacts.tar artifacts/
|
||||||
|
- name: Archive packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifacts_${{ inputs.build-name }}.tar
|
||||||
|
path: artifacts.tar
|
379
.github/workflows/automated-tests.yml
vendored
379
.github/workflows/automated-tests.yml
vendored
@ -15,318 +15,96 @@ on:
|
|||||||
- '**/*.md'
|
- '**/*.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
|
||||||
|
88
.github/workflows/publish-test-report.yml
vendored
Normal file
88
.github/workflows/publish-test-report.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
name: Publish Test Results
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- Automated tests
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-pr-data:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||||
|
outputs:
|
||||||
|
pr-number: ${{ steps.set-env.outputs.pr-number }}
|
||||||
|
workflow-id: ${{ steps.set-env.outputs.workflow-id }}
|
||||||
|
steps:
|
||||||
|
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "pr-comment-data"
|
||||||
|
})[0];
|
||||||
|
let download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
let fs = require('fs');
|
||||||
|
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr-comment-data.zip`, Buffer.from(download.data));
|
||||||
|
- name: Unzip artifact
|
||||||
|
run: unzip pr-comment-data.zip
|
||||||
|
- name: Set env variables
|
||||||
|
id: set-env
|
||||||
|
run: |
|
||||||
|
echo "pr-number=$(cat ./pr_number)" >> $GITHUB_OUTPUT
|
||||||
|
echo "workflow-id=$(cat ./workflow_id)" >> $GITHUB_OUTPUT
|
||||||
|
comment-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
needs: get-pr-data
|
||||||
|
steps:
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v2
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||||
|
comment-author: "github-actions[bot]"
|
||||||
|
body-includes: Automated tests Summary
|
||||||
|
- name: Remove previous comment
|
||||||
|
if: steps.fc.outputs.comment-id != ''
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.deleteComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
comment_id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
})
|
||||||
|
- name: Passing tests comment
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
uses: peter-evans/create-or-update-comment@v2
|
||||||
|
with:
|
||||||
|
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||||
|
body: |
|
||||||
|
<h1>Automated tests Summary</h1>
|
||||||
|
<h3><strong>:white_check_mark:</strong> All the CI tests have passed!</h3>
|
||||||
|
- name: Failing tests comment
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
|
||||||
|
uses: peter-evans/create-or-update-comment@v2
|
||||||
|
with:
|
||||||
|
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
|
||||||
|
body: |
|
||||||
|
<h1> Automated tests Summary</h1>
|
||||||
|
<h3><strong>:rotating_light:</strong> Test workflow has failed</h3>
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ needs.get-pr-data.outputs.workflow-id }}) to check the action test reports
|
@ -41,6 +41,7 @@ trait SystemConfiguration {
|
|||||||
lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43)
|
lazy val 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)
|
||||||
|
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 _ =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
package org.bigbluebutton.core.apps.presentationpod
|
|
||||||
|
|
||||||
import org.bigbluebutton.common2.msgs._
|
|
||||||
import org.bigbluebutton.core.apps.RightsManagementTrait
|
|
||||||
import org.bigbluebutton.core.bus.MessageBus
|
|
||||||
import org.bigbluebutton.core.db.PresPageDAO
|
|
||||||
import org.bigbluebutton.core.domain.MeetingState2x
|
|
||||||
import org.bigbluebutton.core.running.LiveMeeting
|
|
||||||
|
|
||||||
trait AddSlidePositionsPubMsgHdlr extends RightsManagementTrait {
|
|
||||||
|
|
||||||
this: PresentationPodHdlrs =>
|
|
||||||
|
|
||||||
def handle(msg: AddSlidePositionsPubMsg, state: MeetingState2x,
|
|
||||||
liveMeeting: LiveMeeting, bus: MessageBus) = {
|
|
||||||
PresPageDAO.addSlidePosition(msg.body.slideId, msg.body.width, msg.body.height,
|
|
||||||
msg.body.viewBoxWidth, msg.body.viewBoxHeight)
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
@ -166,7 +166,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
|
|||||||
|
|
||||||
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
|
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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.bigbluebutton.core.apps.voice
|
||||||
|
|
||||||
|
import org.bigbluebutton.common2.msgs._
|
||||||
|
import org.bigbluebutton.core.running.{ MeetingActor, LiveMeeting, OutMsgRouter }
|
||||||
|
|
||||||
|
trait ChannelHoldChangedVoiceConfEvtMsgHdlr {
|
||||||
|
this: MeetingActor =>
|
||||||
|
|
||||||
|
val liveMeeting: LiveMeeting
|
||||||
|
val outGW: OutMsgRouter
|
||||||
|
|
||||||
|
def handleChannelHoldChangedVoiceConfEvtMsg(msg: ChannelHoldChangedVoiceConfEvtMsg): Unit = {
|
||||||
|
VoiceApp.handleChannelHoldChanged(
|
||||||
|
liveMeeting,
|
||||||
|
outGW,
|
||||||
|
msg.body.intId,
|
||||||
|
msg.body.uuid,
|
||||||
|
msg.body.hold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package org.bigbluebutton.core.apps.voice
|
||||||
|
|
||||||
|
import org.bigbluebutton.common2.msgs._
|
||||||
|
import org.bigbluebutton.core.models.VoiceUsers
|
||||||
|
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
|
||||||
|
|
||||||
|
trait ListenOnlyModeToggledInSfuEvtMsgHdlr {
|
||||||
|
this: BaseMeetingActor =>
|
||||||
|
|
||||||
|
val liveMeeting: LiveMeeting
|
||||||
|
val outGW: OutMsgRouter
|
||||||
|
|
||||||
|
def handleListenOnlyModeToggledInSfuEvtMsg(msg: ListenOnlyModeToggledInSfuEvtMsg): Unit = {
|
||||||
|
for {
|
||||||
|
vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
|
||||||
|
} yield {
|
||||||
|
VoiceApp.holdChannelInVoiceConf(
|
||||||
|
liveMeeting,
|
||||||
|
outGW,
|
||||||
|
vu.uuid,
|
||||||
|
msg.body.enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -93,7 +93,9 @@ trait UserJoinedVoiceConfEvtMsgHdlr extends SystemConfiguration {
|
|||||||
userColor,
|
userColor,
|
||||||
msg.body.muted,
|
msg.body.muted,
|
||||||
msg.body.talking,
|
msg.body.talking,
|
||||||
"freeswitch"
|
"freeswitch",
|
||||||
|
msg.body.hold,
|
||||||
|
msg.body.uuid
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 _ =>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 =>
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package org.bigbluebutton.core.db
|
||||||
|
import org.bigbluebutton.core.models.PadGroup
|
||||||
|
import slick.jdbc.PostgresProfile.api._
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import scala.util.{ Failure, Success }
|
||||||
|
|
||||||
|
case class SharedNotesDbModel(
|
||||||
|
meetingId: String,
|
||||||
|
sharedNotesExtId: String,
|
||||||
|
padId: String,
|
||||||
|
model: String,
|
||||||
|
name: String,
|
||||||
|
pinned: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
class SharedNotesDbTableDef(tag: Tag) extends Table[SharedNotesDbModel](tag, None, "sharedNotes") {
|
||||||
|
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||||
|
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||||
|
val padId = column[String]("padId")
|
||||||
|
val model = column[String]("model")
|
||||||
|
val name = column[String]("name")
|
||||||
|
val pinned = column[Boolean]("pinned")
|
||||||
|
val * = (
|
||||||
|
meetingId, sharedNotesExtId, padId, model, name, pinned
|
||||||
|
) <> (SharedNotesDbModel.tupled, SharedNotesDbModel.unapply)
|
||||||
|
}
|
||||||
|
|
||||||
|
object SharedNotesDAO {
|
||||||
|
def insert(meetingId: String, group: PadGroup, padId: String, name: String) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesDbTableDef].insertOrUpdate(
|
||||||
|
SharedNotesDbModel(
|
||||||
|
meetingId = meetingId,
|
||||||
|
sharedNotesExtId = group.externalId,
|
||||||
|
padId = padId,
|
||||||
|
model = group.model,
|
||||||
|
name = name,
|
||||||
|
pinned = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotes table!")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotes: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updatePinned(meetingId: String, sharedNotesExtId: String, pinned: Boolean) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesDbTableDef]
|
||||||
|
.filter(_.meetingId === meetingId)
|
||||||
|
.filter(_.sharedNotesExtId === sharedNotesExtId)
|
||||||
|
.map(n => n.pinned)
|
||||||
|
.update(pinned)
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated pinned on SharedNotes table!")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.error(s"Error updating pinned SharedNotes: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package org.bigbluebutton.core.db
|
||||||
|
|
||||||
|
import slick.jdbc.PostgresProfile.api._
|
||||||
|
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import scala.util.{ Failure, Success }
|
||||||
|
|
||||||
|
case class SharedNotesRevDbModel(
|
||||||
|
meetingId: String,
|
||||||
|
sharedNotesExtId: String,
|
||||||
|
rev: Int,
|
||||||
|
userId: String,
|
||||||
|
changeset: String,
|
||||||
|
start: Option[Int],
|
||||||
|
end: Option[Int],
|
||||||
|
diff: Option[String],
|
||||||
|
createdAt: java.sql.Timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
class SharedNotesRevDbTableDef(tag: Tag) extends Table[SharedNotesRevDbModel](tag, None, "sharedNotes_rev") {
|
||||||
|
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||||
|
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||||
|
val rev = column[Int]("rev", O.PrimaryKey)
|
||||||
|
val userId = column[String]("userId")
|
||||||
|
val changeset = column[String]("changeset")
|
||||||
|
val start = column[Option[Int]]("start")
|
||||||
|
val end = column[Option[Int]]("end")
|
||||||
|
val diff = column[Option[String]]("diff")
|
||||||
|
val createdAt = column[java.sql.Timestamp]("createdAt")
|
||||||
|
val * = (meetingId, sharedNotesExtId, rev, userId, changeset, start, end, diff, createdAt) <> (SharedNotesRevDbModel.tupled, SharedNotesRevDbModel.unapply)
|
||||||
|
}
|
||||||
|
|
||||||
|
object SharedNotesRevDAO {
|
||||||
|
def insert(meetingId: String, sharedNotesExtId: String, revId: Int, userId: String, changeset: String) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesRevDbTableDef].insertOrUpdate(
|
||||||
|
SharedNotesRevDbModel(
|
||||||
|
meetingId = meetingId,
|
||||||
|
sharedNotesExtId = sharedNotesExtId,
|
||||||
|
rev = revId,
|
||||||
|
userId = userId,
|
||||||
|
changeset = changeset,
|
||||||
|
start = None,
|
||||||
|
end = None,
|
||||||
|
diff = None,
|
||||||
|
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesRev table!")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesRev: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(meetingId: String, sharedNotesExtId: String, revId: Int, start: Int, end: Int, text: String) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesRevDbTableDef]
|
||||||
|
.filter(_.meetingId === meetingId)
|
||||||
|
.filter(_.sharedNotesExtId === sharedNotesExtId)
|
||||||
|
.filter(_.rev === revId)
|
||||||
|
.map(n => (n.start, n.end, n.diff))
|
||||||
|
.update((Some(start), Some(end), Some(text)))
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated Rev on SharedNotes table!")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.error(s"Error updating Rev SharedNotes: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package org.bigbluebutton.core.db
|
||||||
|
import slick.jdbc.PostgresProfile.api._
|
||||||
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
import scala.util.{ Failure, Success }
|
||||||
|
|
||||||
|
case class SharedNotesSessionDbModel(
|
||||||
|
meetingId: String,
|
||||||
|
sharedNotesExtId: String,
|
||||||
|
userId: String,
|
||||||
|
sessionId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
class SharedNotesSessionDbTableDef(tag: Tag) extends Table[SharedNotesSessionDbModel](tag, None, "sharedNotes_session") {
|
||||||
|
val meetingId = column[String]("meetingId", O.PrimaryKey)
|
||||||
|
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
|
||||||
|
val userId = column[String]("userId", O.PrimaryKey)
|
||||||
|
val sessionId = column[String]("sessionId")
|
||||||
|
val * = (meetingId, sharedNotesExtId, userId, sessionId) <> (SharedNotesSessionDbModel.tupled, SharedNotesSessionDbModel.unapply)
|
||||||
|
}
|
||||||
|
|
||||||
|
object SharedNotesSessionDAO {
|
||||||
|
def insert(meetingId: String, sharedNotesExtId: String, userId: String, sessionId: String) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesSessionDbTableDef].insertOrUpdate(
|
||||||
|
SharedNotesSessionDbModel(
|
||||||
|
meetingId = meetingId,
|
||||||
|
sharedNotesExtId = sharedNotesExtId,
|
||||||
|
userId = userId,
|
||||||
|
sessionId = sessionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesSession table!")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesSession: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete(intId: String, sessionId: String) = {
|
||||||
|
DatabaseConnection.db.run(
|
||||||
|
TableQuery[SharedNotesSessionDbTableDef]
|
||||||
|
.filter(_.userId === intId)
|
||||||
|
.filter(_.sessionId === sessionId)
|
||||||
|
.delete
|
||||||
|
).onComplete {
|
||||||
|
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"SharedNotesSession ${sessionId} deleted")
|
||||||
|
case Failure(e) => DatabaseConnection.logger.error(s"Error deleting SharedNotesSession ${sessionId}: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,9 @@ case class PresentationPage(
|
|||||||
xOffset: Double = 0,
|
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 {
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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 =>
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||||
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.bigbluebutton.freeswitch.voice.events;
|
||||||
|
|
||||||
|
public class ChannelHoldChangedEvent extends VoiceConferenceEvent {
|
||||||
|
|
||||||
|
private final String userId;
|
||||||
|
private final String uuid;
|
||||||
|
private final boolean hold;
|
||||||
|
|
||||||
|
public ChannelHoldChangedEvent(
|
||||||
|
String room,
|
||||||
|
String userId,
|
||||||
|
String uuid,
|
||||||
|
boolean hold
|
||||||
|
) {
|
||||||
|
super(room);
|
||||||
|
this.userId = userId;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.hold = hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUUID() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHeld() {
|
||||||
|
return hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,6 +9,8 @@ public class ConfMember {
|
|||||||
public final Boolean locked = false;
|
public final 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
|
@ -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())) {
|
||||||
|
|
||||||
|
@ -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())) {
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free Software
|
||||||
|
* Foundation; either version 3.0 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License along
|
||||||
|
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.bigbluebutton.freeswitch.voice.freeswitch.actions;
|
||||||
|
|
||||||
|
public class HoldChannelCommand extends FreeswitchCommand {
|
||||||
|
private final String uuid;
|
||||||
|
private final Boolean hold;
|
||||||
|
|
||||||
|
public HoldChannelCommand(String room, String uuid, Boolean hold, String requesterId) {
|
||||||
|
super(room, requesterId);
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.hold = hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCommand() {
|
||||||
|
return "uuid_hold";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCommandArgs() {
|
||||||
|
if (hold) {
|
||||||
|
return "toggle" + SPACE + uuid;
|
||||||
|
} else {
|
||||||
|
return "off" + SPACE + uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,10 @@ public class ConferenceMember {
|
|||||||
return flags.getIsSpeaking();
|
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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -243,4 +243,21 @@ trait RxJsonMsgDeserializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def routeHoldChannelInVoiceConfMsg(envelope: BbbCoreEnvelope, jsonNode: JsonNode): Unit = {
|
||||||
|
def deserialize(jsonNode: JsonNode): Option[HoldChannelInVoiceConfSysMsg] = {
|
||||||
|
val (result, error) = JsonDeserializer.toBbbCommonMsg[HoldChannelInVoiceConfSysMsg](jsonNode)
|
||||||
|
result match {
|
||||||
|
case Some(msg) => Some(msg.asInstanceOf[HoldChannelInVoiceConfSysMsg])
|
||||||
|
case None =>
|
||||||
|
log.error("Failed to deserialize message: error: {} \n msg: {}", error, jsonNode)
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
m <- deserialize(jsonNode)
|
||||||
|
} yield {
|
||||||
|
fsApp.holdChannel(m.body.voiceConf, m.body.uuid, m.body.hold)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,8 @@ class RxJsonMsgHdlrActor(val fsApp: FreeswitchApplication) extends Actor with Ac
|
|||||||
routeCheckRunningAndRecordingToVoiceConfSysMsg(envelope, jsonNode)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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],
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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);
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
table:
|
||||||
|
name: v_sharedNotes
|
||||||
|
schema: public
|
||||||
|
configuration:
|
||||||
|
column_config: {}
|
||||||
|
custom_column_names: {}
|
||||||
|
custom_name: sharedNotes
|
||||||
|
custom_root_fields: {}
|
||||||
|
select_permissions:
|
||||||
|
- role: bbb_client
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- lastRev
|
||||||
|
- model
|
||||||
|
- name
|
||||||
|
- padId
|
||||||
|
- pinned
|
||||||
|
- sharedNotesExtId
|
||||||
|
filter:
|
||||||
|
meetingId:
|
||||||
|
_eq: X-Hasura-MeetingId
|
@ -0,0 +1,29 @@
|
|||||||
|
table:
|
||||||
|
name: v_sharedNotes_session
|
||||||
|
schema: public
|
||||||
|
configuration:
|
||||||
|
column_config: {}
|
||||||
|
custom_column_names: {}
|
||||||
|
custom_name: sharedNotes_session
|
||||||
|
custom_root_fields: {}
|
||||||
|
object_relationships:
|
||||||
|
- name: sharedNotes
|
||||||
|
using:
|
||||||
|
manual_configuration:
|
||||||
|
column_mapping:
|
||||||
|
meetingId: meetingId
|
||||||
|
sharedNotesExtId: sharedNotesExtId
|
||||||
|
insertion_order: null
|
||||||
|
remote_table:
|
||||||
|
name: v_sharedNotes
|
||||||
|
schema: public
|
||||||
|
select_permissions:
|
||||||
|
- role: bbb_client
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- padId
|
||||||
|
- sessionId
|
||||||
|
- sharedNotesExtId
|
||||||
|
filter:
|
||||||
|
userId:
|
||||||
|
_eq: X-Hasura-UserId
|
@ -25,3 +25,4 @@ select_permissions:
|
|||||||
filter:
|
filter:
|
||||||
meetingId:
|
meetingId:
|
||||||
_eq: X-Hasura-MeetingId
|
_eq: X-Hasura-MeetingId
|
||||||
|
allow_aggregations: true
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -1 +1 @@
|
|||||||
git clone --branch v0.3.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
git clone --branch v0.4.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
|
||||||
|
@ -1 +1 @@
|
|||||||
git clone --branch v2.10.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
git clone --branch v2.11.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
|
||||||
|
@ -366,6 +366,10 @@ start_bigbluebutton () {
|
|||||||
|
|
||||||
echo "Starting BigBlueButton"
|
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
|
||||||
|
@ -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
|
||||||
|
@ -1 +1 @@
|
|||||||
METEOR@2.12
|
METEOR@2.13
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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) => {
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ const notifyExpiredReaction = (meetingId, userId) => {
|
|||||||
check(meetingId, String);
|
check(meetingId, String);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
emoji,
|
|
||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(() => ({
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
};
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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,
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user