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

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

View File

@ -0,0 +1,55 @@
name: Build service and cache
on:
workflow_call:
inputs:
build-name:
required: true
type: string
build-list:
type: string
cache-files-list:
type: string
cache-urls-list:
type: string
jobs:
b:
runs-on: ubuntu-22.04
steps:
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.base.ref || '' }}
fetch-depth: 0 # Fetch all history
- name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }}
if: github.event_name == 'pull_request'
run: |
git config user.name "BBB Automated Tests"
git config user.email "tests@bigbluebutton.org"
git config pull.rebase false
git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }}
- name: Set cache-key vars
run: |
echo "CACHE_KEY_FILES=$(echo '${{ inputs.cache-files-list }} .gitlab-ci.yml build/deb-helper.sh' | xargs -n1 git log -1 --format=%h -- | tr '\n' '-' | sed 's/-$//')" >> $GITHUB_ENV
echo "CACHE_KEY_URLS=$(echo '${{ inputs.cache-urls-list }}' | xargs -r -n 1 curl -Is | grep -i 'Last-Modified' | md5sum | cut -c1-10)" >> $GITHUB_ENV
cat bigbluebutton-config/bigbluebutton-release >> $GITHUB_ENV
echo "RUNNER_OS=$(lsb_release -d | cut -f2 | tr -d ' ')" >> $GITHUB_ENV
echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
if: inputs.cache-files-list != ''
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ env.RUNNER_OS }}-${{ inputs.build-name }}-${{ env.BIGBLUEBUTTON_RELEASE }}-commits-${{ env.CACHE_KEY_FILES }}-urls-${{ env.CACHE_KEY_URLS }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
echo "${{ inputs.build-list || inputs.build-name }}" | xargs -n 1 ./build/setup.sh
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_${{ inputs.build-name }}.tar
path: artifacts.tar

View File

@ -15,318 +15,96 @@ on:
- '**/*.md'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-bbb-apps-akka:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_AKKA_APPS_KEY=$(git log -1 --format=%H -- akka-bbb-apps)" >> $GITHUB_ENV
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-apps-akka-${{ env.CACHE_AKKA_APPS_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-apps-akka
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-apps-akka.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-apps-akka
cache-files-list: akka-bbb-apps bbb-common-message
build-bbb-config:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-config
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-config.tar
path: artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-config
cache-files-list: bigbluebutton-config
build-bbb-export-annotations:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-export-annotations
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-export-annotations.tar
path: artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-export-annotations
cache-files-list: bbb-export-annotations
build-bbb-learning-dashboard:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_LEARNING_DASHBOARD_KEY=$(git log -1 --format=%H -- bbb-learning-dashboard)" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-learning-dashboard-${{ env.CACHE_LEARNING_DASHBOARD_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-learning-dashboard
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-learning-dashboard.tar
path: artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-learning-dashboard
cache-files-list: bbb-learning-dashboard
build-bbb-playback-record:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: ./build/get_external_dependencies.sh
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- run: ./build/setup.sh bbb-playback
- run: ./build/setup.sh bbb-playback-notes
- run: ./build/setup.sh bbb-playback-podcast
- run: ./build/setup.sh bbb-playback-presentation
- run: ./build/setup.sh bbb-playback-screenshare
- run: ./build/setup.sh bbb-playback-video
- run: ./build/setup.sh bbb-record-core
- run: tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-playback-record.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-playback-record
build-list: bbb-playback bbb-playback-notes bbb-playback-podcast bbb-playback-presentation bbb-playback-screenshare bbb-playback-video bbb-record-core
build-bbb-graphql-server:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: ./build/get_external_dependencies.sh
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- run: ./build/setup.sh bbb-graphql-middleware
- run: ./build/setup.sh bbb-graphql-server
- run: tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-graphql-server.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-graphql-server
build-list: bbb-graphql-server bbb-graphql-middleware
build-bbb-etherpad:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_ETHERPAD_VERSION_KEY=$(git log -1 --format=%H -- bbb-etherpad.placeholder.sh)" >> $GITHUB_ENV
- run: echo "CACHE_ETHERPAD_BUILD_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-etherpad)" >> $GITHUB_ENV
- run: echo "CACHE_URL1_KEY=$(curl -s https://api.github.com/repos/mconf/ep_pad_ttl/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
- run: echo "CACHE_URL2_KEY=$(curl -s https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
- run: echo "CACHE_URL3_KEY=$(curl -s https://api.github.com/repos/mconf/ep_redis_publisher/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
- run: echo "CACHE_URL4_KEY=$(curl -s https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-etherpad-${{ env.CACHE_ETHERPAD_VERSION_KEY }}-${{ env.CACHE_ETHERPAD_BUILD_KEY }}-${{ env.CACHE_URL1_KEY }}-${{ env.CACHE_URL2_KEY }}-${{ env.CACHE_URL3_KEY }}-${{ env.CACHE_URL4_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-etherpad
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-etherpad.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-etherpad
cache-files-list: bbb-etherpad.placeholder.sh build/packages-template/bbb-etherpad
cache-urls-list: https://api.github.com/repos/mconf/ep_pad_ttl/commits https://api.github.com/repos/alangecker/bbb-etherpad-plugin/commits https://api.github.com/repos/mconf/ep_redis_publisher/commits https://api.github.com/repos/alangecker/bbb-etherpad-skin/commits
build-bbb-bbb-web:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_BBB_WEB_KEY=$(git log -1 --format=%H -- bigbluebutton-web)" >> $GITHUB_ENV
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
- run: echo "CACHE_COMMON_WEB_KEY=$(git log -1 --format=%H -- bbb-common-web)" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-web-${{ env.CACHE_BBB_WEB_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_COMMON_WEB_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-web
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-web.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-web
cache-files-list: bigbluebutton-web bbb-common-message bbb-common-web
build-bbb-fsesl-akka:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_AKKA_FSESL_KEY=$(git log -1 --format=%H -- akka-bbb-fsesl)" >> $GITHUB_ENV
- run: echo "CACHE_COMMON_MSG_KEY=$(git log -1 --format=%H -- bbb-common-message)" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-fsesl-akka-${{ env.CACHE_AKKA_FSESL_KEY }}-${{ env.CACHE_COMMON_MSG_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-fsesl-akka
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-fsesl-akka.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-fsesl-akka
cache-files-list: akka-bbb-fsesl bbb-common-message
build-bbb-html5:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_KEY=$(git log -1 --format=%H -- bigbluebutton-html5)" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-html5-${{ env.CACHE_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-html5-nodejs
./build/setup.sh bbb-html5
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-html5.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-html5
build-list: bbb-html5-nodejs bbb-html5
cache-files-list: bigbluebutton-html5
build-bbb-freeswitch:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history
- run: echo "CACHE_FREESWITCH_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-core)" >> $GITHUB_ENV
- run: echo "CACHE_FREESWITCH_SOUNDS_KEY=$(git log -1 --format=%H -- build/packages-template/bbb-freeswitch-sounds)" >> $GITHUB_ENV
- run: echo "CACHE_SOUNDS_KEY=$(curl -Is http://bigbluebutton.org/downloads/sounds.tar.gz | grep "Last-Modified" | md5sum | awk '{ print $1 }')" >> $GITHUB_ENV
- run: echo "CACHE_BBB_RELEASE_KEY=$(git log -1 --format=%H -- bigbluebutton-config/bigbluebutton-release)" >> $GITHUB_ENV
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- name: Handle cache
id: cache-action
uses: actions/cache@v3
with:
path: artifacts.tar
key: ${{ runner.os }}-bbb-freeswitch-${{ env.CACHE_FREESWITCH_KEY }}-${{ env.CACHE_FREESWITCH_SOUNDS_KEY }}-${{ env.CACHE_SOUNDS_KEY }}-${{ env.CACHE_BBB_RELEASE_KEY }}
- if: ${{ steps.cache-action.outputs.cache-hit != 'true' }}
name: Generate artifacts
run: |
./build/get_external_dependencies.sh
./build/setup.sh bbb-freeswitch-core
./build/setup.sh bbb-freeswitch-sounds
tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts_bbb-freeswitch.tar
path: |
artifacts.tar
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-freeswitch
build-list: bbb-freeswitch-core bbb-freeswitch-sounds
cache-files-list: freeswitch.placeholder.sh build/packages-template/bbb-freeswitch-core build/packages-template/bbb-freeswitch-sounds
cache-urls-list: http://bigbluebutton.org/downloads/sounds.tar.gz
build-bbb-webrtc:
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: bbb-webrtc
build-list: bbb-webrtc-sfu bbb-webrtc-recorder
cache-files-list: bbb-webrtc-sfu.placeholder.sh bbb-webrtc-recorder.placeholder.sh build/packages-template/bbb-webrtc-sfu build/packages-template/bbb-webrtc-recorder
build-others:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- run: ./build/get_external_dependencies.sh
- run: echo "FORCE_GIT_REV=0" >> $GITHUB_ENV #used by setup.sh
- run: echo "FORCE_COMMIT_DATE=0" >> $GITHUB_ENV #used by setup.sh
- run: ./build/setup.sh bbb-mkclean
- run: ./build/setup.sh bbb-pads
- run: ./build/setup.sh bbb-libreoffice-docker
- run: ./build/setup.sh bbb-webrtc-sfu
- run: ./build/setup.sh bbb-webrtc-recorder
- run: ./build/setup.sh bbb-transcription-controller
- run: ./build/setup.sh bigbluebutton
- run: tar cvf artifacts.tar artifacts/
- name: Archive packages
uses: actions/upload-artifact@v3
with:
name: artifacts.tar
path: |
artifacts.tar
# - name: Fake package build
# run: |
# sudo -i <<EOF
# set -e
# echo "Faking a package build (to speed up installation test)"
# cd /
# wget -nv "http://ci.bbb.imdt.dev/artifacts.tar"
# tar xf artifacts.tar
# mv artifacts /home/runner/work/bigbluebutton/bigbluebutton/artifacts/
# EOF
uses: bigbluebutton/bigbluebutton/.github/workflows/automated-tests-build-package-job.yml@develop
with:
build-name: others
build-list: bbb-mkclean bbb-pads bbb-libreoffice-docker bbb-transcription-controller bigbluebutton
install-and-run-tests:
needs: [build-bbb-apps-akka, build-bbb-config, build-bbb-export-annotations, build-bbb-learning-dashboard, build-bbb-playback-record, build-bbb-graphql-server, build-bbb-etherpad, build-bbb-bbb-web, build-bbb-fsesl-akka, build-bbb-html5, build-bbb-freeswitch, build-others]
needs: [build-bbb-apps-akka, build-bbb-config, build-bbb-export-annotations, build-bbb-learning-dashboard, build-bbb-playback-record, build-bbb-graphql-server, build-bbb-etherpad, build-bbb-bbb-web, build-bbb-fsesl-akka, build-bbb-html5, build-bbb-freeswitch, build-bbb-webrtc, build-others]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Checkout ${{ github.event.pull_request.base.ref || 'master' }}
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.base.ref || '' }}
fetch-depth: 0 # Fetch all history
- name: Merge pr-${{ github.event.number }} into ${{ github.event.pull_request.base.ref }}
if: github.event_name == 'pull_request'
run: |
git config user.name "BBB Automated Tests"
git config user.email "tests@bigbluebutton.org"
git config pull.rebase false
git pull origin pull/${{ github.event.number }}/head:${{ github.head_ref }}
- run: ./build/get_external_dependencies.sh
- name: Download artifacts_bbb-apps-akka
uses: actions/download-artifact@v3
@ -368,6 +146,11 @@ jobs:
with:
name: artifacts_bbb-freeswitch.tar
- run: tar xf artifacts.tar
- name: Download artifacts_bbb-webrtc
uses: actions/download-artifact@v3
with:
name: artifacts_bbb-webrtc.tar
- run: tar xf artifacts.tar
- name: Download artifacts_bbb-web
uses: actions/download-artifact@v3
with:
@ -386,7 +169,7 @@ jobs:
- name: Download artifacts
uses: actions/download-artifact@v3
with:
name: artifacts.tar
name: artifacts_others.tar
- run: tar xf artifacts.tar
- name: Extracting files .tar
run: |
@ -461,7 +244,7 @@ jobs:
run: |
sudo -i <<EOF
set -e
cd /root/ && wget -q https://raw.githubusercontent.com/bigbluebutton/bbb-install/v2.8.x-release/bbb-install.sh -O bbb-install.sh
cd /root/ && wget -nv https://raw.githubusercontent.com/bigbluebutton/bbb-install/v2.8.x-release/bbb-install.sh -O bbb-install.sh
cat bbb-install.sh | sed "s|> /etc/apt/sources.list.d/bigbluebutton.list||g" | bash -s -- -v jammy-28-develop -s bbb-ci.test -j -d /certs/
bbb-conf --salt bbbci
echo "NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt" >> /usr/share/meteor/bundle/bbb-html5-with-roles.conf

View File

@ -0,0 +1,88 @@
name: Publish Test Results
on:
workflow_run:
workflows:
- Automated tests
types:
- completed
jobs:
get-pr-data:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.event == 'pull_request' }}
outputs:
pr-number: ${{ steps.set-env.outputs.pr-number }}
workflow-id: ${{ steps.set-env.outputs.workflow-id }}
steps:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
- name: Download artifact
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr-comment-data"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr-comment-data.zip`, Buffer.from(download.data));
- name: Unzip artifact
run: unzip pr-comment-data.zip
- name: Set env variables
id: set-env
run: |
echo "pr-number=$(cat ./pr_number)" >> $GITHUB_OUTPUT
echo "workflow-id=$(cat ./workflow_id)" >> $GITHUB_OUTPUT
comment-pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
needs: get-pr-data
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: Automated tests Summary
- name: Remove previous comment
if: steps.fc.outputs.comment-id != ''
uses: actions/github-script@v6
with:
script: |
github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ steps.fc.outputs.comment-id }}
})
- name: Passing tests comment
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
body: |
<h1>Automated tests Summary</h1>
<h3><strong>:white_check_mark:</strong> All the CI tests have passed!</h3>
- name: Failing tests comment
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
uses: peter-evans/create-or-update-comment@v2
with:
issue-number: ${{ needs.get-pr-data.outputs.pr-number }}
body: |
<h1> Automated tests Summary</h1>
<h3><strong>:rotating_light:</strong> Test workflow has failed</h3>
___
[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ needs.get-pr-data.outputs.workflow-id }}) to check the action test reports

View File

@ -41,6 +41,7 @@ trait SystemConfiguration {
lazy val syncVoiceUsersStatusInterval = Try(config.getInt("voiceConf.syncUserStatusInterval")).getOrElse(43)
lazy val ejectRogueVoiceUsers = Try(config.getBoolean("voiceConf.ejectRogueVoiceUsers")).getOrElse(true)
lazy val dialInApprovalAudioPath = Try(config.getString("voiceConf.dialInApprovalAudioPath")).getOrElse("ivr/ivr-please_hold_while_party_contacted.wav")
lazy val toggleListenOnlyAfterMuteTimer = Try(config.getInt("voiceConf.toggleListenOnlyAfterMuteTimer")).getOrElse(4)
lazy val recordingChapterBreakLengthInMinutes = Try(config.getInt("recording.chapterBreakLengthInMinutes")).getOrElse(0)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
package org.bigbluebutton.core.apps.presentationpod
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.apps.RightsManagementTrait
import org.bigbluebutton.core.bus.MessageBus
import org.bigbluebutton.core.db.PresPageDAO
import org.bigbluebutton.core.domain.MeetingState2x
import org.bigbluebutton.core.running.LiveMeeting
trait AddSlidePositionsPubMsgHdlr extends RightsManagementTrait {
this: PresentationPodHdlrs =>
def handle(msg: AddSlidePositionsPubMsg, state: MeetingState2x,
liveMeeting: LiveMeeting, bus: MessageBus) = {
PresPageDAO.addSlidePosition(msg.body.slideId, msg.body.width, msg.body.height,
msg.body.viewBoxWidth, msg.body.viewBoxHeight)
state
}
}

View File

@ -166,7 +166,7 @@ trait MakePresentationDownloadReqMsgHdlr extends RightsManagementTrait {
PresentationSender.broadcastSetPresentationDownloadableEvtMsg(bus, meetingId, "DEFAULT_PRESENTATION_POD", "not-used", presId, true, filename)
val fileURI = List("bigbluebutton", "presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString("", File.separator, "")
val fileURI = List("presentation", "download", meetingId, s"${presId}?presFilename=${presId}.${presFilenameExt}&filename=${filename}").mkString("", File.separator, "")
val event = buildNewPresFileAvailable(fileURI, presId, m.body.typeOfExport)
handle(event, liveMeeting, bus)

View File

@ -54,7 +54,9 @@ trait PresentationPageConvertedSysMsgHdlr {
msg.body.page.id,
msg.body.page.num,
msg.body.page.urls,
msg.body.page.current
msg.body.page.current,
width = msg.body.page.width,
height = msg.body.page.height
)
val newState = for {

View File

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

View File

@ -86,7 +86,9 @@ object PresentationPodsApp {
xOffset = page.xOffset,
yOffset = page.yOffset,
widthRatio = page.widthRatio,
heightRatio = page.heightRatio
heightRatio = page.heightRatio,
width = page.width,
height = page.height
)
}
PresentationVO(pres.id, temporaryPresentationId, pres.name, pres.current, pages.toVector, pres.downloadable,

View File

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

View File

@ -0,0 +1,21 @@
package org.bigbluebutton.core.apps.voice
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.running.{ MeetingActor, LiveMeeting, OutMsgRouter }
trait ChannelHoldChangedVoiceConfEvtMsgHdlr {
this: MeetingActor =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleChannelHoldChangedVoiceConfEvtMsg(msg: ChannelHoldChangedVoiceConfEvtMsg): Unit = {
VoiceApp.handleChannelHoldChanged(
liveMeeting,
outGW,
msg.body.intId,
msg.body.uuid,
msg.body.hold
)
}
}

View File

@ -0,0 +1,25 @@
package org.bigbluebutton.core.apps.voice
import org.bigbluebutton.common2.msgs._
import org.bigbluebutton.core.models.VoiceUsers
import org.bigbluebutton.core.running.{ BaseMeetingActor, LiveMeeting, OutMsgRouter }
trait ListenOnlyModeToggledInSfuEvtMsgHdlr {
this: BaseMeetingActor =>
val liveMeeting: LiveMeeting
val outGW: OutMsgRouter
def handleListenOnlyModeToggledInSfuEvtMsg(msg: ListenOnlyModeToggledInSfuEvtMsg): Unit = {
for {
vu <- VoiceUsers.findWithIntId(liveMeeting.voiceUsers, msg.body.userId)
} yield {
VoiceApp.holdChannelInVoiceConf(
liveMeeting,
outGW,
vu.uuid,
msg.body.enabled
)
}
}
}

View File

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

View File

@ -1,5 +1,6 @@
package org.bigbluebutton.core.apps.voice
import akka.actor.{ ActorContext, ActorSystem, Cancellable }
import org.bigbluebutton.SystemConfiguration
import org.bigbluebutton.LockSettingsUtil
import org.bigbluebutton.core.apps.breakout.BreakoutHdlrHelpers
@ -12,9 +13,14 @@ import org.bigbluebutton.core.models._
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.util.ColorPicker
import org.bigbluebutton.core.util.TimeUtil
import scala.collection.immutable.Map
import scala.concurrent.duration._
object VoiceApp extends SystemConfiguration {
// Key is userId
var toggleListenOnlyTasks: Map[String, Cancellable] = Map()
def genRecordPath(
recordDir: String,
meetingId: String,
@ -104,7 +110,7 @@ object VoiceApp extends SystemConfiguration {
outGW: OutMsgRouter,
voiceUserId: String,
muted: Boolean
): Unit = {
)(implicit context: ActorContext): Unit = {
for {
mutedUser <- VoiceUsers.userMuted(liveMeeting.voiceUsers, voiceUserId, muted)
} yield {
@ -117,13 +123,32 @@ object VoiceApp extends SystemConfiguration {
)
}
broadcastUserMutedVoiceEvtMsg(
liveMeeting.props.meetingProp.intId,
mutedUser,
liveMeeting.props.voiceProp.voiceConf,
outGW
// Ask for the audio channel to be switched to listen only mode
// if the user is muted, otherwise switch back to normal mode
// This is only effective if the "transparent listen only" mode is active
// for the target user.
toggleListenOnlyMode(
liveMeeting,
outGW,
mutedUser.intId,
muted,
toggleListenOnlyAfterMuteTimer
)
// If the user is muted or unmuted with an unheld channel, broadcast
// the event right away.
// If the user is unmuted, but channel is held, we need to wait for the
// channel to be active again to broadcast the event. See
// VoiceApp.handleChannelHoldChanged for this second case.
if (muted || (!muted && !mutedUser.hold)) {
broadcastUserMutedVoiceEvtMsg(
liveMeeting.props.meetingProp.intId,
mutedUser,
liveMeeting.props.voiceProp.voiceConf,
outGW
)
}
}
}
@ -132,7 +157,7 @@ object VoiceApp extends SystemConfiguration {
outGW: OutMsgRouter,
eventBus: InternalEventBus,
users: Vector[ConfVoiceUser]
): Unit = {
)(implicit context: ActorContext): Unit = {
users foreach { cvu =>
VoiceUsers.findWithVoiceUserId(
liveMeeting.voiceUsers,
@ -179,7 +204,9 @@ object VoiceApp extends SystemConfiguration {
ColorPicker.nextColor(liveMeeting.props.meetingProp.intId),
cvu.muted,
cvu.talking,
cvu.calledInto
cvu.calledInto,
cvu.hold,
cvu.uuid,
)
}
}
@ -229,7 +256,9 @@ object VoiceApp extends SystemConfiguration {
color: String,
muted: Boolean,
talking: Boolean,
callingInto: String
callingInto: String,
hold: Boolean,
uuid: String = "unused"
): Unit = {
def broadcastEvent(voiceUserState: VoiceUserState): Unit = {
@ -289,7 +318,9 @@ object VoiceApp extends SystemConfiguration {
callingInto,
System.currentTimeMillis(),
floor = false,
lastFloorTime = "0"
lastFloorTime = "0",
hold,
uuid
)
VoiceUsers.add(liveMeeting.voiceUsers, voiceUserState)
@ -431,4 +462,108 @@ object VoiceApp extends SystemConfiguration {
)
outGW.send(deafEvent)
}
def removeToggleListenOnlyTask(userId: String): Unit = {
toggleListenOnlyTasks get userId match {
case Some(task) =>
task.cancel()
toggleListenOnlyTasks = toggleListenOnlyTasks - userId
case _ =>
}
}
def toggleListenOnlyMode(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
userId: String,
enabled: Boolean,
delay: Int = 0
)(implicit context: ActorContext): Unit = {
implicit def executionContext = context.system.dispatcher
def broacastEvent(): Unit = {
val event = MsgBuilder.buildToggleListenOnlyModeSysMsg(
liveMeeting.props.meetingProp.intId,
liveMeeting.props.voiceProp.voiceConf,
userId,
enabled
)
outGW.send(event)
}
// Guarantee there are no other tasks for this channel
removeToggleListenOnlyTask(userId)
if (enabled && delay > 0) {
// If we are enabling listen only mode, we wait a bit before actually
// dispatching the command - the idea is that recently muted users
// are more likely to unmute themselves right after the action, so this
// should make frequent mute-unmute transitions smoother.
// This is just one of the heuristics we have to implement for this to
// work seamlessly, but it's a start. - prlanzarin Aug 04 2023
val newTask = context.system.scheduler.scheduleOnce(delay seconds) {
broacastEvent()
removeToggleListenOnlyTask(userId)
}
toggleListenOnlyTasks = toggleListenOnlyTasks + (userId -> newTask)
} else {
// If we are disabling listen only mode, we can broadcast the event
// right away
broacastEvent()
}
}
def holdChannelInVoiceConf(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
uuid: String,
hold: Boolean
): Unit = {
val event = MsgBuilder.buildHoldChannelInVoiceConfSysMsg(
liveMeeting.props.meetingProp.intId,
liveMeeting.props.voiceProp.voiceConf,
uuid,
hold
)
outGW.send(event)
}
def handleChannelHoldChanged(
liveMeeting: LiveMeeting,
outGW: OutMsgRouter,
intId: String,
uuid: String,
hold: Boolean
)(implicit context: ActorContext): Unit = {
VoiceUsers.holdStateChanged(
liveMeeting.voiceUsers,
intId,
uuid,
hold
) match {
case Some(vu) =>
// Mute vs hold state mismatch, enforce hold state again.
// Mute state is the predominant one here.
if (vu.muted != hold) {
toggleListenOnlyMode(
liveMeeting,
outGW,
intId,
vu.muted
)
}
// User unmuted and channel is not on hold, broadcast user unmuted
if (!vu.muted && !vu.hold) {
broadcastUserMutedVoiceEvtMsg(
liveMeeting.props.meetingProp.intId,
vu,
liveMeeting.props.voiceProp.voiceConf,
outGW
)
}
case _ =>
}
}
}

View File

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

View File

@ -74,19 +74,6 @@ object PresPageDAO {
}
}
def addSlidePosition(slideId: String, width: Double, height: Double,
viewBoxWidth: Double, viewBoxHeight: Double) = {
DatabaseConnection.db.run(
TableQuery[PresPageDbTableDef]
.filter(_.pageId === slideId)
.map(p => (p.width, p.height, p.viewBoxWidth, p.viewBoxHeight))
.update((width, height, viewBoxWidth, viewBoxHeight))
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) added slide position on PresPage table")
case Failure(e) => DatabaseConnection.logger.debug(s"Error updating slide position on PresPage: $e")
}
}
def updateSlidePosition(pageId: String, width: Double, height: Double, xOffset: Double, yOffset: Double,
widthRatio: Double, heightRatio: Double) = {
DatabaseConnection.db.run(

View File

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

View File

@ -0,0 +1,60 @@
package org.bigbluebutton.core.db
import org.bigbluebutton.core.models.PadGroup
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class SharedNotesDbModel(
meetingId: String,
sharedNotesExtId: String,
padId: String,
model: String,
name: String,
pinned: Boolean
)
class SharedNotesDbTableDef(tag: Tag) extends Table[SharedNotesDbModel](tag, None, "sharedNotes") {
val meetingId = column[String]("meetingId", O.PrimaryKey)
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
val padId = column[String]("padId")
val model = column[String]("model")
val name = column[String]("name")
val pinned = column[Boolean]("pinned")
val * = (
meetingId, sharedNotesExtId, padId, model, name, pinned
) <> (SharedNotesDbModel.tupled, SharedNotesDbModel.unapply)
}
object SharedNotesDAO {
def insert(meetingId: String, group: PadGroup, padId: String, name: String) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesDbTableDef].insertOrUpdate(
SharedNotesDbModel(
meetingId = meetingId,
sharedNotesExtId = group.externalId,
padId = padId,
model = group.model,
name = name,
pinned = false
)
)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotes table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotes: $e")
}
}
def updatePinned(meetingId: String, sharedNotesExtId: String, pinned: Boolean) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesDbTableDef]
.filter(_.meetingId === meetingId)
.filter(_.sharedNotesExtId === sharedNotesExtId)
.map(n => n.pinned)
.update(pinned)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated pinned on SharedNotes table!")
case Failure(e) => DatabaseConnection.logger.error(s"Error updating pinned SharedNotes: $e")
}
}
}

View File

@ -0,0 +1,68 @@
package org.bigbluebutton.core.db
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class SharedNotesRevDbModel(
meetingId: String,
sharedNotesExtId: String,
rev: Int,
userId: String,
changeset: String,
start: Option[Int],
end: Option[Int],
diff: Option[String],
createdAt: java.sql.Timestamp
)
class SharedNotesRevDbTableDef(tag: Tag) extends Table[SharedNotesRevDbModel](tag, None, "sharedNotes_rev") {
val meetingId = column[String]("meetingId", O.PrimaryKey)
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
val rev = column[Int]("rev", O.PrimaryKey)
val userId = column[String]("userId")
val changeset = column[String]("changeset")
val start = column[Option[Int]]("start")
val end = column[Option[Int]]("end")
val diff = column[Option[String]]("diff")
val createdAt = column[java.sql.Timestamp]("createdAt")
val * = (meetingId, sharedNotesExtId, rev, userId, changeset, start, end, diff, createdAt) <> (SharedNotesRevDbModel.tupled, SharedNotesRevDbModel.unapply)
}
object SharedNotesRevDAO {
def insert(meetingId: String, sharedNotesExtId: String, revId: Int, userId: String, changeset: String) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesRevDbTableDef].insertOrUpdate(
SharedNotesRevDbModel(
meetingId = meetingId,
sharedNotesExtId = sharedNotesExtId,
rev = revId,
userId = userId,
changeset = changeset,
start = None,
end = None,
diff = None,
createdAt = new java.sql.Timestamp(System.currentTimeMillis())
)
)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesRev table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesRev: $e")
}
}
def update(meetingId: String, sharedNotesExtId: String, revId: Int, start: Int, end: Int, text: String) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesRevDbTableDef]
.filter(_.meetingId === meetingId)
.filter(_.sharedNotesExtId === sharedNotesExtId)
.filter(_.rev === revId)
.map(n => (n.start, n.end, n.diff))
.update((Some(start), Some(end), Some(text)))
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated Rev on SharedNotes table!")
case Failure(e) => DatabaseConnection.logger.error(s"Error updating Rev SharedNotes: $e")
}
}
}

View File

@ -0,0 +1,49 @@
package org.bigbluebutton.core.db
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{ Failure, Success }
case class SharedNotesSessionDbModel(
meetingId: String,
sharedNotesExtId: String,
userId: String,
sessionId: String
)
class SharedNotesSessionDbTableDef(tag: Tag) extends Table[SharedNotesSessionDbModel](tag, None, "sharedNotes_session") {
val meetingId = column[String]("meetingId", O.PrimaryKey)
val sharedNotesExtId = column[String]("sharedNotesExtId", O.PrimaryKey)
val userId = column[String]("userId", O.PrimaryKey)
val sessionId = column[String]("sessionId")
val * = (meetingId, sharedNotesExtId, userId, sessionId) <> (SharedNotesSessionDbModel.tupled, SharedNotesSessionDbModel.unapply)
}
object SharedNotesSessionDAO {
def insert(meetingId: String, sharedNotesExtId: String, userId: String, sessionId: String) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesSessionDbTableDef].insertOrUpdate(
SharedNotesSessionDbModel(
meetingId = meetingId,
sharedNotesExtId = sharedNotesExtId,
userId = userId,
sessionId = sessionId
)
)
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) inserted on SharedNotesSession table!")
case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting SharedNotesSession: $e")
}
}
def delete(intId: String, sessionId: String) = {
DatabaseConnection.db.run(
TableQuery[SharedNotesSessionDbTableDef]
.filter(_.userId === intId)
.filter(_.sessionId === sessionId)
.delete
).onComplete {
case Success(rowsAffected) => DatabaseConnection.logger.debug(s"SharedNotesSession ${sessionId} deleted")
case Failure(e) => DatabaseConnection.logger.error(s"Error deleting SharedNotesSession ${sessionId}: $e")
}
}
}

View File

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

View File

@ -12,6 +12,10 @@ object VoiceUsers {
users.toVector.find(u => u.intId == intId)
}
def findWithIntIdAndUUID(users: VoiceUsers, intId: String, uuid: String): Option[VoiceUserState] = {
users.toVector.find(u => u.uuid == uuid && u.intId == intId)
}
def findAll(users: VoiceUsers): Vector[VoiceUserState] = users.toVector
def findAllNonListenOnlyVoiceUsers(users: VoiceUsers): Vector[VoiceUserState] = users.toVector.filter(u => u.listenOnly == false)
@ -100,6 +104,17 @@ object VoiceUsers {
}
}
def holdStateChanged(users: VoiceUsers, intId: String, uuid: String, hold: Boolean): Option[VoiceUserState] = {
for {
u <- findWithIntIdAndUUID(users, intId, uuid)
} yield {
val vu = u.modify(_.hold).setTo(hold)
.modify(_.lastStatusUpdateOn).setTo(System.currentTimeMillis())
users.save(vu)
vu
}
}
def setLastStatusUpdate(users: VoiceUsers, user: VoiceUserState): VoiceUserState = {
val vu = user.copy(lastStatusUpdateOn = System.currentTimeMillis())
users.save(vu)
@ -174,7 +189,9 @@ case class VoiceUserVO2x(
callingWith: String,
listenOnly: Boolean,
floor: Boolean,
lastFloorTime: String
lastFloorTime: String,
hold: Boolean,
uuid: String
)
case class VoiceUserState(
@ -190,5 +207,7 @@ case class VoiceUserState(
calledInto: String,
lastStatusUpdateOn: Long,
floor: Boolean,
lastFloorTime: String
lastFloorTime: String,
hold: Boolean,
uuid: String
)

View File

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

View File

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

View File

@ -102,6 +102,10 @@ class AnalyticsActor(val includeChat: Boolean) extends Actor with ActorLogging {
logMessage(msg)
case m: VoiceConfCallStateEvtMsg => logMessage(msg)
case m: VoiceCallStateEvtMsg => logMessage(msg)
case m: HoldChannelInVoiceConfSysMsg => logMessage(msg)
case m: ChannelHoldChangedVoiceConfEvtMsg => logMessage(msg)
case m: ToggleListenOnlyModeSysMsg => logMessage(msg)
case m: ListenOnlyModeToggledInSfuEvtMsg => logMessage(msg)
// Breakout
case m: BreakoutRoomEndedEvtMsg => logMessage(msg)

View File

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

View File

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

View File

@ -628,4 +628,34 @@ object MsgBuilder {
BbbCommonEnvCoreMsg(envelope, event)
}
def buildHoldChannelInVoiceConfSysMsg(
meetingId: String,
voiceConf: String,
uuid: String,
hold: Boolean
): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(HoldChannelInVoiceConfSysMsg.NAME, routing)
val body = HoldChannelInVoiceConfSysMsgBody(voiceConf, uuid, hold)
val header = BbbCoreHeaderWithMeetingId(HoldChannelInVoiceConfSysMsg.NAME, meetingId)
val event = HoldChannelInVoiceConfSysMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
def buildToggleListenOnlyModeSysMsg(
meetingId: String,
voiceConf: String,
userId: String,
enabled: Boolean
): BbbCommonEnvCoreMsg = {
val routing = collection.immutable.HashMap("sender" -> "bbb-apps-akka")
val envelope = BbbCoreEnvelope(ToggleListenOnlyModeSysMsg.NAME, routing)
val body = ToggleListenOnlyModeSysMsgBody(voiceConf, userId, enabled)
val header = BbbCoreHeaderWithMeetingId(ToggleListenOnlyModeSysMsg.NAME, meetingId)
val event = ToggleListenOnlyModeSysMsg(header, body)
BbbCommonEnvCoreMsg(envelope, event)
}
}

View File

@ -66,7 +66,9 @@ object FakeUserGenerator {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
callerNum = user.name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime,
false,
"9b3f4504-275d-4315-9922-21174262d88c")
}
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -76,7 +78,9 @@ object FakeUserGenerator {
val name = getRandomElement(firstNames, random) + " " + getRandomElement(lastNames, random)
val lastFloorTime = System.currentTimeMillis().toString();
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime)
callerNum = name, "#ff6242", muted, talking, listenOnly, "freeswitch", System.currentTimeMillis(), floor, lastFloorTime,
false,
"9b3f4504-275d-4315-9922-21174262d88c")
}
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {

View File

@ -25,7 +25,9 @@ object TestDataGen {
listenOnly: Boolean): VoiceUserState = {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
VoiceUserState(intId = user.id, voiceUserId = voiceUserId, callingWith, callerName = user.name,
callerNum = user.name, "#ff6242", muted, talking, listenOnly)
callerNum = user.name, "#ff6242", muted, talking, listenOnly,
false,
"9b3f4504-275d-4315-9922-21174262d88c")
}
def createFakeVoiceOnlyUser(callingWith: String, muted: Boolean, talking: Boolean,
@ -33,7 +35,9 @@ object TestDataGen {
val voiceUserId = RandomStringGenerator.randomAlphanumericString(8)
val intId = "v_" + RandomStringGenerator.randomAlphanumericString(16)
VoiceUserState(intId, voiceUserId = voiceUserId, callingWith, callerName = name,
callerNum = name, "#ff6242", muted, talking, listenOnly)
callerNum = name, "#ff6242", muted, talking, listenOnly
false,
"9b3f4504-275d-4315-9922-21174262d88c")
}
def createFakeWebcamStreamFor(userId: String, subscribers: Set[String]): WebcamStream = {

View File

@ -107,6 +107,10 @@ voiceConf {
# Path to the audio file being played when dial-in user is waiting for
# approval. This can be relative to FreeSWITCH sounds folder
dialInApprovalAudioPath = "ivr/ivr-please_hold_while_party_contacted.wav"
# Time (seconds) to wait before requesting an audio channel hold after
# muting a user. Used in the experimental, transparent listen only mode.
toggleListenOnlyAfterMuteTimer = 4
}
recording {

View File

@ -57,8 +57,18 @@ public class FreeswitchConferenceEventListener implements ConferenceEventListene
if (event instanceof VoiceUserJoinedEvent) {
VoiceUserJoinedEvent evt = (VoiceUserJoinedEvent) event;
vcs.userJoinedVoiceConf(evt.getRoom(), evt.getVoiceUserId(), evt.getUserId(), evt.getCallerIdName(),
evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith());
} else if (event instanceof VoiceConfRunningEvent) {
evt.getCallerIdNum(), evt.getMuted(), evt.getSpeaking(), evt.getCallingWith(),
evt.getHold(),
evt.getUUID());
} else if (event instanceof ChannelHoldChangedEvent) {
ChannelHoldChangedEvent evt = (ChannelHoldChangedEvent) event;
vcs.channelHoldChanged(
evt.getRoom(),
evt.getUserId(),
evt.getUUID(),
evt.isHeld()
);
} else if (event instanceof VoiceConfRunningEvent) {
VoiceConfRunningEvent evt = (VoiceConfRunningEvent) event;
vcs.voiceConfRunning(evt.getRoom(), evt.isRunning());
} else if (event instanceof VoiceUserLeftEvent) {

View File

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

View File

@ -0,0 +1,51 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.freeswitch.voice.events;
public class ChannelHoldChangedEvent extends VoiceConferenceEvent {
private final String userId;
private final String uuid;
private final boolean hold;
public ChannelHoldChangedEvent(
String room,
String userId,
String uuid,
boolean hold
) {
super(room);
this.userId = userId;
this.uuid = uuid;
this.hold = hold;
}
public String getUserId() {
return userId;
}
public String getUUID() {
return uuid;
}
public boolean isHeld() {
return hold;
}
}

View File

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

View File

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

View File

@ -94,6 +94,7 @@ public class ConnectionManager {
//c.addEventFilter(EVENT_NAME, "background_job");
c.addEventFilter(EVENT_NAME, "CHANNEL_EXECUTE");
c.addEventFilter(EVENT_NAME, "CHANNEL_STATE");
c.addEventFilter(EVENT_NAME, "CHANNEL_CALLSTATE");
subscribed = true;
} else {
// Let's check for status every minute.
@ -239,6 +240,13 @@ public class ConnectionManager {
}
}
public void holdChannel(HoldChannelCommand hcc) {
Client c = manager.getESLClient();
if (c.canSend()) {
c.sendAsyncApiCommand(hcc.getCommand(), hcc.getCommandArgs());
}
}
public void eject(EjectUserCommand mpc) {
Client c = manager.getESLClient();
if (c.canSend()) {

View File

@ -26,6 +26,9 @@ public class ESLEventListener implements IEslEventListener {
private static final String CONFERENCE_CREATED_EVENT = "conference-create";
private static final String CONFERENCE_DESTROYED_EVENT = "conference-destroy";
private static final String FLOOR_CHANGE_EVENT = "video-floor-change";
private static final String CHANNEL_CALLSTATE_EVENT = "CHANNEL_CALLSTATE";
private static final String CHANNEL_CALLSTATE_HELD = "HELD";
private static final String CHANNEL_CALLSTATE_ACTIVE = "ACTIVE";
private final ConferenceEventListener conferenceEventListener;
@ -59,12 +62,14 @@ public class ESLEventListener implements IEslEventListener {
@Override
public void conferenceEventJoin(String uniqueId, String confName, int confSize, EslEvent event) {
Integer memberId = this.getMemberIdFromEvent(event);
Integer memberId = this.getMemberId(event);
Map<String, String> headers = event.getEventHeaders();
String callerId = this.getCallerIdFromEvent(event);
String callerId = this.getCallerId(event);
String callerIdName = this.getCallerIdNameFromEvent(event);
String channelCallState = this.getChannelCallState(headers);
boolean muted = headers.get("Speak").equals("true") ? false : true; //Was inverted which was causing a State issue
boolean speaking = headers.get("Talking").equals("true") ? true : false;
boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD);
String voiceUserId = callerIdName;
@ -124,14 +129,16 @@ public class ESLEventListener implements IEslEventListener {
callerIdName,
muted,
speaking,
"none");
"none",
hold,
callerUUID);
conferenceEventListener.handleConferenceEvent(pj);
}
@Override
public void conferenceEventLeave(String uniqueId, String confName, int confSize, EslEvent event) {
Integer memberId = this.getMemberIdFromEvent(event);
String callerId = this.getCallerIdFromEvent(event);
Integer memberId = this.getMemberId(event);
String callerId = this.getCallerId(event);
String callerIdName = this.getCallerIdNameFromEvent(event);
String callerUUID = this.getMemberUUIDFromEvent(event);
@ -146,14 +153,14 @@ public class ESLEventListener implements IEslEventListener {
@Override
public void conferenceEventMute(String uniqueId, String confName, int confSize, EslEvent event) {
Integer memberId = this.getMemberIdFromEvent(event);
Integer memberId = this.getMemberId(event);
VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, true);
conferenceEventListener.handleConferenceEvent(pm);
}
@Override
public void conferenceEventUnMute(String uniqueId, String confName, int confSize, EslEvent event) {
Integer memberId = this.getMemberIdFromEvent(event);
Integer memberId = this.getMemberId(event);
VoiceUserMutedEvent pm = new VoiceUserMutedEvent(memberId.toString(), confName, false);
conferenceEventListener.handleConferenceEvent(pm);
}
@ -165,11 +172,11 @@ public class ESLEventListener implements IEslEventListener {
}
if (action.equals(START_TALKING_EVENT)) {
Integer memberId = this.getMemberIdFromEvent(event);
Integer memberId = this.getMemberId(event);
VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, true);
conferenceEventListener.handleConferenceEvent(pt);
} else if (action.equals(STOP_TALKING_EVENT)) {
Integer memberId = this.getMemberIdFromEvent(event);
Integer memberId = this.getMemberId(event);
VoiceUserTalkingEvent pt = new VoiceUserTalkingEvent(memberId.toString(), confName, false);
conferenceEventListener.handleConferenceEvent(pt);
} else if (action.equals(CONFERENCE_CREATED_EVENT)) {
@ -437,16 +444,92 @@ public class ESLEventListener implements IEslEventListener {
);
conferenceEventListener.handleConferenceEvent(csEvent);
}
} else if (event.getEventName().equals(CHANNEL_CALLSTATE_EVENT)) {
Map<String, String> eventHeaders = event.getEventHeaders();
String channelCallState = this.getChannelCallState(eventHeaders);
String originalChannelCallState = eventHeaders.get("Original-Channel-Call-State");
if (channelCallState == null
|| originalChannelCallState == null
|| channelCallState.equals(originalChannelCallState)
|| !(channelCallState.equals(CHANNEL_CALLSTATE_HELD) || channelCallState.equals(CHANNEL_CALLSTATE_ACTIVE))) {
// No call state info, or no change in call state, or not a call state we care about
return;
}
String intId = this.getIntId(event);
if (intId == null) {
return;
}
Boolean hold = channelCallState.equals(CHANNEL_CALLSTATE_HELD);
String uuid = this.getMemberUUIDFromEvent(event);
String conference = eventHeaders.get("Caller-Destination-Number");
Matcher callerDestNumberMatcher = ECHO_TEST_DEST_PATTERN.matcher(conference);
if (callerDestNumberMatcher.matches()) {
conference = callerDestNumberMatcher.group(1).trim();
}
ChannelHoldChangedEvent csEvent = new ChannelHoldChangedEvent(
conference,
intId,
uuid,
hold
);
conferenceEventListener.handleConferenceEvent(csEvent);
}
}
private String getIntId(EslEvent event) {
return this.getIntId(event.getEventHeaders());
}
private String getIntId(Map<String, String> eventHeaders) {
String origCallerIdName = this.getCallerId(eventHeaders);
Integer memberId = this.getMemberId(eventHeaders);
Matcher callerListenOnly = CALLERNAME_LISTENONLY_PATTERN.matcher(origCallerIdName);
Matcher callWithSess = CALLERNAME_WITH_SESS_INFO_PATTERN.matcher(origCallerIdName);
if (callWithSess.matches()) {
return callWithSess.group(1).trim();
} else if (callerListenOnly.matches()) {
return callerListenOnly.group(1).trim();
} else if (memberId != null) {
return "v_" + memberId.toString();
} else {
return null;
}
}
private Integer getMemberIdFromEvent(EslEvent e) {
return new Integer(e.getEventHeaders().get("Member-ID"));
private Integer getMemberId(EslEvent event) {
return this.getMemberId(event.getEventHeaders());
}
private String getCallerIdFromEvent(EslEvent e) {
return e.getEventHeaders().get("Caller-Caller-ID-Number");
private Integer getMemberId(Map<String, String> eventHeaders) {
String memberId = eventHeaders.get("Member-ID");
if (memberId == null) {
return null;
}
return Integer.valueOf(memberId);
}
private String getCallerId(EslEvent event) {
return this.getCallerId(event.getEventHeaders());
}
private String getCallerId(Map<String, String> eventHeaders) {
return eventHeaders.get("Caller-Caller-ID-Number");
}
private String getChannelCallState(EslEvent event) {
return this.getChannelCallState(event.getEventHeaders());
}
private String getChannelCallState(Map<String, String> eventHeaders) {
return eventHeaders.get("Channel-Call-State");
}
private String getMemberUUIDFromEvent(EslEvent e) {

View File

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

View File

@ -108,7 +108,9 @@ public class GetAllUsersCommand extends FreeswitchCommand {
}
VoiceUserJoinedEvent pj = new VoiceUserJoinedEvent(voiceUserId, member.getId().toString(), confXML.getConferenceRoom(),
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none");
callerId, callerIdName, member.getMuted(), member.getSpeaking(), "none",
member.getHold(),
uuid);
eventListener.handleConferenceEvent(pj);
} else if ("recording_node".equals(member.getMemberType())) {

View File

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

View File

@ -0,0 +1,44 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2023 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.freeswitch.voice.freeswitch.actions;
public class HoldChannelCommand extends FreeswitchCommand {
private final String uuid;
private final Boolean hold;
public HoldChannelCommand(String room, String uuid, Boolean hold, String requesterId) {
super(room, requesterId);
this.uuid = uuid;
this.hold = hold;
}
@Override
public String getCommand() {
return "uuid_hold";
}
@Override
public String getCommandArgs() {
if (hold) {
return "toggle" + SPACE + uuid;
} else {
return "off" + SPACE + uuid;
}
}
}

View File

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

View File

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

View File

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

View File

@ -243,4 +243,21 @@ trait RxJsonMsgDeserializer {
}
}
def routeHoldChannelInVoiceConfMsg(envelope: BbbCoreEnvelope, jsonNode: JsonNode): Unit = {
def deserialize(jsonNode: JsonNode): Option[HoldChannelInVoiceConfSysMsg] = {
val (result, error) = JsonDeserializer.toBbbCommonMsg[HoldChannelInVoiceConfSysMsg](jsonNode)
result match {
case Some(msg) => Some(msg.asInstanceOf[HoldChannelInVoiceConfSysMsg])
case None =>
log.error("Failed to deserialize message: error: {} \n msg: {}", error, jsonNode)
None
}
}
for {
m <- deserialize(jsonNode)
} yield {
fsApp.holdChannel(m.body.voiceConf, m.body.uuid, m.body.hold)
}
}
}

View File

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

View File

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

View File

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

View File

@ -45,11 +45,6 @@ case class SlideResizedPubMsg(header: BbbClientMsgHeader, body: SlideResizedPubM
case class SlideResizedPubMsgBody(pageId: String, width: Double, height: Double,
xOffset: Double, yOffset: Double, widthRatio: Double, heightRatio: Double)
object AddSlidePositionsPubMsg { val NAME = "AddSlidePositionsPubMsg" }
case class AddSlidePositionsPubMsg(header: BbbClientMsgHeader, body: AddSlidePositionsPubMsgBody) extends StandardMsg
case class AddSlidePositionsPubMsgBody(slideId: String, width: Double,
height: Double, viewBoxWidth: Double, viewBoxHeight: Double)
object SetCurrentPresentationPubMsg { val NAME = "SetCurrentPresentationPubMsg" }
case class SetCurrentPresentationPubMsg(header: BbbClientMsgHeader, body: SetCurrentPresentationPubMsgBody) extends StandardMsg
case class SetCurrentPresentationPubMsgBody(podId: String, presentationId: String)

View File

@ -387,8 +387,9 @@ case class UserStatusVoiceConfEvtMsgBody(voiceConf: String, confUsers: Vector[Co
case class ConfVoiceUser(voiceUserId: String, intId: String,
callerIdName: String, callerIdNum: String, muted: Boolean,
talking: Boolean, callingWith: String,
calledInto: String // freeswitch, kms
)
calledInto: String, // freeswitch, kms
hold: Boolean,
uuid: String)
case class ConfVoiceRecording(recordPath: String, recordStartTime: Long)
/**
@ -401,7 +402,9 @@ case class UserJoinedVoiceConfEvtMsg(
) extends VoiceStandardMsg
case class UserJoinedVoiceConfEvtMsgBody(voiceConf: String, voiceUserId: String, intId: String,
callerIdName: String, callerIdNum: String, muted: Boolean,
talking: Boolean, callingWith: String)
talking: Boolean, callingWith: String,
hold: Boolean,
uuid: String)
/**
* Sent to client that a user has joined the voice conference.
@ -639,3 +642,64 @@ case class GetMicrophonePermissionRespMsgBody(
sfuSessionId: String,
allowed: Boolean
)
/**
* Sent to FS to hold an audio channel
*/
object HoldChannelInVoiceConfSysMsg { val NAME = "HoldChannelInVoiceConfSysMsg" }
case class HoldChannelInVoiceConfSysMsg(
header: BbbCoreHeaderWithMeetingId,
body: HoldChannelInVoiceConfSysMsgBody
) extends BbbCoreMsg
case class HoldChannelInVoiceConfSysMsgBody(
voiceConf: String,
uuid: String,
hold: Boolean
)
/**
* Received from FS that the user channel hold state has changed
*/
object ChannelHoldChangedVoiceConfEvtMsg { val NAME = "ChannelHoldChangedVoiceConfEvtMsg" }
case class ChannelHoldChangedVoiceConfEvtMsg(
header: BbbCoreVoiceConfHeader,
body: ChannelHoldChangedVoiceConfEvtMsgBody
) extends VoiceStandardMsg
case class ChannelHoldChangedVoiceConfEvtMsgBody(
voiceConf: String,
intId: String,
uuid: String,
hold: Boolean
)
/**
* Sent to bbb-webrtc-sfu to request for userId's microphone connection
* to be toggled between bidirectional and unidirectional (listen only) modes
* (enabled = unidirectional, listen only, !enabled = bidirectional);
*/
object ToggleListenOnlyModeSysMsg { val NAME = "ToggleListenOnlyModeSysMsg" }
case class ToggleListenOnlyModeSysMsg(
header: BbbCoreHeaderWithMeetingId,
body: ToggleListenOnlyModeSysMsgBody
) extends BbbCoreMsg
case class ToggleListenOnlyModeSysMsgBody(
voiceConf: String,
userId: String,
enabled: Boolean
)
/**
* Sent from bbb-webrtc-sfu to indicate that userId's microphone channel switched
* modes (enabled = unidirectional, listen only, !enabled = bidirectional);
*/
object ListenOnlyModeToggledInSfuEvtMsg { val NAME = "ListenOnlyModeToggledInSfuEvtMsg" }
case class ListenOnlyModeToggledInSfuEvtMsg(
header: BbbCoreVoiceConfHeader,
body: ListenOnlyModeToggledInSfuEvtMsgBody
) extends VoiceStandardMsg
case class ListenOnlyModeToggledInSfuEvtMsgBody(
meetingId: String,
voiceConf: String,
userId: String,
enabled: Boolean
)

View File

@ -112,5 +112,6 @@ libraryDependencies ++= Seq(
"com.zaxxer" % "HikariCP" % "4.0.3",
"commons-validator" % "commons-validator" % "1.7",
"org.apache.tika" % "tika-core" % "2.8.0",
"org.apache.tika" % "tika-parsers-standard-package" % "2.8.0"
"org.apache.tika" % "tika-parsers-standard-package" % "2.8.0",
"org.scala-lang.modules" %% "scala-xml" % "2.2.0"
)

View File

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

View File

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

View File

@ -468,17 +468,22 @@ CREATE TABLE "user_voice" (
"startTime" bigint
);
--CREATE INDEX "idx_user_voice_userId" ON "user_voice"("userId");
ALTER TABLE "user_voice" ADD COLUMN "hideTalkingIndicatorAt" timestamp with time zone GENERATED ALWAYS AS (to_timestamp((COALESCE("endTime","startTime") + 6000) / 1000)) STORED;
CREATE INDEX "idx_user_voice_userId_talking" ON "user_voice"("userId","hideTalkingIndicatorAt","startTime");
ALTER TABLE "user_voice" ADD COLUMN "hideTalkingIndicatorAt" timestamp with time zone
GENERATED ALWAYS AS (to_timestamp((COALESCE("endTime","startTime") + 6000) / 1000)) STORED;
CREATE INDEX "idx_user_voice_userId_talking" ON "user_voice"("userId","talking");
CREATE INDEX "idx_user_voice_userId_hideTalkingIndicatorAt" ON "user_voice"("userId","hideTalkingIndicatorAt");
CREATE OR REPLACE VIEW "v_user_voice" AS
SELECT
u."meetingId",
"user_voice" .*,
greatest(coalesce(user_voice."startTime", 0), coalesce(user_voice."endTime", 0)) AS "lastSpeakChangedAt",
case when "hideTalkingIndicatorAt" > current_timestamp then true else false end "showTalkingIndicator"
user_talking."userId" IS NOT NULL "showTalkingIndicator"
FROM "user" u
JOIN "user_voice" ON u."userId" = "user_voice"."userId";
JOIN "user_voice" ON "user_voice"."userId" = u."userId"
LEFT JOIN "user_voice" user_talking ON (user_talking."userId" = u."userId" and user_talking."talking" IS TRUE)
OR (user_talking."userId" = u."userId" and user_talking."hideTalkingIndicatorAt" > now());
CREATE TABLE "user_camera" (
"streamId" varchar(100) PRIMARY KEY,
@ -1276,6 +1281,53 @@ JOIN "v_meeting_breakoutPolicies"vmbp using("meetingId")
JOIN "breakoutRoom" br ON br."parentMeetingId" = vmbp."parentId" AND br."externalId" = m."extId";
------------------------------------
----sharedNotes
create table "sharedNotes" (
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
"sharedNotesExtId" varchar(25),
"padId" varchar(25),
"model" varchar(25),
"name" varchar(25),
"pinned" boolean,
constraint "pk_sharedNotes" primary key ("meetingId", "sharedNotesExtId")
);
create table "sharedNotes_rev" (
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
"sharedNotesExtId" varchar(25),
"rev" integer,
"userId" varchar(50) references "user"("userId") ON DELETE SET NULL,
"changeset" varchar(25),
"start" integer,
"end" integer,
"diff" TEXT,
"createdAt" timestamp with time zone,
constraint "pk_sharedNotes_rev" primary key ("meetingId", "sharedNotesExtId", "rev")
);
--create view "v_sharedNotes_rev" as select * from "sharedNotes_rev";
create table "sharedNotes_session" (
"meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE,
"sharedNotesExtId" varchar(25),
"userId" varchar(50) references "user"("userId") ON DELETE CASCADE,
"sessionId" varchar(50),
constraint "pk_sharedNotes_session" primary key ("meetingId", "sharedNotesExtId", "userId")
);
create index "sharedNotes_session_userId" on "sharedNotes_session"("userId");
create view "v_sharedNotes" as
SELECT sn.*, max(snr.rev) "lastRev"
FROM "sharedNotes" sn
LEFT JOIN "sharedNotes_rev" snr ON snr."meetingId" = sn."meetingId" AND snr."sharedNotesExtId" = sn."sharedNotesExtId"
GROUP BY sn."meetingId", sn."sharedNotesExtId";
create view "v_sharedNotes_session" as
SELECT sns.*, sn."padId"
FROM "sharedNotes_session" sns
JOIN "sharedNotes" sn ON sn."meetingId" = sns."meetingId" AND sn."sharedNotesExtId" = sn."sharedNotesExtId";
----------------------
CREATE OR REPLACE VIEW "v_current_time" AS

View File

@ -0,0 +1,21 @@
table:
name: v_sharedNotes
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: sharedNotes
custom_root_fields: {}
select_permissions:
- role: bbb_client
permission:
columns:
- lastRev
- model
- name
- padId
- pinned
- sharedNotesExtId
filter:
meetingId:
_eq: X-Hasura-MeetingId

View File

@ -0,0 +1,29 @@
table:
name: v_sharedNotes_session
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_name: sharedNotes_session
custom_root_fields: {}
object_relationships:
- name: sharedNotes
using:
manual_configuration:
column_mapping:
meetingId: meetingId
sharedNotesExtId: sharedNotesExtId
insertion_order: null
remote_table:
name: v_sharedNotes
schema: public
select_permissions:
- role: bbb_client
permission:
columns:
- padId
- sessionId
- sharedNotesExtId
filter:
userId:
_eq: X-Hasura-UserId

View File

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

View File

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

View File

@ -1,15 +1,13 @@
- "!include public_v_chat_user.yaml"
- "!include public_v_meeting.yaml"
- "!include public_v_user_connectionStatus.yaml"
- "!include public_v_user_localSettings.yaml"
- "!include public_v_breakoutRoom.yaml"
- "!include public_v_breakoutRoom_assignedUser.yaml"
- "!include public_v_breakoutRoom_participant.yaml"
- "!include public_v_chat.yaml"
- "!include public_v_chat_message_private.yaml"
- "!include public_v_chat_message_public.yaml"
- "!include public_v_chat_user.yaml"
- "!include public_v_current_time.yaml"
- "!include public_v_external_video.yaml"
- "!include public_v_meeting.yaml"
- "!include public_v_meeting_breakoutPolicies.yaml"
- "!include public_v_meeting_group.yaml"
- "!include public_v_meeting_lockSettings.yaml"
@ -30,14 +28,18 @@
- "!include public_v_pres_page_writers.yaml"
- "!include public_v_pres_presentation.yaml"
- "!include public_v_screenshare.yaml"
- "!include public_v_sharedNotes.yaml"
- "!include public_v_sharedNotes_session.yaml"
- "!include public_v_timer.yaml"
- "!include public_v_user.yaml"
- "!include public_v_user_breakoutRoom.yaml"
- "!include public_v_user_camera.yaml"
- "!include public_v_user_connectionStatus.yaml"
- "!include public_v_user_connectionStatusReport.yaml"
- "!include public_v_user_current.yaml"
- "!include public_v_user_customParameter.yaml"
- "!include public_v_user_guest.yaml"
- "!include public_v_user_localSettings.yaml"
- "!include public_v_user_reaction.yaml"
- "!include public_v_user_reaction_current.yaml"
- "!include public_v_user_ref.yaml"

View File

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

View File

@ -1 +1 @@
git clone --branch v0.3.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder
git clone --branch v0.4.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder

View File

@ -1 +1 @@
git clone --branch v2.10.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu
git clone --branch v2.11.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu

View File

@ -366,6 +366,10 @@ start_bigbluebutton () {
echo "Starting BigBlueButton"
systemctl mask bbb-rap-resque-worker
systemctl mask bbb-rap-starter
systemctl mask bbb-rap-caption-inbox
systemctl restart bigbluebutton.target
if [ -f /usr/lib/systemd/system/bbb-html5.service ]; then

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
getMappedFallbackStun,
} from '/imports/utils/fetchStunTurnServers';
import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
import getFromUserSettings from '/imports/ui/services/users-settings';
import browserInfo from '/imports/utils/browserInfo';
import {
getAudioSessionNumber,
@ -26,6 +27,8 @@ const MEDIA = Meteor.settings.public.media;
const DEFAULT_FULLAUDIO_MEDIA_SERVER = MEDIA.audio.fullAudioMediaServer;
const RETRY_THROUGH_RELAY = MEDIA.audio.retryThroughRelay || false;
const LISTEN_ONLY_OFFERING = MEDIA.listenOnlyOffering;
const FULLAUDIO_OFFERING = MEDIA.fullAudioOffering;
const TRANSPARENT_LISTEN_ONLY = MEDIA.transparentListenOnly;
const MEDIA_TAG = MEDIA.mediaTag.replace(/#/g, '');
const CONNECTION_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 15000;
const { audio: NETWORK_PRIORITY } = MEDIA.networkPriorities || {};
@ -71,7 +74,18 @@ const getMediaServerAdapter = (listenOnly = false) => {
);
};
const isTransparentListenOnlyEnabled = () => getFromUserSettings(
'bbb_transparent_listen_only',
TRANSPARENT_LISTEN_ONLY,
);
export default class SFUAudioBridge extends BaseAudioBridge {
static getOfferingRole(isListenOnly) {
return isListenOnly
? LISTEN_ONLY_OFFERING
: (!isTransparentListenOnlyEnabled() && FULLAUDIO_OFFERING);
}
constructor(userData) {
super();
this.userId = userData.userId;
@ -266,6 +280,11 @@ export default class SFUAudioBridge extends BaseAudioBridge {
},
}, 'SFU audio media play failed due to autoplay error');
this.dispatchAutoplayHandlingEvent(mediaElement);
// For connection purposes, this worked - the autoplay thing is a client
// side soft issue to be handled at the UI/UX level, not WebRTC/negotiation
// So: clear the connection timer
this.clearConnectionTimeout();
this.reconnecting = false;
} else {
const normalizedError = {
errorCode: 1004,
@ -320,12 +339,13 @@ export default class SFUAudioBridge extends BaseAudioBridge {
constraints: getAudioConstraints({ deviceId: this.inputDeviceId }),
forceRelay: _forceRelay || shouldForceRelay(),
stream: (inputStream && inputStream.active) ? inputStream : undefined,
offering: isListenOnly ? LISTEN_ONLY_OFFERING : true,
offering: SFUAudioBridge.getOfferingRole(this.isListenOnly),
signalCandidates: SIGNAL_CANDIDATES,
traceLogs: TRACE_LOGS,
networkPriority: NETWORK_PRIORITY,
mediaStreamFactory: this.mediaStreamFactory,
gatheringTimeout: GATHERING_TIMEOUT,
transparentListenOnly: isTransparentListenOnlyEnabled(),
};
this.broker = new AudioBroker(

View File

@ -1,6 +1,7 @@
import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
import RegexWebUrl from '/imports/utils/regex-weburl';
import { BREAK_LINE } from '/imports/utils/lineEndings';
const MSG_DIRECT_TYPE = 'DIRECT';
const NODE_USER = 'nodeJSapp';
@ -25,9 +26,29 @@ export const parseMessage = (message) => {
// Replace flash links to flash valid ones
parsedMessage = parsedMessage.replace(RegexWebUrl, "<a href='event:$&'><u>$&</u></a>");
// Replace flash links to html valid ones
parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\'');
parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="');
// Replace \r and \n to <br/>
parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
return parsedMessage;
};
export const textToMarkdown = (message) => {
let parsedMessage = message || '';
parsedMessage = parsedMessage.trim();
// replace url with markdown links
const urlRegex = /(?<!\]\()https?:\/\/([\w-]+\.)+\w{1,6}([/?=&#.]?[\w-]+)*/gm;
parsedMessage = parsedMessage.replace(urlRegex, '[$&]($&)');
// replace new lines with markdown new lines
parsedMessage = parsedMessage.replace(/\n\r?/g, ' \n');
return parsedMessage;
};
export const spokeTimeoutHandles = {};
export const clearSpokeTimeout = (meetingId, userId) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,8 +51,10 @@ const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButt
const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
const isReactionsButtonEnabled = () => {
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled;
return getFromUserSettings('enable-reactions-button', REACTIONS_BUTTON_ENABLED);
return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED;
};
export default withTracker(() => ({

View File

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

View File

@ -2,11 +2,11 @@ import React, { useState } from 'react';
import { defineMessages } from 'react-intl';
import PropTypes from 'prop-types';
import BBBMenu from '/imports/ui/components/common/menu/component';
import ReactionsBar from '/imports/ui/components/emoji-picker/reactions-bar/component';
import UserReactionService from '/imports/ui/components/user-reaction/service';
import UserListService from '/imports/ui/components/user-list/service';
import { Emoji } from 'emoji-mart';
import Styled from '../styles';
import Styled from './styles';
const ReactionsButton = (props) => {
const {
@ -16,6 +16,7 @@ const ReactionsButton = (props) => {
raiseHand,
isMobile,
currentUserReaction,
autoCloseReactionsBar,
} = props;
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@ -25,6 +26,14 @@ const ReactionsButton = (props) => {
id: 'app.actionsBar.reactions.reactionsButtonLabel',
description: 'reactions Label',
},
raiseHandLabel: {
id: 'app.actionsBar.reactions.raiseHand',
description: 'raise Hand Label',
},
notRaiseHandLabel: {
id: 'app.actionsBar.reactions.lowHand',
description: 'not Raise Hand Label',
},
});
const handleClose = () => {
@ -43,17 +52,80 @@ const ReactionsButton = (props) => {
UserListService.setUserRaiseHand(userId, !raiseHand);
};
const renderReactionsBar = () => (
<Styled.Wrapper>
<ReactionsBar
{...props}
onReactionSelect={handleReactionSelect}
onRaiseHand={handleRaiseHandButtonClick}
/>
</Styled.Wrapper>
);
const RaiseHandButtonLabel = () => {
if (isMobile) return null;
const customStyles = { top: '-1rem', borderRadius: '1.7rem' };
return raiseHand
? intl.formatMessage(intlMessages.notRaiseHandLabel)
: intl.formatMessage(intlMessages.raiseHandLabel);
};
const customStyles = {
top: '-1rem',
borderRadius: '1.7rem',
};
const actionCustomStyles = {
paddingLeft: 0,
paddingRight: 0,
paddingTop: isMobile ? '0' : '0.5rem',
paddingBottom: isMobile ? '0' : '0.5rem',
};
const emojiProps = {
native: true,
size: '1.5rem',
padding: '4px',
};
const reactions = [
{
id: 'smiley',
native: '😃',
},
{
id: 'neutral_face',
native: '😐',
},
{
id: 'slightly_frowning_face',
native: '🙁',
},
{
id: '+1',
native: '👍',
},
{
id: '-1',
native: '👎',
},
{
id: 'clap',
native: '👏',
},
];
let actions = [];
reactions.forEach(({ id, native }) => {
actions.push({
label: <Styled.ButtonWrapper active={currentUserReaction === native}><Emoji key={id} emoji={{ id }} {...emojiProps} /></Styled.ButtonWrapper>,
key: id,
onClick: () => handleReactionSelect(native),
customStyles: actionCustomStyles,
});
});
actions.push({
label: <Styled.RaiseHandButtonWrapper isMobile={isMobile} data-test={raiseHand ? 'lowerHandBtn' : 'raiseHandBtn'} active={raiseHand}><Emoji key="hand" emoji={{ id: 'hand' }} {...emojiProps} />{RaiseHandButtonLabel()}</Styled.RaiseHandButtonWrapper>,
key: 'hand',
onClick: () => handleRaiseHandButtonClick(),
customStyles: {...actionCustomStyles, width: 'auto'},
});
const icon = currentUserReaction === 'none' ? 'hand' : null;
const currentUserReactionEmoji = reactions.find(({ native }) => native === currentUserReaction);
const customIcon = !icon ? <Emoji key={currentUserReactionEmoji?.id} emoji={{ id: currentUserReactionEmoji?.id }} {...emojiProps} /> : null;
return (
<BBBMenu
@ -61,26 +133,31 @@ const ReactionsButton = (props) => {
<Styled.ReactionsDropdown>
<Styled.RaiseHandButton
data-test="reactionsButton"
icon="hand"
icon={icon}
customIcon={customIcon}
label={intl.formatMessage(intlMessages.reactionsLabel)}
description="Reactions"
ghost={!showEmojiPicker}
ghost={!showEmojiPicker && !customIcon}
onKeyPress={() => {}}
onClick={() => setShowEmojiPicker(true)}
color={showEmojiPicker ? 'primary' : 'default'}
color={showEmojiPicker || customIcon ? 'primary' : 'default'}
hideLabel
circle
size="lg"
/>
</Styled.ReactionsDropdown>
)}
renderOtherComponents={showEmojiPicker ? renderReactionsBar() : null}
actions={actions}
onCloseCallback={() => handleClose()}
customAnchorEl={!isMobile ? actionsBarRef.current : null}
customStyles={customStyles}
open={showEmojiPicker}
hasRoundedCorners
overrideMobileStyles
isHorizontal={!isMobile}
isMobile={isMobile}
roundButtons={true}
keepOpen={!autoCloseReactionsBar}
opts={{
id: 'reactions-dropdown-menu',
keepMounted: true,

View File

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

View File

@ -0,0 +1,88 @@
import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import Button from '/imports/ui/components/common/button/component';
import {
colorGrayDark,
colorGrayLightest,
btnPrimaryColor,
btnPrimaryActiveBg,
} from '/imports/ui/stylesheets/styled-components/palette';
const RaiseHandButton = styled(Button)`
${({ ghost }) => ghost && `
& > span {
box-shadow: none;
background-color: transparent !important;
border-color: ${colorWhite} !important;
}
`}
`;
const ReactionsDropdown = styled.div`
position: relative;
`;
const ButtonWrapper = styled.div`
border: 1px solid transparent;
cursor: pointer;
height: 2.5rem;
display: flex;
align-items: center;
border-radius: 50%;
margin: 0 .5rem;
&:focus {
background-color: ${colorGrayDark};
}
& > button {
cursor: pointer;
flex: auto;
}
& > * > span {
padding: 4px;
}
${({ active }) => active && `
color: ${btnPrimaryColor};
background-color: ${btnPrimaryActiveBg};
&:hover{
filter: brightness(90%);
color: ${btnPrimaryColor};
background-color: ${btnPrimaryActiveBg} !important;
}
`}
`;
const RaiseHandButtonWrapper = styled(ButtonWrapper)`
width: 2.5rem;
border-radius: 1.7rem;
${({ isMobile }) => !isMobile && `
border: 1px solid ${colorGrayLightest};
padding: 1rem 0.5rem;
width: auto;
`}
${({ active }) => active && `
color: ${btnPrimaryColor};
background-color: ${btnPrimaryActiveBg};
&:hover{
filter: brightness(90%);
color: ${btnPrimaryColor};
background-color: ${btnPrimaryActiveBg} !important;
}
`}
`;
export default {
RaiseHandButton,
ReactionsDropdown,
ButtonWrapper,
RaiseHandButtonWrapper,
};

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState, useMemo } from "react";
import { Meteor } from "meteor/meteor";
import { makeVar, useMutation } from "@apollo/client";
import { LAST_SEEN_MUTATION } from "./queries";
@ -6,6 +6,7 @@ import {
ButtonLoadMore,
MessageList,
MessageListWrapper,
UnreadButton,
} from "./styles";
import { layoutSelect } from "../../../layout/context";
import ChatListPage from "./page/component";
@ -30,6 +31,10 @@ const intlMessages = defineMessages({
id: 'app.chat.loadMoreButtonLabel',
description: 'Label for load more button',
},
moreMessages: {
id: 'app.chat.moreMessages',
description: 'Chat message when the user has unread messages below the scroll',
},
});
interface ChatListProps {
@ -97,15 +102,18 @@ const ChatMessageList: React.FC<ChatListProps> = ({
chatId,
setMessageAsSeenMutation,
lastSeenAt,
totalUnread,
}) => {
const intl = useIntl();
const messageListRef = React.useRef<HTMLDivElement>();
const contentRef = React.useRef<HTMLDivElement>();
// I used a ref here because I don't want to re-render the component when the last sender changes
const lastSenderPerPage = React.useRef<Map<number, string>>(new Map());
const messagesEndRef = React.useRef<HTMLDivElement>();
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
const [lastMessageCreatedTime, setLastMessageCreatedTime] = useState<number>(0);
const [followingTail, setFollowingTail] = React.useState(true);
useEffect(() => {
setter({
...setter(),
@ -167,6 +175,24 @@ const ChatMessageList: React.FC<ChatListProps> = ({
}
};
const renderUnreadNotification = useMemo(() => {
if (totalUnread && !followingTail) {
return (
<UnreadButton
aria-hidden="true"
color="primary"
size="sm"
key="unread-messages"
label={intl.formatMessage(intlMessages.moreMessages)}
onClick={() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}}
/>
);
}
return null;
}, [totalUnread, followingTail]);
useEffect(() => {
const setScrollToTailEventHandler = () => {
if (scrollObserver && contentRef.current) {
@ -209,66 +235,70 @@ const ChatMessageList: React.FC<ChatListProps> = ({
? userLoadedBackUntilPage : Math.max(totalPages - 2, 0);
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
return (
<MessageListWrapper>
<MessageList
ref={messageListRef}
onWheel={(e) => {
if (e.deltaY < 0) {
if (isElement(contentRef.current) && followingTail) {
toggleFollowingTail(false)
[
<MessageListWrapper>
<MessageList
ref={messageListRef}
onWheel={(e) => {
if (e.deltaY < 0) {
if (isElement(contentRef.current) && followingTail) {
toggleFollowingTail(false)
}
} else if (e.deltaY > 0) {
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
}
} else if (e.deltaY > 0) {
}}
onMouseUp={() => {
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
}
}}
onMouseUp={() => {
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
}}
onTouchEnd={() => {
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
}}
>
<span>
{
(userLoadedBackUntilPage)
? (
<ButtonLoadMore
onClick={() => {
if (followingTail) {
toggleFollowingTail(false);
}}
onTouchEnd={() => {
setScrollToTailEventHandler(messageListRef.current as HTMLDivElement);
}}
>
<span>
{
(userLoadedBackUntilPage)
? (
<ButtonLoadMore
onClick={() => {
if (followingTail) {
toggleFollowingTail(false);
}
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
}
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
}
}
>
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
</ButtonLoadMore>
) : null
}
</span>
<div id="contentRef" ref={contentRef}>
<ChatPopupContainer />
{
// @ts-ignore
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
return (
<ChatListPage
key={`page-${page}`}
page={page}
pageSize={PAGE_SIZE}
setLastSender={setLastSender(lastSenderPerPage.current)}
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
chatId={chatId}
markMessageAsSeen={markMessageAsSeen}
scrollRef={messageListRef}
lastSeenAt={lastSeenAt}
/>
)
})
}
</div>
</MessageList>
</MessageListWrapper >
}
>
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
</ButtonLoadMore>
) : null
}
</span>
<div id="contentRef" ref={contentRef}>
<ChatPopupContainer />
{
// @ts-ignore
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
return (
<ChatListPage
key={`page-${page}`}
page={page}
pageSize={PAGE_SIZE}
setLastSender={setLastSender(lastSenderPerPage.current)}
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
chatId={chatId}
markMessageAsSeen={markMessageAsSeen}
scrollRef={messageListRef}
lastSeenAt={lastSeenAt}
/>
)
})
}
</div>
<div ref={messagesEndRef} />
</MessageList>
</MessageListWrapper >,
renderUnreadNotification,
]
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,23 @@
import styled, { css } from 'styled-components';
import styled from 'styled-components';
import {
userIndicatorsOffset,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorWhite,
userListBg,
colorSuccess,
colorHeading,
palettePlaceholderText,
colorGrayLight,
} from '/imports/ui/stylesheets/styled-components/palette';
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
interface ChatUserNameProps {
isOnline: boolean;
}
export const HeaderContent = styled.div`
display: flex;
flex-flow: row;
width: 100%;
`;
export const ChatUserName = styled.div`
export const ChatUserName = styled.div<ChatUserNameProps>`
display: flex;
min-width: 0;
font-weight: 600;
@ -83,91 +81,6 @@ export const ChatTime = styled.time`
}
`;
export const ChatAvatar = styled.div`
flex: 0 0 2.25rem;
margin: 0px calc(0.5rem) 0px 0px;
box-flex: 0;
position: relative;
height: 2.25rem;
width: 2.25rem;
border-radius: 50%;
text-align: center;
font-size: .85rem;
border: 2px solid transparent;
user-select: none;
${({ color }) => css`
background-color: ${color};
`}
}
&:after,
&:before {
content: "";
position: absolute;
width: 0;
height: 0;
padding-top: .5rem;
padding-right: 0;
padding-left: 0;
padding-bottom: 0;
color: inherit;
top: auto;
left: auto;
bottom: ${userIndicatorsOffset};
right: ${userIndicatorsOffset};
border: 1.5px solid ${userListBg};
border-radius: 50%;
background-color: ${colorSuccess};
color: ${colorWhite};
opacity: 0;
font-family: 'bbb-icons';
font-size: .65rem;
line-height: 0;
text-align: center;
vertical-align: middle;
letter-spacing: -.65rem;
z-index: 1;
[dir="rtl"] & {
left: ${userIndicatorsOffset};
right: auto;
padding-right: .65rem;
padding-left: 0;
}
}
${({ moderator }) =>
moderator &&
`
border-radius: 5px;
`}
// ================ image ================
${({ avatar, emoji }) =>
avatar?.length !== 0 &&
!emoji &&
css`
background-image: url(${avatar});
background-repeat: no-repeat;
background-size: contain;
`}
// ================ image ================
// ================ content ================
color: ${colorWhite};
font-size: 110%;
text-transform: capitalize;
display: flex;
justify-content: center;
align-items:center;
// ================ content ================
& .react-loading-skeleton {
height: 2.25rem;
width: 2.25rem;
}
`;
export const ChatHeaderText = styled.div`
display: flex;
align-items: baseline;
@ -176,7 +89,6 @@ export const ChatHeaderText = styled.div`
export default {
HeaderContent,
ChatAvatar,
ChatTime,
ChatUserOffline,
ChatUserName,

View File

@ -1,20 +1,36 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import {
borderSize,
userIndicatorsOffset,
} from '/imports/ui/stylesheets/styled-components/general';
import {
lineHeightComputed,
fontSizeBase,
} 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;
[dir='rtl'] & {
direction: rtl;
}
display: flex;
flex-flow: column;
flex-flow: row;
position: relative;
${({ sameSender }) =>
sameSender &&
@ -34,8 +50,100 @@ export const ChatWrapper = styled.div`
font-size: ${fontSizeBase};
`;
export const ChatContent = styled.div`
export const ChatContent = styled.div<ChatContentProps>`
display: flex;
flex-flow: column;
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