diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b38269cbc7a..cb5b260d06e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,7 +171,7 @@ jobs: # Pull requests show details of coverage changes - name: Get jest tests coverage of main from gist if: ${{ github.event_name == 'pull_request' && !startsWith(github.event.pull_request.base.ref, 'release/') }} - run: curl -o temp/jest/base/coverage-report.json "https://gist.github.com/communication-ui-bot/${{ matrix.coverage_gist_id }}/raw/communication-react-jest-report-${{ matrix.flavor }}.json" --create-dirs -L + run: curl -o temp/jest/base/coverage-report.json "https://gist.github.com/alkwa-msft/${{ matrix.coverage_gist_id }}/raw/communication-react-jest-report-${{ matrix.flavor }}.json" --create-dirs -L - name: Calculate coverage change if: ${{ github.event_name == 'pull_request' && !startsWith(github.event.pull_request.base.ref, 'release/') }} id: coverage @@ -787,11 +787,11 @@ jobs: include: # These are gist to track bundle sizes - app: Chat - gist: https://gist.github.com/communication-ui-bot/db13fa7067b083b7a91d7a95adf28be1 + gist: https://gist.github.com/alkwa-msft/f19649d941b2739b13402ee4802aa851 - app: Calling - gist: https://gist.github.com/communication-ui-bot/87d57b63bbbaee12273a9a901b67885c + gist: https://gist.github.com/alkwa-msft/8160c0362cb05b7b09f3dbf803007c73 - app: CallWithChat - gist: https://gist.github.com/communication-ui-bot/72a7fca0af8a3c5b37f966bb6d4bcd11 + gist: https://gist.github.com/alkwa-msft/c43393ae31b5b08215f3f49ed327d912 steps: # checkout base bundle stats - name: Get bundle stats of main from gist @@ -867,11 +867,11 @@ jobs: include: # These are gist to track bundle sizes - app: Chat - gist_id: db13fa7067b083b7a91d7a95adf28be1 + gist_id: f19649d941b2739b13402ee4802aa851 - app: Calling - gist_id: 87d57b63bbbaee12273a9a901b67885c + gist_id: 8160c0362cb05b7b09f3dbf803007c73 - app: CallWithChat - gist_id: 72a7fca0af8a3c5b37f966bb6d4bcd11 + gist_id: c43393ae31b5b08215f3f49ed327d912 steps: - uses: actions/download-artifact@v4 with: diff --git a/change-beta/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json b/change-beta/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json new file mode 100644 index 00000000000..c2ed125b180 --- /dev/null +++ b/change-beta/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "feature", + "workstream": "Hard mute", + "comment": "We are excited to announce that the Azure Communication Services Web UI Library now supports the Media Access feature. This feature allows organizers, co-organizers, and presenters to control the ability of other attendees to send audio and video while in a call. Attendees can determine if their audio or video is enabled or disabled and check the media access status of other participants. Developers can integrate this functionality through our composites (e.g., CallComposite, CallWithChatComposite) as well as through components.", + "packageName": "@azure/communication-react", + "email": "fuyan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change-beta/@azure-communication-react-4bc858eb-45eb-4062-b228-7107a452688d.json b/change-beta/@azure-communication-react-4bc858eb-45eb-4062-b228-7107a452688d.json new file mode 100644 index 00000000000..1452cb86128 --- /dev/null +++ b/change-beta/@azure-communication-react-4bc858eb-45eb-4062-b228-7107a452688d.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "area": "feature", + "workstream": "together-mode", + "comment": "together mode component", + "packageName": "@azure/communication-react", + "dependentChangeType": "patch" +} diff --git a/change-beta/@azure-communication-react-a7e8be69-cfac-4bc0-99b4-883defecf226.json b/change-beta/@azure-communication-react-a7e8be69-cfac-4bc0-99b4-883defecf226.json new file mode 100644 index 00000000000..da81f4fc772 --- /dev/null +++ b/change-beta/@azure-communication-react-a7e8be69-cfac-4bc0-99b4-883defecf226.json @@ -0,0 +1,9 @@ +{ + "type": "prerelease", + "area": "improvement", + "workstream": "Repairing Bundle Size Gist download/upload for CI", + "comment": "temporary change to get bundle size updates to work again", + "packageName": "@azure/communication-react", + "email": "79329532+alkwa-msft@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json b/change/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json new file mode 100644 index 00000000000..c2ed125b180 --- /dev/null +++ b/change/@azure-communication-react-34244f58-3cf8-44b0-9484-8a89802eff66.json @@ -0,0 +1,9 @@ +{ + "type": "minor", + "area": "feature", + "workstream": "Hard mute", + "comment": "We are excited to announce that the Azure Communication Services Web UI Library now supports the Media Access feature. This feature allows organizers, co-organizers, and presenters to control the ability of other attendees to send audio and video while in a call. Attendees can determine if their audio or video is enabled or disabled and check the media access status of other participants. Developers can integrate this functionality through our composites (e.g., CallComposite, CallWithChatComposite) as well as through components.", + "packageName": "@azure/communication-react", + "email": "fuyan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-communication-react-e4791dc2-7f02-4b0d-9147-ca8b52f51701.json b/change/@azure-communication-react-e4791dc2-7f02-4b0d-9147-ca8b52f51701.json new file mode 100644 index 00000000000..3d1f37618bd --- /dev/null +++ b/change/@azure-communication-react-e4791dc2-7f02-4b0d-9147-ca8b52f51701.json @@ -0,0 +1,9 @@ +{ + "type": "none", + "area": "improvement", + "workstream": "[Test Coverage] Adding additional unit tests for _isValidIdentifier and _toCommunicationIdentifier", + "comment": "Added on several unit tests to the acs-ui-common packlet", + "packageName": "@azure/communication-react", + "email": "alkwa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/common/config/babel/features.js b/common/config/babel/features.js index 82e2fd33ae6..94ce847e67e 100644 --- a/common/config/babel/features.js +++ b/common/config/babel/features.js @@ -78,7 +78,7 @@ module.exports = { // Demo feature. Used in live-documentation of conditional compilation. // Do not use in production code. "stabilizedDemo", - // Feature for forbid/permit remote participants audio/video access + // Feature for forbid/permit Teams meeting/group call attendee' audio/video access "media-access" ] } diff --git a/common/config/rush/variants/stable/common-versions.json b/common/config/rush/variants/stable/common-versions.json index bdf06c618ac..8555fb519d5 100644 --- a/common/config/rush/variants/stable/common-versions.json +++ b/common/config/rush/variants/stable/common-versions.json @@ -25,7 +25,7 @@ */ // "some-library": "1.2.3" // This is the version for stable build (please also update allowedAlternativeVersions below) - "@azure/communication-calling": "^1.31.2", + "@azure/communication-calling": "^1.32.1", "@azure/communication-common": "2.3.0", "@azure/communication-chat": "^1.5.4", "@azure/communication-signaling": "1.0.0-beta.29", @@ -58,7 +58,7 @@ */ "allowedAlternativeVersions": { // This is the version for stable build (please also update preferredVersions above) - "@azure/communication-calling": ["^1.31.2"], + "@azure/communication-calling": ["^1.32.1"], "@azure/communication-common": ["2.3.0"], "@azure/communication-chat": ["^1.5.4"], "@azure/communication-signaling": ["1.0.0-beta.29"], diff --git a/common/config/rush/variants/stable/pnpm-lock.yaml b/common/config/rush/variants/stable/pnpm-lock.yaml index 694ca6cdfa8..a27a72dd563 100644 --- a/common/config/rush/variants/stable/pnpm-lock.yaml +++ b/common/config/rush/variants/stable/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@azure/communication-calling': - specifier: ^1.31.2 - version: 1.31.2 + specifier: ^1.32.1 + version: 1.32.1 '@azure/communication-calling-effects': specifier: ^1.1.2 version: 1.1.2 @@ -152,8 +152,8 @@ packages: events: 3.3.0 dev: false - /@azure/communication-calling@1.31.2: - resolution: {integrity: sha512-Hvpeue+WwEpEp5v83jDec1carhOb7fp2+ywwJ8ZFoywOWmfCSXaLhu3bE6olnohTpmXc1UNjq5iutd7WYSpfAw==} + /@azure/communication-calling@1.32.1: + resolution: {integrity: sha512-YKt7BZVS48S97Iepj72r/vEG/USRU+qIQXoj+dQvHDdwdyU8vTAjvOpOdKcti9PfljDEpG9Js841n7nKU4GJpw==} dependencies: '@azure/communication-common': 2.3.0 '@azure/logger': 1.1.4 @@ -17992,11 +17992,11 @@ packages: dev: false file:projects/calling-component-bindings.tgz: - resolution: {integrity: sha512-bmkDBGd6quqjSkjGiHW+sh2pcBMdtizT3bLh+e8EpYBU1yzD+F08VoYVWLeWUtAKyHwAU9FcMLp06I2UQgEyKg==, tarball: file:projects/calling-component-bindings.tgz} + resolution: {integrity: sha512-hm+dXOUat8xsujs9sug/pLgVjwX0vpFc7sfUcPc0unxjJTvk6QH979N/FRAZ0YPl6Jw9LOqzt1+t87glGox8cw==, tarball: file:projects/calling-component-bindings.tgz} name: '@rush-temp/calling-component-bindings' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-calling-effects': 1.1.2 '@azure/communication-common': 2.3.1 '@babel/cli': 7.25.9(@babel/core@7.26.0) @@ -18039,11 +18039,11 @@ packages: dev: false file:projects/calling-stateful-client.tgz: - resolution: {integrity: sha512-qeGY7Dzfg2JhypxRj+WgqVNVNzVDXLySZF/0uIgHSAGqKtAiQz7HrIUF9kbKZxt3lyDo6ZoCVQW7Iz7fL3/kQA==, tarball: file:projects/calling-stateful-client.tgz} + resolution: {integrity: sha512-TKaBJEXNYTY/QJXplVfxAIFqbbXy9+N2DBvd4ABnBKqcHWHjJqHt+DeI6YPAopdJl5eLVwLel1C6nLpxxDHLXw==, tarball: file:projects/calling-stateful-client.tgz} name: '@rush-temp/calling-stateful-client' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-common': 2.3.1 '@azure/core-auth': 1.9.0 '@azure/logger': 1.1.4 @@ -18087,12 +18087,12 @@ packages: dev: false file:projects/calling-stateful-samples.tgz: - resolution: {integrity: sha512-ylQ7+ijsUxSSDmbE+6Ar5TiloHJ+EIEGDOi9K4I8fz88PPTMWkbp1DIo4wIZq76XEkDWh/cIHf6GB8DcBi8w1Q==, tarball: file:projects/calling-stateful-samples.tgz} + resolution: {integrity: sha512-eVrrh0OlhV8KXDcIdi3AhNfx+/8jzo1mUgmeZz+2WIp+YYlmvuASLR8RPIG+40l252KZ78uGErXayxw7pvw6ZA==, tarball: file:projects/calling-stateful-samples.tgz} name: '@rush-temp/calling-stateful-samples' version: 0.0.0 dependencies: '@azure/abort-controller': 1.1.0 - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 '@azure/logger': 1.1.4 @@ -18181,12 +18181,12 @@ packages: dev: false file:projects/calling.tgz: - resolution: {integrity: sha512-EP69o8RqqJGKw+kSa6xSZ7YzZ2fTqGDK3d/ORSvxvqVqo3BMpUrmrl3Yp7IBWO2Q3RHSlJkWKGUe98Jg7R0A7w==, tarball: file:projects/calling.tgz} + resolution: {integrity: sha512-fgmuiWZ4+TIYGMnpBqq7U5l1Ej5F0y0kwmY3d0QokLYxzv7CTpNhk4KWJERe7zEbycKsSKW27RrKZe9zL+VNsg==, tarball: file:projects/calling.tgz} name: '@rush-temp/calling' version: 0.0.0 dependencies: '@azure/abort-controller': 1.1.0 - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 '@azure/logger': 1.1.4 @@ -18275,12 +18275,12 @@ packages: dev: false file:projects/callwithchat.tgz: - resolution: {integrity: sha512-YhazbYTBTzzCqV+hv+lUsOb7mG4m9utUzdtszCer7tSPcmw0N6oj7I+YTlrYPHqpqyWeWDVyORdyd48+4EKdKg==, tarball: file:projects/callwithchat.tgz} + resolution: {integrity: sha512-vYJvvn11FcOgZvH8l2agSyNefKWOISRgqnrYCGaHMrYEXJInr+W2CY69pOxUnQ5uervpJLYE2F8nWSPskcuihw==, tarball: file:projects/callwithchat.tgz} name: '@rush-temp/callwithchat' version: 0.0.0 dependencies: '@azure/abort-controller': 1.1.0 - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 @@ -18598,11 +18598,11 @@ packages: dev: false file:projects/communication-react.tgz: - resolution: {integrity: sha512-BhHzqQkJvXFZqEaF/BOGZiegY8PBTEQ8Dr2F1kyVwXU8M3zalZp5k2usHqmoPscJXlpW5FSlXgkrsjR+/a+IhQ==, tarball: file:projects/communication-react.tgz} + resolution: {integrity: sha512-EoJtLJwYV6abvRkqFBHd8+g89Bd/GuDUhAA4YrGX273oyMNRMDfGeMuSutaeZHZm+AY3wmd9H3CIgspCs69QKA==, tarball: file:projects/communication-react.tgz} name: '@rush-temp/communication-react' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-calling-effects': 1.1.2 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 @@ -18703,11 +18703,11 @@ packages: dev: false file:projects/component-examples.tgz: - resolution: {integrity: sha512-v6VWjyfZ3iLzNyXZI8GymegU6LW3YPRKXhJdYXKY5ftMixa94EHhihQ3MM0OBQxe5zUjd5XJQsj/oQf3mJNr2w==, tarball: file:projects/component-examples.tgz} + resolution: {integrity: sha512-/oWU3RU0uE/WcYA6Ie8b8vsod6piIHY8FwRNardRk21Jj7we9newgMOUXN3Gh5n5JSQF6H6HoMKYwPxt5HfYtA==, tarball: file:projects/component-examples.tgz} name: '@rush-temp/component-examples' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 @@ -18973,11 +18973,11 @@ packages: dev: false file:projects/react-composites.tgz: - resolution: {integrity: sha512-3HDDRPtGnl5bIvxzF8CW0qxjhc1vWED3oojPlfm7Zg47/Nx+qq+vA0XcCsrtq4ln2m6roZj9PSiLwjzhXWL7vw==, tarball: file:projects/react-composites.tgz} + resolution: {integrity: sha512-GkpC8cOe/aCpPEsmg6WAHOfnYFFH7ES8DliRJSFOViCNLIFr4od4fFomGfYck4vfueLkBM3J6qgp1C0+a3srcA==, tarball: file:projects/react-composites.tgz} name: '@rush-temp/react-composites' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-calling-effects': 1.1.2 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 @@ -19092,11 +19092,11 @@ packages: dev: false file:projects/sample-automation-tests.tgz: - resolution: {integrity: sha512-QAk5eN8qoSzn0bg1c/kEVFUlz/uZy0ByRNWzaglQmVtMKEQQu7xvbnZ9y8+dZfRjyeQ2nq4nelK/CYYcuDLXXg==, tarball: file:projects/sample-automation-tests.tgz} + resolution: {integrity: sha512-YoVUeC6udOpkjAnDLSPb4gHS+A0TnkB6Znp6T6enY+Bh6J4uqHLyA9w23an39T5ZPE2Bdq2d92Y5IbtA6C+iPg==, tarball: file:projects/sample-automation-tests.tgz} name: '@rush-temp/sample-automation-tests' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 @@ -19116,11 +19116,11 @@ packages: dev: false file:projects/sample-static-html-composites.tgz: - resolution: {integrity: sha512-UtfNEWIDY60ZwuzurB0tCN0PvfHmIsVAByRMfWy2UWZ8jquYia1gPBZY102lYhNY6h4+T2zpYrBoA+fn5283vQ==, tarball: file:projects/sample-static-html-composites.tgz} + resolution: {integrity: sha512-Wn2ihvvL5pABcfWjlcftu9VW0JLks41D04706Yvw7YD8KRRTi9jg8xED5aUkTv6HR3UrUGlrWmXMS2X5okMRzw==, tarball: file:projects/sample-static-html-composites.tgz} name: '@rush-temp/sample-static-html-composites' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 @@ -19264,11 +19264,11 @@ packages: dev: false file:projects/storybook8.tgz: - resolution: {integrity: sha512-NSttU4/oYChcVytibGD9kjDnmrHYk7755QKLEnhZ850KYehrpojXUV97SoAlBu0HEHFJhy3LXtRIyKIKlUFNiw==, tarball: file:projects/storybook8.tgz} + resolution: {integrity: sha512-Jc6L2hBWHhRSnpqrqS6eNiTgbHzfLEOPZ1RoZhsKSZLxZEpJ9CL7BRRzyrb4wss18JkzJf1z4cjpS+79D8bK/g==, tarball: file:projects/storybook8.tgz} name: '@rush-temp/storybook8' version: 0.0.0 dependencies: - '@azure/communication-calling': 1.31.2 + '@azure/communication-calling': 1.32.1 '@azure/communication-chat': 1.5.4 '@azure/communication-common': 2.3.1 '@azure/communication-identity': 1.3.1 diff --git a/common/config/workflows/matrix.json b/common/config/workflows/matrix.json index bf1c2c50537..ecce5d085c4 100644 --- a/common/config/workflows/matrix.json +++ b/common/config/workflows/matrix.json @@ -2,11 +2,11 @@ "include": [ { "flavor": "stable", - "coverage_gist_id": "d1528e50153e6f7ea569040d7b437abb" + "coverage_gist_id": "19b238d46a84c28e44d689f0a8b57202" }, { "flavor": "beta", - "coverage_gist_id": "67224b1799c92f0e1cd2da24798e4dd5" + "coverage_gist_id": "aadc866e05b5243846b1c15a79ec1779" } ] diff --git a/packages/acs-ui-common/src/identifier.test.ts b/packages/acs-ui-common/src/identifier.test.ts index fb87e4034ac..7da0451f86e 100644 --- a/packages/acs-ui-common/src/identifier.test.ts +++ b/packages/acs-ui-common/src/identifier.test.ts @@ -7,7 +7,12 @@ import { isPhoneNumberIdentifier, isUnknownIdentifier } from '@azure/communication-common'; -import { fromFlatCommunicationIdentifier, toFlatCommunicationIdentifier } from './identifier'; +import { + fromFlatCommunicationIdentifier, + toFlatCommunicationIdentifier, + _toCommunicationIdentifier, + _isValidIdentifier +} from './identifier'; test('Communication user conversions', () => { const parsed = fromFlatCommunicationIdentifier('8:acs:OPAQUE'); @@ -94,3 +99,34 @@ test('Unknown user conversions', () => { }); expect(toFlatCommunicationIdentifier(parsed)).toEqual('OPAQUE'); }); + +test('toCommunicationIdentifier with communication identifier', () => { + const userId = { kind: 'communicationUser', communicationUserId: '8:acs:OPAQUE' }; + const identifierResponse = _toCommunicationIdentifier(userId); + expect(userId).toEqual(identifierResponse); + expect(identifierResponse).toEqual({ + kind: 'communicationUser', + communicationUserId: '8:acs:OPAQUE' + }); +}); + +test('toCommunicationIdentifier with communication identifier as string', () => { + const identifierResponse = _toCommunicationIdentifier('8:acs:OPAQUE'); + expect(isCommunicationUserIdentifier(identifierResponse)).toBeTruthy(); + expect(identifierResponse).toEqual({ + kind: 'communicationUser', + communicationUserId: '8:acs:OPAQUE' + }); +}); + +test('isValidIdentifier with communication identifier', () => { + const userId = { kind: 'communicationUser', communicationUserId: '8:acs:OPAQUE' }; + const isValid = _isValidIdentifier(userId); + expect(isValid).toBeTruthy(); +}); + +test('isValidIdentifier with unknown identifier', () => { + const userId = { kind: 'unknown', id: 'OPAQUE' }; + const isValid = _isValidIdentifier(userId); + expect(isValid).toBeTruthy(); +}); diff --git a/packages/calling-component-bindings/package.json b/packages/calling-component-bindings/package.json index 02d9e2f9f2e..6eaa5363780 100644 --- a/packages/calling-component-bindings/package.json +++ b/packages/calling-component-bindings/package.json @@ -39,13 +39,13 @@ "reselect": "^4.0.0" }, "peerDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-calling-effects": "^1.1.2", "@types/react": ">=16.8.0 <19.0.0", "react": ">=16.8.0 <19.0.0" }, "devDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-calling-effects": "^1.1.2", "@babel/cli": "^7.24.8", "@babel/core": "^7.25.2", diff --git a/packages/calling-stateful-client/package.json b/packages/calling-stateful-client/package.json index 59d69b33e17..e82987dca6f 100644 --- a/packages/calling-stateful-client/package.json +++ b/packages/calling-stateful-client/package.json @@ -38,10 +38,10 @@ "immer": "10.1.1" }, "peerDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2" + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1" }, "devDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/core-auth": "^1.7.2", "@babel/cli": "^7.24.8", "@babel/core": "^7.25.2", diff --git a/packages/communication-react/package.json b/packages/communication-react/package.json index 491317dfc2b..e70aa2eef59 100644 --- a/packages/communication-react/package.json +++ b/packages/communication-react/package.json @@ -55,7 +55,7 @@ "uuid": "^9.0.0" }, "peerDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-calling-effects": "^1.1.2", "@azure/communication-chat": "1.6.0-beta.3 || >=1.5.4", "@types/react": ">=16.8.0 <19.0.0", @@ -111,7 +111,7 @@ "_by-flavor": "rushx _current-flavor && env-cmd -f ../../common/config/env/.env --use-shell" }, "devDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-calling-effects": "^1.1.2", "@azure/core-auth": "^1.7.2", "@babel/cli": "^7.24.8", diff --git a/packages/communication-react/review/beta/communication-react.api.md b/packages/communication-react/review/beta/communication-react.api.md index 38316848a7d..37ca6051f0c 100644 --- a/packages/communication-react/review/beta/communication-react.api.md +++ b/packages/communication-react/review/beta/communication-react.api.md @@ -5499,7 +5499,7 @@ export interface VideoBackgroundReplacementEffect extends BackgroundReplacementC export const VideoGallery: (props: VideoGalleryProps) => JSX.Element; // @public (undocumented) -export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | 'focusedContent'; +export type VideoGalleryLayout = 'default' | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' | /* @conditional-compile-remove(together-mode) */ 'togetherMode' | 'focusedContent'; // @public export interface VideoGalleryLocalParticipant extends VideoGalleryParticipant { @@ -5525,6 +5525,8 @@ export type VideoGalleryParticipant = { // @public export interface VideoGalleryProps { dominantSpeakers?: string[]; + // (undocumented) + isTogetherModeActive?: boolean; layout?: VideoGalleryLayout; localParticipant: VideoGalleryLocalParticipant; localVideoCameraCycleButtonProps?: LocalVideoCameraCycleButtonProps; @@ -5534,12 +5536,16 @@ export interface VideoGalleryProps { maxRemoteVideoStreams?: number; onCreateLocalStreamView?: (options?: VideoStreamOptions) => Promise; onCreateRemoteStreamView?: (userId: string, options?: VideoStreamOptions) => Promise; + // (undocumented) + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; onDisposeLocalScreenShareStreamView?: () => Promise; onDisposeLocalStreamView?: () => void; onDisposeRemoteScreenShareStreamView?: (userId: string) => Promise; // @deprecated (undocumented) onDisposeRemoteStreamView?: (userId: string) => Promise; onDisposeRemoteVideoStreamView?: (userId: string) => Promise; + // (undocumented) + onDisposeTogetherModeStreamView?: () => Promise; onForbidAudio?: (userIds: string[]) => Promise; onForbidVideo?: (userIds: string[]) => Promise; onMuteParticipant?: (userId: string) => Promise; @@ -5549,8 +5555,11 @@ export interface VideoGalleryProps { onRenderAvatar?: OnRenderAvatarCallback; onRenderLocalVideoTile?: (localParticipant: VideoGalleryLocalParticipant) => JSX.Element; onRenderRemoteVideoTile?: (remoteParticipant: VideoGalleryRemoteParticipant) => JSX.Element; + // (undocumented) + onSetTogetherModeSceneSize?: (width: number, height: number) => void; onStartLocalSpotlight?: () => Promise; onStartRemoteSpotlight?: (userIds: string[]) => Promise; + onStartTogetherMode?: () => Promise; onStopLocalSpotlight?: () => Promise; onStopRemoteSpotlight?: (userIds: string[]) => Promise; onUnpinParticipant?: (userId: string) => void; @@ -5563,8 +5572,14 @@ export interface VideoGalleryProps { showCameraSwitcherInLocalPreview?: boolean; showMuteIndicator?: boolean; spotlightedParticipants?: string[]; + // (undocumented) + startTogetherModeEnabled?: boolean; strings?: Partial; styles?: VideoGalleryStyles; + // (undocumented) + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; + // (undocumented) + togetherModeStreams?: VideoGalleryTogetherModeStreams; videoTilesOptions?: VideoTilesOptions; } diff --git a/packages/react-components/src/components/MeetingReactionOverlay.tsx b/packages/react-components/src/components/MeetingReactionOverlay.tsx index 8566c089f38..68ee1276cd5 100644 --- a/packages/react-components/src/components/MeetingReactionOverlay.tsx +++ b/packages/react-components/src/components/MeetingReactionOverlay.tsx @@ -8,9 +8,15 @@ import { VideoGalleryLocalParticipant, VideoGalleryRemoteParticipant } from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { VideoGalleryTogetherModeParticipantPosition } from '../types'; import React, { useLayoutEffect, useRef, useState } from 'react'; import { ParticipantVideoTileOverlay } from './VideoGallery/ParticipantVideoTileOverlay'; import { RemoteContentShareReactionOverlay } from './VideoGallery/RemoteContentShareReactionOverlay'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeOverlay } from './TogetherModeOverlay'; +/* @conditional-compile-remove(together-mode) */ +import { togetherModeMeetingOverlayStyle } from './styles/TogetherMode.styles'; /** * Reaction overlay component props @@ -40,6 +46,9 @@ export interface MeetingReactionOverlayProps { * Remote participant's reaction event. */ remoteParticipants?: VideoGalleryRemoteParticipant[]; + + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatPositions?: VideoGalleryTogetherModeParticipantPosition; } /** @@ -68,7 +77,14 @@ const REACTION_EMOJI_RESIZE_SCALE_CONSTANT = 3; * @internal */ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX.Element => { - const { overlayMode, reaction, reactionResources, localParticipant, remoteParticipants } = props; + const { + overlayMode, + reaction, + reactionResources, + localParticipant, + remoteParticipants, + /* @conditional-compile-remove(together-mode) */ togetherModeSeatPositions + } = props; const [emojiSizePx, setEmojiSizePx] = useState(0); const [divHeight, setDivHeight] = useState(0); const [divWidth, setDivWidth] = useState(0); @@ -125,6 +141,24 @@ export const MeetingReactionOverlay = (props: MeetingReactionOverlayProps): JSX. /> ); + } else if (props.overlayMode === 'together-mode') { + /* @conditional-compile-remove(together-mode) */ + return ( +
+ +
+ ); + return <>; } else { return <>; } diff --git a/packages/react-components/src/components/TogetherModeOverlay.tsx b/packages/react-components/src/components/TogetherModeOverlay.tsx new file mode 100644 index 00000000000..c7ae672093e --- /dev/null +++ b/packages/react-components/src/components/TogetherModeOverlay.tsx @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import React, { useMemo, useState, memo } from 'react'; +/* @conditional-compile-remove(together-mode) */ +import { + Reaction, + ReactionResources, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryLocalParticipant, + VideoGalleryRemoteParticipant +} from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { moveAnimationStyles, spriteAnimationStyles } from './styles/ReactionOverlay.style'; +/* @conditional-compile-remove(together-mode) */ +import { REACTION_NUMBER_OF_ANIMATION_FRAMES } from './VideoGallery/utils/reactionUtils'; +/* @conditional-compile-remove(together-mode) */ +import { Icon, mergeStyles, Stack, Text } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ +import { getEmojiResource } from './VideoGallery/utils/videoGalleryLayoutUtils'; +/* @conditional-compile-remove(together-mode) */ +import { useLocale } from '../localization'; +/* @conditional-compile-remove(together-mode) */ +import { _HighContrastAwareIcon } from './HighContrastAwareIcon'; +/* @conditional-compile-remove(together-mode) */ +import { + calculateScaledSize, + getTogetherModeParticipantOverlayStyle, + REACTION_MAX_TRAVEL_HEIGHT, + REACTION_TRAVEL_HEIGHT, + setTogetherModeSeatPositionStyle, + togetherModeIconStyle, + togetherModeParticipantDisplayName, + togetherModeParticipantEmojiSpriteStyle, + togetherModeParticipantStatusContainer, + TogetherModeSeatStyle +} from './styles/TogetherMode.styles'; +/* @conditional-compile-remove(together-mode) */ +import { CallingTheme, useTheme } from '../theming'; +/* @conditional-compile-remove(together-mode) */ +import { RaisedHandIcon } from './assets/RaisedHandIcon'; +/* @conditional-compile-remove(together-mode) */ +import { _pxToRem } from '@internal/acs-ui-common'; + +/* @conditional-compile-remove(together-mode) */ +/** + * Signaling action overlay component props + * @internal + */ +type TogetherModeParticipantStatus = { + reaction?: Reaction; + scaledSize?: number; + isHandRaised?: boolean; + isSpotlighted?: boolean; + isMuted?: boolean; + id: string; + seatPositionStyle: TogetherModeSeatStyle; + displayName: string; + showDisplayName: boolean; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * TogetherModeOverlay component renders an empty JSX element. + * + * @returns {JSX.Element} An empty JSX element. + */ +export const TogetherModeOverlay = memo( + (props: { + emojiSize: number; + reactionResources: ReactionResources; + localParticipant: VideoGalleryLocalParticipant; + remoteParticipants: VideoGalleryRemoteParticipant[]; + togetherModeSeatPositions: VideoGalleryTogetherModeParticipantPosition; + }) => { + const locale = useLocale(); + const theme = useTheme(); + const callingPalette = (theme as unknown as CallingTheme).callingPalette; + + const { emojiSize, reactionResources, remoteParticipants, localParticipant, togetherModeSeatPositions } = props; + const [togetherModeParticipantStatus, setTogetherModeParticipantStatus] = useState<{ + [key: string]: TogetherModeParticipantStatus; + }>({}); + const [hoveredParticipantID, setHoveredParticipantID] = useState(''); + + /* + * The useMemo hook is used to calculate the participant status for the Together Mode overlay. + * It updates the togetherModeParticipantStatus state when there's a change in the remoteParticipants, localParticipant, + * raisedHand, spotlight, isMuted, displayName, or hoveredParticipantID. + */ + useMemo(() => { + const allParticipants = [...remoteParticipants, localParticipant]; + + const participantsWithVideoAvailable = allParticipants.filter( + (p) => p.videoStream?.isAvailable && togetherModeSeatPositions[p.userId] + ); + + const updatedSignals: { [key: string]: TogetherModeParticipantStatus } = {}; + for (const p of participantsWithVideoAvailable) { + const { userId, reaction, raisedHand, spotlight, isMuted, displayName } = p; + const seatingPosition = togetherModeSeatPositions[userId]; + if (seatingPosition) { + updatedSignals[userId] = { + id: userId, + reaction: reactionResources && reaction, + isHandRaised: !!raisedHand, + isSpotlighted: !!spotlight, + isMuted, + displayName: displayName || locale.strings.videoGallery.displayNamePlaceholder, + showDisplayName: !!(spotlight || raisedHand || hoveredParticipantID === userId), + scaledSize: calculateScaledSize(seatingPosition.width, seatingPosition.height), + seatPositionStyle: setTogetherModeSeatPositionStyle(seatingPosition) + }; + } + } + + // This is used to remove the participants bounding box from the DOM when they are no longer in the stream + const participantsNotInTogetherModeStream = Object.keys(togetherModeParticipantStatus).filter( + (id) => !updatedSignals[id] + ); + + setTogetherModeParticipantStatus((prevSignals) => { + const newSignals = { ...prevSignals, ...updatedSignals }; + const newSignalsLength = Object.keys(newSignals).length; + + participantsNotInTogetherModeStream.forEach((id) => { + delete newSignals[id]; + }); + + const hasChanges = Object.keys(newSignals).some( + (key) => + JSON.stringify(newSignals[key]) !== JSON.stringify(prevSignals[key]) || + newSignalsLength !== Object.keys(prevSignals).length + ); + + return hasChanges ? newSignals : prevSignals; + }); + }, [ + remoteParticipants, + localParticipant, + togetherModeParticipantStatus, + togetherModeSeatPositions, + reactionResources, + locale.strings.videoGallery.displayNamePlaceholder, + hoveredParticipantID + ]); + + /* + * When a larger participant scene switches to a smaller group in Together Mode, + * participant video streams remain available because their video is still active, + * even though they are not visible in the Together Mode stream. + * Therefore, we rely on the updated seating position values to identify who is included in the Together Mode stream. + * The Together mode seat position will only contain seat coordinates of participants who are visible in the Together Mode stream. + */ + useMemo(() => { + const removedVisibleParticipants = Object.keys(togetherModeParticipantStatus).filter( + (participantId) => !togetherModeSeatPositions[participantId] + ); + + setTogetherModeParticipantStatus((prevSignals) => { + const newSignals = { ...prevSignals }; + removedVisibleParticipants.forEach((participantId) => { + delete newSignals[participantId]; + }); + + // Trigger a re-render only if changes occurred + const hasChanges = Object.keys(newSignals).length !== Object.keys(prevSignals).length; + return hasChanges ? newSignals : prevSignals; + }); + }, [togetherModeParticipantStatus, togetherModeSeatPositions]); + + return ( +
+ {Object.values(togetherModeParticipantStatus).map( + (participantStatus) => + participantStatus.id && ( +
setHoveredParticipantID(participantStatus.id)} + onMouseLeave={() => setHoveredParticipantID('')} + > +
+ {participantStatus.reaction?.reactionType && ( + // First div - Section that fixes the travel height and applies the movement animation + // Second div - Responsible for ensuring the sprite emoji is always centered in the participant seat position + // Third div - Play Animation as the other animation applies on the base play animation for the sprite +
+
+
+
+
+ )} + + {participantStatus.showDisplayName && ( +
+
+ {participantStatus.isHandRaised && } + {participantStatus.showDisplayName && ( + + {participantStatus.displayName} + + )} + {participantStatus.isMuted && ( + + + + )} + {participantStatus.isSpotlighted && ( + + + + )} +
+
+ )} +
+
+ ) + )} +
+ ); + } +); diff --git a/packages/react-components/src/components/VideoGallery.tsx b/packages/react-components/src/components/VideoGallery.tsx index aac6f5bd873..82fef7a1f90 100644 --- a/packages/react-components/src/components/VideoGallery.tsx +++ b/packages/react-components/src/components/VideoGallery.tsx @@ -16,6 +16,12 @@ import { VideoStreamOptions, CreateVideoStreamViewResult } from '../types'; +/* @conditional-compile-remove(together-mode) */ +import { + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeStreams, + TogetherModeStreamViewResult +} from '../types/TogetherModeTypes'; import { ViewScalingMode } from '../types'; import { HorizontalGalleryStyles } from './HorizontalGallery'; import { _RemoteVideoTile } from './RemoteVideoTile'; @@ -24,7 +30,10 @@ import { LocalScreenShare } from './VideoGallery/LocalScreenShare'; import { RemoteScreenShare } from './VideoGallery/RemoteScreenShare'; import { LocalVideoCameraCycleButtonProps } from './LocalVideoCameraButton'; import { _ICoordinates, _ModalClone } from './ModalClone/ModalClone'; -import { _formatString } from '@internal/acs-ui-common'; +import { + _formatString, + /* @conditional-compile-remove(together-mode) */ _isIdentityMicrosoftTeamsUser +} from '@internal/acs-ui-common'; import { _LocalVideoTile } from './LocalVideoTile'; import { DefaultLayout } from './VideoGallery/DefaultLayout'; import { FloatingLocalVideoLayout } from './VideoGallery/FloatingLocalVideoLayout'; @@ -37,8 +46,13 @@ import { SpeakerVideoLayout } from './VideoGallery/SpeakerVideoLayout'; import { FocusedContentLayout } from './VideoGallery/FocusContentLayout'; /* @conditional-compile-remove(large-gallery) */ import { LargeGalleryLayout } from './VideoGallery/LargeGalleryLayout'; + +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeLayout } from './VideoGallery/TogetherModeLayout'; import { LayoutProps } from './VideoGallery/Layout'; import { ReactionResources } from '../types/ReactionTypes'; +/* @conditional-compile-remove(together-mode) */ +import { TogetherModeStream } from './VideoGallery/TogetherModeStream'; /** * @private @@ -142,6 +156,7 @@ export type VideoGalleryLayout = | 'floatingLocalVideo' | 'speaker' | /* @conditional-compile-remove(large-gallery) */ 'largeGallery' + | /* @conditional-compile-remove(together-mode) */ 'togetherMode' | 'focusedContent'; /** @@ -313,6 +328,23 @@ export interface VideoGalleryProps { * This callback is to mute a remote participant */ onMuteParticipant?: (userId: string) => Promise; + /* @conditional-compile-remove(together-mode) */ + startTogetherModeEnabled?: boolean; + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive?: boolean; + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; + /* @conditional-compile-remove(together-mode) */ + /** Callback to create the local video stream view */ + onStartTogetherMode?: () => Promise; + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize?: (width: number, height: number) => void; + /* @conditional-compile-remove(together-mode) */ + togetherModeStreams?: VideoGalleryTogetherModeStreams; + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamView?: () => Promise; /* @conditional-compile-remove(media-access) */ /** * This callback is to forbid audio for remote participant(s) @@ -418,6 +450,22 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { reactionResources, videoTilesOptions, onMuteParticipant, + /* @conditional-compile-remove(together-mode) */ + startTogetherModeEnabled, + /* @conditional-compile-remove(together-mode) */ + isTogetherModeActive, + /* @conditional-compile-remove(together-mode) */ + onCreateTogetherModeStreamView, + /* @conditional-compile-remove(together-mode) */ + onStartTogetherMode, + /* @conditional-compile-remove(together-mode) */ + onSetTogetherModeSceneSize, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreams, + /* @conditional-compile-remove(together-mode) */ + togetherModeSeatingCoordinates, + /* @conditional-compile-remove(together-mode) */ + onDisposeTogetherModeStreamView, /* @conditional-compile-remove(media-access) */ onForbidAudio, /* @conditional-compile-remove(media-access) */ @@ -722,7 +770,6 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { ); const screenShareParticipant = remoteParticipants.find((participant) => participant.screenShareStream?.isAvailable); - const localScreenShareStreamComponent = ( { ? localScreenShareStreamComponent : undefined; + /* @conditional-compile-remove(together-mode) */ + const togetherModeStreamComponent = useMemo( + () => ( + + ), + [ + startTogetherModeEnabled, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onDisposeTogetherModeStreamView, + onSetTogetherModeSceneSize, + togetherModeStreams, + togetherModeSeatingCoordinates, + localParticipant, + remoteParticipants, + reactionResources, + screenShareComponent, + containerWidth, + containerHeight + ] + ); + /* @conditional-compile-remove(together-mode) */ + // Current implementation of capabilities is only based on user role. + // This logic checks for the user role and if the user is a Teams user. + const canSwitchToTogetherModeLayout = + isTogetherModeActive || (_isIdentityMicrosoftTeamsUser(localParticipant.userId) && startTogetherModeEnabled); + const layoutProps = useMemo( () => ({ remoteParticipants, @@ -769,7 +859,9 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipantUserIds: pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipantUserIds: spotlightedParticipants + spotlightedParticipantUserIds: spotlightedParticipants, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreamComponent }), [ remoteParticipants, @@ -787,7 +879,9 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { pinnedParticipants, overflowGalleryPosition, localVideoTileSize, - spotlightedParticipants + spotlightedParticipants, + /* @conditional-compile-remove(together-mode) */ + togetherModeStreamComponent ] ); @@ -806,8 +900,19 @@ export const VideoGallery = (props: VideoGalleryProps): JSX.Element => { if (layout === 'largeGallery') { return ; } + /* @conditional-compile-remove(together-mode) */ + // Teams users can switch to Together mode layout only if they have the capability, + // while ACS users can do so only if Together mode is enabled. + if (layout === 'togetherMode' && canSwitchToTogetherModeLayout) { + return ; + } return ; - }, [layout, layoutProps, screenShareParticipant]); + }, [ + /* @conditional-compile-remove(together-mode) */ canSwitchToTogetherModeLayout, + layout, + layoutProps, + screenShareParticipant + ]); return (
{ + const { + remoteParticipants = [], + dominantSpeakers, + screenShareComponent, + onRenderRemoteParticipant, + styles, + maxRemoteVideoStreams, + parentWidth, + parentHeight, + overflowGalleryPosition = 'horizontalBottom', + pinnedParticipantUserIds = [], + togetherModeStreamComponent + } = props; + const isNarrow = parentWidth ? isNarrowWidth(parentWidth) : false; + + const isShort = parentHeight ? isShortHeight(parentHeight) : false; + + const [indexesToRender, setIndexesToRender] = useState([]); + const childrenPerPage = useRef(4); + + const { gridParticipants, overflowGalleryParticipants } = useOrganizedParticipants({ + remoteParticipants, + dominantSpeakers, + maxGridParticipants: maxRemoteVideoStreams, + isScreenShareActive: !!screenShareComponent, + maxOverflowGalleryDominantSpeakers: screenShareComponent + ? childrenPerPage.current - (pinnedParticipantUserIds.length % childrenPerPage.current) + : childrenPerPage.current, + pinnedParticipantUserIds, + layout: 'floatingLocalVideo' + }); + const { gridTiles, overflowGalleryTiles } = renderTiles( + gridParticipants, + onRenderRemoteParticipant, + maxRemoteVideoStreams, + indexesToRender, + overflowGalleryParticipants, + dominantSpeakers + ); + + const layerHostId = useId('layerhost'); + const togetherModeOverFlowGalleryTiles = useMemo(() => { + let newTiles = overflowGalleryTiles; + if (togetherModeStreamComponent) { + if (screenShareComponent) { + newTiles = gridTiles.concat(overflowGalleryTiles); + } + } + return newTiles; + }, [gridTiles, overflowGalleryTiles, screenShareComponent, togetherModeStreamComponent]); + + const overflowGallery = useMemo(() => { + if (overflowGalleryTiles.length === 0 && !props.screenShareComponent) { + return null; + } + return ( + { + childrenPerPage.current = n; + }} + parentWidth={parentWidth} + /> + ); + }, [ + overflowGalleryTiles.length, + props.screenShareComponent, + isShort, + isNarrow, + togetherModeOverFlowGalleryTiles, + styles?.horizontalGallery, + styles?.verticalGallery, + overflowGalleryPosition, + parentWidth + ]); + + return screenShareComponent ? ( + + + + {props.overflowGalleryPosition === 'horizontalTop' ? overflowGallery : <>} + {screenShareComponent} + {overflowGalleryTrampoline(overflowGallery, props.overflowGalleryPosition)} + + + ) : ( + {props.togetherModeStreamComponent} + ); +}; + +/* @conditional-compile-remove(together-mode) */ +const overflowGalleryTrampoline = ( + gallery: JSX.Element | null, + galleryPosition?: 'horizontalBottom' | 'verticalRight' | 'horizontalTop' +): JSX.Element | null => { + return galleryPosition !== 'horizontalTop' ? gallery : <>; + return gallery; +}; diff --git a/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx new file mode 100644 index 00000000000..521a58aa2dc --- /dev/null +++ b/packages/react-components/src/components/VideoGallery/TogetherModeStream.tsx @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* @conditional-compile-remove(together-mode) */ +import React, { useEffect, useMemo, memo } from 'react'; +/* @conditional-compile-remove(together-mode) */ +import { _formatString, _pxToRem } from '@internal/acs-ui-common'; +/* @conditional-compile-remove(together-mode) */ +import { + ReactionResources, + VideoGalleryTogetherModeParticipantPosition, + VideoGalleryTogetherModeStreams, + TogetherModeStreamViewResult, + VideoGalleryLocalParticipant, + VideoGalleryRemoteParticipant, + VideoStreamOptions +} from '../../types'; +/* @conditional-compile-remove(together-mode) */ +import { StreamMedia } from '../StreamMedia'; +/* @conditional-compile-remove(together-mode) */ +import { MeetingReactionOverlay } from '../MeetingReactionOverlay'; +/* @conditional-compile-remove(together-mode) */ +import { Stack } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ +import { togetherModeStreamRootStyle } from '../styles/TogetherMode.styles'; +/* @conditional-compile-remove(together-mode) */ +/** + * A memoized version of local screen share component. React.memo is used for a performance + * boost by memoizing the same rendered component to avoid rerendering this when the parent component rerenders. + * https://reactjs.org/docs/react-api.html#reactmemo + */ +export const TogetherModeStream = memo( + (props: { + startTogetherModeEnabled?: boolean; + isTogetherModeActive?: boolean; + onCreateTogetherModeStreamView?: (options?: VideoStreamOptions) => Promise; + onStartTogetherMode?: (options?: VideoStreamOptions) => Promise; + onDisposeTogetherModeStreamView?: () => Promise; + onSetTogetherModeSceneSize?: (width: number, height: number) => void; + togetherModeStreams?: VideoGalleryTogetherModeStreams; + seatingCoordinates?: VideoGalleryTogetherModeParticipantPosition; + reactionResources?: ReactionResources; + localParticipant?: VideoGalleryLocalParticipant; + remoteParticipants?: VideoGalleryRemoteParticipant[]; + screenShareComponent?: JSX.Element; + containerWidth?: number; + containerHeight?: number; + }): JSX.Element => { + const { + startTogetherModeEnabled, + isTogetherModeActive, + onCreateTogetherModeStreamView, + onStartTogetherMode, + onSetTogetherModeSceneSize, + onDisposeTogetherModeStreamView, + togetherModeStreams, + containerWidth, + containerHeight + } = props; + + useEffect(() => { + return () => { + // TODO: Isolate disposing behaviors for screenShare and videoStream + onDisposeTogetherModeStreamView && onDisposeTogetherModeStreamView(); + }; + }, [onDisposeTogetherModeStreamView]); + + // Trigger startTogetherMode only when needed + useEffect(() => { + if (startTogetherModeEnabled && !isTogetherModeActive) { + onStartTogetherMode?.(); + } + }, [startTogetherModeEnabled, isTogetherModeActive, onStartTogetherMode]); + + // Create stream view if not already created + useEffect(() => { + if (!togetherModeStreams?.mainVideoStream?.renderElement) { + onCreateTogetherModeStreamView?.(); + } + }, [togetherModeStreams?.mainVideoStream?.renderElement, onCreateTogetherModeStreamView]); + + // Update scene size only when container dimensions change + useMemo(() => { + if (onSetTogetherModeSceneSize && containerWidth && containerHeight) { + onSetTogetherModeSceneSize(containerWidth, containerHeight); + } + }, [onSetTogetherModeSceneSize, containerWidth, containerHeight]); + + const stream = props.togetherModeStreams?.mainVideoStream; + const showLoadingIndicator = !(stream && stream.isAvailable && stream.isReceiving); + + return containerWidth && containerHeight ? ( + + + + + ) : ( + <> + ); + } +); diff --git a/packages/react-components/src/components/styles/TogetherMode.styles.ts b/packages/react-components/src/components/styles/TogetherMode.styles.ts new file mode 100644 index 00000000000..b816b7a06b9 --- /dev/null +++ b/packages/react-components/src/components/styles/TogetherMode.styles.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* @conditional-compile-remove(together-mode) */ +import { _pxToRem } from '@internal/acs-ui-common'; +/* @conditional-compile-remove(together-mode) */ +import { VideoGalleryTogetherModeSeatingInfo } from '../../types/TogetherModeTypes'; +/* @conditional-compile-remove(together-mode) */ +import { IStackStyles } from '@fluentui/react'; +/* @conditional-compile-remove(together-mode) */ +import { CSSProperties } from 'react'; + +/* @conditional-compile-remove(together-mode) */ +/** + * Multiplier to convert rem units to pixels. + */ +export const REM_TO_PX_MULTIPLIER = 16; + +/* @conditional-compile-remove(together-mode) */ +/** + * The travel height for reactions in Together Mode. + * The reaction move overlay uses pixel units, so the seat position height, defined in rem, needs to be converted to pixels + */ +export const REACTION_TRAVEL_HEIGHT = 0.35 * REM_TO_PX_MULTIPLIER; + +/* @conditional-compile-remove(together-mode) */ +/** + * Defines the maximum travel height for reactions in Together Mode. + * Ensures the reaction animation does not exceed the center point from the top. + * Since the reaction move overlay uses pixel units, the seat position height (defined in rem) must be converted to pixels. + */ +export const REACTION_MAX_TRAVEL_HEIGHT = 0.5 * REM_TO_PX_MULTIPLIER; + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface for defining the coordinates of a seat in Together Mode. + */ +export interface TogetherModeParticipantSeatPosition { + height: string; + width: string; + left: string; + top: string; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Interface for defining the style of a seat position in Together Mode. + */ +export interface TogetherModeSeatStyle { + seatPosition: TogetherModeParticipantSeatPosition; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Sets the seating position for a participant in Together Mode. + * + * @param seatingPosition - The seating position information. + * @returns The style object for the seating position. + */ +export function setParticipantSeatingPosition( + seatingPosition: VideoGalleryTogetherModeSeatingInfo +): TogetherModeParticipantSeatPosition { + return { + width: _pxToRem(seatingPosition.width), + height: _pxToRem(seatingPosition.height), + left: _pxToRem(seatingPosition.left), + top: _pxToRem(seatingPosition.top) + }; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * Return a style bucket based on the number of active sprites. + * For example, the first three reactions should appear at maximum + * height, width, and opacity. + * @private + */ +export function setTogetherModeSeatPositionStyle( + seatingPosition: VideoGalleryTogetherModeSeatingInfo +): TogetherModeSeatStyle { + return { + seatPosition: setParticipantSeatingPosition(seatingPosition) + }; +} + +/* @conditional-compile-remove(together-mode) */ +/** + * The style for the Together Mode meeting overlay. + */ +export const togetherModeMeetingOverlayStyle: CSSProperties = { + width: '100%', + height: '100%', + position: 'absolute', + top: '0', + left: '0' +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * Generates the overlay style for a participant in Together Mode. + * + * @param seatingPosition - The seating position information. + * @returns The style object for the participant overlay. + */ +export function getTogetherModeParticipantOverlayStyle(seatingPositionStyle: TogetherModeSeatStyle): CSSProperties { + return { + ...seatingPositionStyle.seatPosition, + position: 'absolute' + }; +} + +/* @conditional-compile-remove(together-mode) */ +// Function to map a value from one range to another +const mapRange = (value: number, inMin: number, inMax: number, outMin: number, outMax: number): number => { + return outMin + ((value - inMin) * (outMax - outMin)) / (inMax - inMin); +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * Calculate the reaction emoji scaled size based on width and height of the participant seat width and height. + * This is needed when the browser is resized and the participant seat width and height changes. + * + * @param width - The width of the element. + * @param height - The height of the element. + * @returns The scaled size. + */ +export const calculateScaledSize = (width: number, height: number): number => { + // Maximum participant seat width and height + const maxSize = 600; + // Minimum participant seat width and height + const minSize = 200; + // Minimum scaled width and height of the reaction emoji + const minScaledSize = 35; + // Maximum scaled width and height of the reaction emoji + const maxScaledSize = 70; + + // Use width or height to determine scaling factor + const size = Math.min(width, height); + + // Map the size to the desired range + return mapRange(size, minSize, maxSize, minScaledSize, maxScaledSize); +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const togetherModeStreamRootStyle: IStackStyles = { + root: { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + top: 0, + left: 0 + } +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const togetherModeIconStyle = (): CSSProperties => { + return { + width: _pxToRem(20), + flexShrink: 0 + }; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * The style for the container holding the display name, raiseHand, spotlight and mute icons. + * @private + */ +export const togetherModeParticipantStatusContainer = ( + backgroundColor: string, + borderRadius: string +): CSSProperties => { + return { + backgroundColor, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: _pxToRem(2), + margin: '0 auto', // Centers the container + padding: `0 ${_pxToRem(5)}`, + borderRadius, + width: 'fit-content' + }; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const togetherModeParticipantDisplayName = ( + isParticipantHovered: boolean, + participantSeatingWidth: number, + color: string +): CSSProperties => { + const MIN_DISPLAY_NAME_WIDTH = 100; + return { + textOverflow: 'ellipsis', + flexGrow: 1, // Allow text to grow within available space + overflow: isParticipantHovered ? 'visible' : 'hidden', + whiteSpace: 'nowrap', + textAlign: 'center', + color, + display: isParticipantHovered || participantSeatingWidth > MIN_DISPLAY_NAME_WIDTH ? 'inline-block' : 'none' // Completely remove the element when hidden + }; +}; + +/* @conditional-compile-remove(together-mode) */ +/** + * @private + */ +export const togetherModeParticipantEmojiSpriteStyle = ( + emojiSize: number, + emojiScaledSize: number, + participantSeatWidth: string +): CSSProperties => { + const participantSeatWidthInPixel = parseFloat(participantSeatWidth) * REM_TO_PX_MULTIPLIER; + const emojiScaledSizeInPercent = (emojiScaledSize / participantSeatWidthInPixel) * 100; + return { + width: `${emojiSize}`, + position: 'absolute', + // Center the emoji sprite within the participant seat + left: `${emojiScaledSizeInPercent / 2}%` + }; +}; diff --git a/packages/react-components/src/types/ReactionTypes.ts b/packages/react-components/src/types/ReactionTypes.ts index 4f28340ed98..5509cfd6a7f 100644 --- a/packages/react-components/src/types/ReactionTypes.ts +++ b/packages/react-components/src/types/ReactionTypes.ts @@ -47,4 +47,9 @@ export interface ReactionResources { * Options for overlay mode for reaction rendering * @internal */ -export type OverlayModeTypes = 'grid-tiles' | 'screen-share' | 'content-share'; +export type OverlayModeTypes = + | 'grid-tiles' + | 'screen-share' + | 'content-share' + /* @conditional-compile-remove(together-mode) */ + | 'together-mode'; diff --git a/packages/react-composites/package.json b/packages/react-composites/package.json index edf67311648..b0e81c32460 100644 --- a/packages/react-composites/package.json +++ b/packages/react-composites/package.json @@ -73,7 +73,7 @@ "nanoid": "3.3.8" }, "peerDependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-calling-effects": "^1.1.2", "@azure/communication-chat": "1.6.0-beta.3 || >=1.5.4", "@types/react": ">=16.8.0 <19.0.0", diff --git a/packages/storybook8/package.json b/packages/storybook8/package.json index a5a91a604d8..76d525202b8 100644 --- a/packages/storybook8/package.json +++ b/packages/storybook8/package.json @@ -21,7 +21,7 @@ }, "license": "MIT", "dependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-chat": "1.6.0-beta.3 || ^1.5.4", "@azure/communication-common": "^2.3.1", "@azure/communication-identity": "^1.3.0", diff --git a/samples/CallWithChat/package.json b/samples/CallWithChat/package.json index 95b4b432ee8..ac29f079a34 100644 --- a/samples/CallWithChat/package.json +++ b/samples/CallWithChat/package.json @@ -35,7 +35,7 @@ "dependencies": { "@azure/abort-controller": "^1.1.0", "@azure/communication-identity": "^1.3.0", - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-chat": "1.6.0-beta.3 || ^1.5.4", "@azure/communication-react": "1.23.0-beta.0", "@azure/communication-common": "^2.3.1", diff --git a/samples/Calling/package.json b/samples/Calling/package.json index 58468be8a5f..0e92e708c45 100644 --- a/samples/Calling/package.json +++ b/samples/Calling/package.json @@ -34,7 +34,7 @@ "@azure/abort-controller": "^1.1.0", "@azure/communication-identity": "^1.3.0", "@azure/communication-react": "1.23.0-beta.0", - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-common": "^2.3.1", "@azure/logger": "^1.0.4", "@babel/preset-react": "^7.12.7", diff --git a/samples/CallingStateful/package.json b/samples/CallingStateful/package.json index f3bcbeda21b..5101312e00f 100644 --- a/samples/CallingStateful/package.json +++ b/samples/CallingStateful/package.json @@ -34,7 +34,7 @@ "@azure/abort-controller": "^1.1.0", "@azure/communication-identity": "^1.3.0", "@azure/communication-react": "1.23.0-beta.0", - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-common": "^2.3.1", "@azure/logger": "^1.0.4", "@babel/preset-react": "^7.12.7", diff --git a/samples/ComponentExamples/package.json b/samples/ComponentExamples/package.json index 1ca0de0360e..c0e6938d74c 100644 --- a/samples/ComponentExamples/package.json +++ b/samples/ComponentExamples/package.json @@ -24,7 +24,7 @@ }, "license": "MIT", "dependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-chat": "1.6.0-beta.3 || ^1.5.4", "@azure/communication-common": "^2.3.1", "@azure/communication-identity": "^1.3.0", diff --git a/samples/StaticHtmlComposites/package.json b/samples/StaticHtmlComposites/package.json index fade0ebe17a..452617bbd20 100644 --- a/samples/StaticHtmlComposites/package.json +++ b/samples/StaticHtmlComposites/package.json @@ -25,7 +25,7 @@ "dependencies": { "@azure/communication-react": "1.23.0-beta.0", "@azure/communication-common": "^2.3.1", - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-chat": "1.6.0-beta.3 || ^1.5.4", "@fluentui/react": "^8.121.11", "react": "18.3.1", diff --git a/samples/tests/package.json b/samples/tests/package.json index 31437f7c94e..bbf49a6fa9f 100644 --- a/samples/tests/package.json +++ b/samples/tests/package.json @@ -21,7 +21,7 @@ }, "license": "MIT", "dependencies": { - "@azure/communication-calling": "1.32.2-beta.1 || ^1.31.2", + "@azure/communication-calling": "1.32.2-beta.1 || ^1.32.1", "@azure/communication-chat": "1.6.0-beta.3 || ^1.5.4", "@azure/communication-common": "^2.3.1", "uuid": "^9.0.0",