Compare commits

...

35 Commits

Author SHA1 Message Date
lebaudantoine 898bc9a0f8 🔖(minor) bump release to 1.21.0 2026-06-15 18:29:52 +02:00
Florent Chehab fd1715bacf ️(frontend) proper aria labels on custom backgrounds
* Set the aria-label to something meaningful,
* Set the delete btn aria label
2026-06-15 17:49:50 +02:00
Florent Chehab c9de7d049f ️(frontend) improve effects accessibility structure
* Set sub-headings to level 3, to be coherent
* Set role to list & list-item on background items list
2026-06-15 17:49:47 +02:00
Florent Chehab 6368b676a6 ️(frontend) set aria hidden on video effects preview
It makes sense to set aria-hidden for this video.
2026-06-15 17:49:47 +02:00
Florent Chehab 65789ef706 ️(frontend) fix blur effects aria label
Was configured to always light.
2026-06-15 17:49:46 +02:00
lebaudantoine c0feb1ee82 🐛(frontend) fix metadata agent collector enabled check
Fix an incorrect check that caused issues in production when
determining whether the metadata agent collector is enabled.
2026-06-15 17:48:50 +02:00
leo 135b99aee7 (summary) add optional satisfaction survey footer
Add a footer to transcription outputs linking to an external satisfaction
survey. The survey URL is built from TRANSCRIPTION_SATISFACTION_FORM_BASE_URL.
When TRANSCRIPTION_SATISFACTION_FORM_BASE_URL is unset or None, the
footer is omitted.
2026-06-15 17:33:24 +02:00
lebaudantoine 040df0e15a 🚸(frontend) mute participants by default when joining a large meeting
When joining a meeting that already has many participants, new
participants are now muted by default.

This is an empirical change, not directly requested by users but
informed by user experience: a lot of people joining large meetings
arrive with their microphone and/or camera open, which can be
painful for the host, who otherwise has to mute everyone or ask
everyone to mute. Many of these participants are inattentive but
still noisy.

If you are joining a room that is already at a certain size, you are
probably not the one expected to speak; presenters typically join
among the very first participants.
2026-06-15 17:02:00 +02:00
lebaudantoine 00e197b216 🚸(frontend) mute join notification sound in larger rooms
In particularly large rooms, with more than 5 or 10 participants,
the entry notification sound can quickly feel spammy.

A configuration to disable it exists, but new users do not discover
it easily. The sound notification is most useful while waiting for
the first few participants to arrive; once a few people are in, the
meeting usually starts and the sound becomes more disruptive than
helpful.

The visual notification remains in place, so users are still aware
when newcomers join. Roll this out and check whether users find the
change helpful.
2026-06-15 17:02:00 +02:00
lebaudantoine eae1a382d5 🩹(frontend) fix options unwrapping when silent login is disabled
The options were not properly unwrapped when silent login was
disabled, leading to incorrect behavior in that code path.
2026-06-14 00:49:42 +02:00
lebaudantoine d4a7cf279c (frontend) allow hiding the login button via a URL parameter
Required by some self-hosters who want to present a link to
participants that are guests only, without access to their SSO. For
example, a meeting between a citizen and a public servant where the
guest is known not to have an account.

Also useful when rendering the join page inside an iframe.
2026-06-14 00:49:42 +02:00
lebaudantoine 70a296eea6 (frontend) allow disabling silent login via a URL parameter
Several clients ran into issues with the silent login, leading to
redirections that were not appropriate for their use case.

Offer a URL parameter to control this behavior and disable silent
login when needed.
2026-06-14 00:49:42 +02:00
lebaudantoine ac85a20271 ️(frontend) lazy-load @libreaudio/la-call via dynamic import
The noise-suppression processor is now imported on demand with a
dynamic import() instead of a top-level static import, so it lands in
its own code-split chunk rather than the main bundle.

Why this is necessary:

* @libreaudio/la-call is fully self-contained. It inlines *everything*
  as JavaScript: two WASM binaries (SIMD and non-SIMD variants), the
  Emscripten glue, the worklet processor source, and the noise model
  plus its weights — the model is compiled into the .wasm, so we ship
  effectively two copies of it.

* The WASM is inlined as numeric array literals (new Uint8Array([...])),
  the least compact representation possible (~2-4 source chars/byte) and
  not meaningfully minifiable. The result is a large module that also
  can't be stream-compiled the way an external .wasm asset would be.

* A static import would pull all of that into the initial bundle,
  inflating critical-path download size and lengthening build time
  (parsing/minifying the big literal) — for a feature that's only used
  when the user actually turns on noise suppression.

* Dynamic import() isolates the whole payload in a separate,
  content-hashed, browser-cached chunk. The cost (chunk fetch + WASM
  instantiation) becomes a one-time hit deferred to first activation,
  and is fully off the page's initial load path.

Notes:

* The library self-bundles its AudioWorklet at runtime from a Blob URL
  and inlines the WASM, so it needs no build-tool asset plumbing
  (no ?worker&url / ?url, no Vite asset config). The dynamic import is
  therefore the only splitting mechanism required.

* Audio processing still runs off the main thread on the AudioWorklet
  path (desktop/most browsers); only the Android ScriptProcessor
  fallback runs on the main thread. Import style does not affect this.

* To hide the first-activation latency, the chunk can be prefetched
  (e.g. import() on idle or <link rel="modulepreload">) so it's warm
  before the user enables suppression.
2026-06-13 13:30:20 +02:00
falkTX 13036f6ab7 (frontend) enhance noise reduction with BBBA audio processing pipeline
Replace the basic RNN noise processor with a more advanced audio
pipeline powered by Big Blue Better Audio (BBBA), a project
supported by the Prototype Fund.

The new WASM-based pipeline introduced by @falkTX and
@trummerschlunk adds:
- voice isolation
- high-pass filtering
- spectral balancing
- multiband compression
- low-pass filtering

This significantly improves overall audio quality and speech
clarity.

More information about the pipeline is available on the
trummerschlunk/BigBlueBetterAudio repo.

Voice isolation still relies on RNNNoise, a widely used deep
learning-based denoising model. Audio quality could be further
improved in the future if DeepFilterNet becomes usable directly
in the browser.

Their code has been released in an NPM package under GPL license.
2026-06-13 13:30:20 +02:00
lebaudantoine 64819b3696 🔖(minor) bump release to 1.20.0 2026-06-12 17:25:56 +02:00
lebaudantoine 53722ad1bc 🩹(frontend) fix CSP regression breaking inline styles and ProConnect
A previous CSP change suggested by CodeRabbit was not properly
tested and broke inline styling as well as the loading of the
ProConnect image.

Adjust the CSP directives to allow these resources again.
2026-06-12 15:54:26 +02:00
lebaudantoine dcfdd35c82 🔖(helm) release chart 0.0.23 2026-06-12 09:38:53 +02:00
snyk-bot 16f465432d fix: upgrade react-i18next from 15.1.1 to 17.0.8
Snyk has created this PR to upgrade react-i18next from 15.1.1 to 17.0.8.

See this package in npm:
react-i18next

See this project in Snyk:
https://app.eu.snyk.io/org/lasuite-dinum-default/project/96ea03d8-8d09-493d-86bf-363f274e129e?utm_source=github&utm_medium=referral&page=upgrade-pr
2026-06-12 09:11:07 +02:00
lebaudantoine 90f95ab2a9 ⬆️(frontend) upgrade libcrypto3 and libssl3 to 3.5.7-r0
Patches CVE-2026-45447 (HIGH), heap use-after-free in OpenSSL
PKCS7_verify(), flagged by Trivy scan of the frontend image.

- libcrypto3: 3.5.6-r0 -> 3.5.7-r0
- libssl3: 3.5.6-r0 -> 3.5.7-r0

Ref: https://avd.aquasec.com/nvd/cve-2026-45447
2026-06-11 16:26:13 +02:00
leo 61f7ad05e9 🐛(frontend) fix noise reduction left-channel-only audio
Fix bug with RNNoise noise reduction which interprets mono input
as left channel with some browsers.
2026-06-11 16:26:13 +02:00
lebaudantoine 69a6dd1463 🩹(frontend) fix missing default-src in CSP configuration
The CSP was missing a default-src directive (flagged by CodeRabbit).
Without it, styles, images, fonts, and media are completely
unrestricted, which undercuts the otherwise strict policy.

Set default-src to 'self' and add an explicit style-src directive
since we rely on inline styles (e.g. #close-msg and the view
toggling use inline style attributes), so style-src needs to allow
'self' and 'unsafe-inline'.
2026-06-11 15:16:52 +02:00
lebaudantoine 6d06aee92d 📝(addon) update the changelog
please refer the previous commits where I enhanced the whole
addin features.
2026-06-11 15:16:52 +02:00
lebaudantoine 54908b9caa 🔧(addon) parametrize the frontend nginx configuration via a volume
Mount the nginx configuration used by the frontend image as a volume
so the default one can be overridden by a custom configuration at
deployment time.

Based on suggestions from @rouja to help parametrize the
configuration at deployment time.
2026-06-11 15:16:52 +02:00
lebaudantoine 08af6e77bb 📌(addon) pin dependencies to their currently installed versions
Improve code quality and reproducibility by pinning project
dependencies to the exact versions currently installed.
2026-06-11 15:16:52 +02:00
lebaudantoine 4525c9c255 💄(addon) align the beta tag with the UI kit styling
Update the beta tag to follow the UI kit styling so it clearly
signals that the plugin is in beta.
2026-06-11 15:16:52 +02:00
lebaudantoine 44d3ed8f2e (addon) add a feedback form link in the footer
Add the possibility to display a feedback form link in the footer to
help collect feedback from the first users of the plugin.

Once the plugin is in production at scale, this will be replaced
with a link to the support page.
2026-06-11 15:16:52 +02:00
lebaudantoine 9522dc72ac 📈(addon) allow appending an Outlook source query param to the URL
Offer the possibility to append a query parameter at the end of the
meeting URL to flag links created from the Outlook plugin, inspired
by Zoom.

The behavior is toggled via an environment variable.
2026-06-11 15:16:52 +02:00
lebaudantoine 09dbb250a7 🚸(addon) switch add button to remove when a link already exists
From the task panel, change the "add" button to "remove" when a
meeting link is already present.

This will be updated to "update" once the external API supports
updating an existing room.
2026-06-11 15:16:52 +02:00
lebaudantoine 321cb99f82 🩹(addon) skip link generation when one already exists in the event
Detect whether a meeting link is already present in the calendar
event item or its body, and prevent generating a new link if one is
found.

Detection is based on the presence of the app URL in the text. This
is imperfect but covers the most naive scenarios.
2026-06-11 15:16:52 +02:00
lebaudantoine 52bad6faaf 🔖(addons) bump plugin version to 0.0.2
Update the plugin version displayed to the user to 0.0.2 and move the
tag from alpha to beta.
2026-06-11 15:16:52 +02:00
lebaudantoine 0065527a5e 🌐(addon) internationalize the addon
Refactor the plugin to support internationalization and ship it in
three languages.

Internationalizing requires updating the manifest, which involves
touching the Helm chart and releasing a new version. Ship a beta
version of the plugin once the i18n work is done to bundle these
changes together.
2026-06-11 15:16:52 +02:00
lebaudantoine 818d20888e 💄(addons) use the app logo for the "create a meeting link" ribbon action
In the ribbon, the "create a meeting link" action was displayed with
the generic add icon, which is fitting in the context of a menu but
not explicit enough on its own.

Following feedback from the social ministries, replace the add icon
with the app logo to make it clear that this button creates a
videoconference link.
2026-06-11 15:16:52 +02:00
lebaudantoine f1fce4431f ♻️(addons) insert meeting link at the cursor position
Instead of appending the meeting link at the end of the email, insert
it where the user's cursor is. This ensures the link is not placed
after the email signature, or below the quoted thread in a reply.
2026-06-11 15:16:52 +02:00
lebaudantoine 491866e584 ♻️(addons) try using the default client font
Refactor the message builder so that rendered messages are displayed
in a nicer way. Based on user feedback from the social ministries.

Go for a hybrid approach: on the desktop client, insert plain text so
it picks up the default font configured by the client theme; on the
web client, insert HTML, as the editor there is rendered using HTML.
2026-06-11 15:16:52 +02:00
lebaudantoine f777f2fdeb 🩹(addon) show fallback message when dialog cannot auto-close
The dialog window does not close itself on Outlook Desktop, which
opens links in a webview that behaves like a different browser.

Calling window.close() fails because, for security reasons, JS is
prevented from closing the window. The browser considers that the JS
script is not the one that opened the dialog.

The dialog also loses its reference to the opener due to the
redirection, so there is no way to message the parent to trigger a
close.

Keeping the dialog reference on the parent side does not help either:
since the opener is lost, the parent cannot call close on the dialog.

After investigation, go with a temporary solution that shows an
explicit message hinting the user to close the dialog manually if it
does not close automatically.
2026-06-11 15:16:52 +02:00
79 changed files with 1331 additions and 2174 deletions
+34 -1
View File
@@ -8,6 +8,39 @@ and this project adheres to
## [Unreleased]
## [1.21.0] - 2026-06-15
### Added
- ✨(frontend) allow disabling silent login via a URL parameter
- ✨(frontend) allow hiding the login button via a URL parameter
- ✨(summary) add optional satisfaction survey footer
### Changed
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
- 🚸(frontend) mute join notification sound in larger rooms
- 🚸(frontend) mute participants by default when joining a large meeting
### Fixed
- 🐛(frontend) fix metadata agent collector enabled check
### Fixed
- ♿️(frontend) improve accessibilty of the Effects panel #1401
## [1.20.0] - 2026-06-12
### Changed
- ♻️(addon) improve Outlook add-on: i18n support, feedback link, smarter link
- ⬆️(frontend) upgrade react-i18next from 15.1.1 to 17.0.8
### Fixed
- 🐛(frontend) fix noise reduction left-channel-only audio
## [1.19.0] - 2026-06-04
### Added
@@ -25,7 +58,7 @@ and this project adheres to
- 🔇(summary) make ffmpeg quiet #1404
- 🔒️(backend) prevent accessing files if they are not ready #1395
- ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
- # ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
## [1.18.0] - 2026-06-03
+2
View File
@@ -60,6 +60,8 @@ USER root
# Security patches for known CVEs
RUN apk update && apk upgrade \
libcrypto3>=3.5.7-r0 \
libssl3>=3.5.7-r0 \
musl \
musl-utils \
zlib>=1.3.2-r0 \
+4 -1
View File
@@ -48,9 +48,12 @@ server {
set $nonce $request_id;
set $csp "upgrade-insecure-requests; ";
set $csp "default-src 'self'; upgrade-insecure-requests; ";
set $csp "${csp}frame-ancestors ${ms_domains}; ";
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
set $csp "${csp}img-src 'self' data:; ";
set $csp "${csp}font-src 'self' data:; ";
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
set $csp "${csp}frame-src 'none'; ";
set $csp "${csp}object-src 'none'; ";
+3
View File
@@ -22,3 +22,6 @@ WEBHOOK_URL="https://configure-your-url.com"
POSTHOG_API_KEY="your-posthog-key"
POSTHOG_ENABLED="False"
# Transcription
TRANSCRIPTION_SATISFACTION_FORM_BASE_URL=
+32 -13
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id>
<Version>0.0.1.0</Version>
<Version>0.0.2.0</Version>
<ProviderName>__APP_NAME__</ProviderName>
<DefaultLocale>fr-FR</DefaultLocale>
<DisplayName DefaultValue="__APP_NAME__"/>
@@ -87,9 +87,9 @@
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Add.16x16"/>
<bt:Image size="32" resid="Add.32x32"/>
<bt:Image size="80" resid="Add.80x80"/>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromMail</FunctionName>
@@ -126,9 +126,9 @@
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Add.16x16"/>
<bt:Image size="32" resid="Add.32x32"/>
<bt:Image size="80" resid="Add.80x80"/>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
@@ -175,15 +175,34 @@
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<!-- Default (French) -->
<bt:String id="GroupLabel" DefaultValue="__APP_NAME__"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__"/>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__">
<bt:Override Locale="en-US" Value="Add a __APP_NAME__ link"/>
<bt:Override Locale="de-DE" Value="__APP_NAME__-Link hinzufügen"/>
</bt:String>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
<bt:Override Locale="en-US" Value="Open settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
</bt:String>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
<bt:Override Locale="en-US" Value="Settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen"/>
</bt:String>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement."/>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement.">
<bt:Override Locale="de-DE" Value="Generiert einen __APP_NAME__-Besprechungslink und fügt ihn in den Termin ein."/>
<bt:Override Locale="en-US" Value="Generates a __APP_NAME__ meeting link and inserts it into the item."/>
</bt:String>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
</bt:String>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
</bt:String>
</bt:LongStrings>
</Resources>
</VersionOverrides>
+65 -28
View File
@@ -9,33 +9,35 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"core-js": "^3.49.0",
"regenerator-runtime": "^0.14.1"
"core-js": "3.49.0",
"i18next": "26.3.1",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.25.4",
"@types/office-js": "^1.0.377",
"@types/office-runtime": "^1.0.35",
"acorn": "^8.11.3",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^14.0.0",
"eslint-plugin-office-addins": "^4.0.3",
"file-loader": "^6.2.0",
"html-loader": "^5.0.0",
"html-webpack-inject-attributes-plugin": "^1.0.6",
"html-webpack-plugin": "^5.6.0",
"office-addin-cli": "^2.0.3",
"office-addin-debugging": "^6.0.3",
"office-addin-dev-certs": "^2.0.3",
"office-addin-lint": "^3.0.3",
"office-addin-manifest": "^2.0.3",
"office-addin-prettier-config": "^2.0.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"source-map-loader": "^5.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.0",
"@types/office-js": "1.0.582",
"@types/office-runtime": "1.0.36",
"acorn": "8.16.0",
"babel-loader": "9.2.1",
"copy-webpack-plugin": "14.0.0",
"eslint-plugin-office-addins": "4.0.6",
"file-loader": "6.2.0",
"html-loader": "5.1.0",
"html-webpack-inject-attributes-plugin": "1.0.6",
"html-webpack-plugin": "5.6.6",
"office-addin-cli": "2.0.6",
"office-addin-debugging": "6.0.6",
"office-addin-dev-certs": "2.0.6",
"office-addin-lint": "3.0.6",
"office-addin-manifest": "2.1.2",
"office-addin-prettier-config": "2.0.1",
"os-browserify": "0.3.0",
"process": "0.11.10",
"source-map-loader": "5.0.0",
"webpack": "5.105.4",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.4"
}
},
@@ -2111,9 +2113,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -9363,6 +9363,43 @@
"node": ">=10.18"
}
},
"node_modules/i18next": {
"version": "26.3.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
"funding": [
{
"type": "individual",
"url": "https://www.locize.com/i18next"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
},
{
"type": "individual",
"url": "https://www.locize.com"
}
],
"license": "MIT",
"peerDependencies": {
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -15213,7 +15250,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
+27 -25
View File
@@ -26,33 +26,35 @@
"watch": "webpack --mode development --watch"
},
"dependencies": {
"core-js": "^3.49.0",
"regenerator-runtime": "^0.14.1"
"core-js": "3.49.0",
"i18next": "26.3.1",
"i18next-browser-languagedetector": "8.2.1",
"regenerator-runtime": "0.14.1"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.25.4",
"@types/office-js": "^1.0.377",
"@types/office-runtime": "^1.0.35",
"acorn": "^8.11.3",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^14.0.0",
"eslint-plugin-office-addins": "^4.0.3",
"file-loader": "^6.2.0",
"html-loader": "^5.0.0",
"html-webpack-inject-attributes-plugin": "^1.0.6",
"html-webpack-plugin": "^5.6.0",
"office-addin-cli": "^2.0.3",
"office-addin-debugging": "^6.0.3",
"office-addin-dev-certs": "^2.0.3",
"office-addin-lint": "^3.0.3",
"office-addin-manifest": "^2.0.3",
"office-addin-prettier-config": "^2.0.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"source-map-loader": "^5.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.0",
"@types/office-js": "1.0.582",
"@types/office-runtime": "1.0.36",
"acorn": "8.16.0",
"babel-loader": "9.2.1",
"copy-webpack-plugin": "14.0.0",
"eslint-plugin-office-addins": "4.0.6",
"file-loader": "6.2.0",
"html-loader": "5.1.0",
"html-webpack-inject-attributes-plugin": "1.0.6",
"html-webpack-plugin": "5.6.6",
"office-addin-cli": "2.0.6",
"office-addin-debugging": "6.0.6",
"office-addin-dev-certs": "2.0.6",
"office-addin-lint": "3.0.6",
"office-addin-manifest": "2.1.2",
"office-addin-prettier-config": "2.0.1",
"os-browserify": "0.3.0",
"process": "0.11.10",
"source-map-loader": "5.0.0",
"webpack": "5.105.4",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.4"
},
"prettier": "office-addin-prettier-config",
+45 -28
View File
@@ -1,12 +1,18 @@
/* global Office */
const { APP_NAME } = require("../common/index");
const { createRoom, initSession } = require("../common/api");
const { startPolling } = require("../common/polling");
const { saveSession, loadSession } = require("../common/session");
const { openTransitDialog } = require("../common/transitDialog");
const { buildMeetingMessage } = require("../common/messageBuilder");
const { applyAppName } = require("../common/helpers");
const { initI18n, t } = require("../common/i18n");
const { isMeetingAlreadyAdded } = require("../common/meetingDetector");
Office.onReady(async function (info) {
await initI18n()
Office.onReady(function (info) {
if (info.host === Office.HostType.Outlook) {
applyAppName();
}
@@ -22,41 +28,52 @@ function notify(message) {
}
function insertMeetingLink(event, session) {
const item = Office.context.mailbox.item;
isMeetingAlreadyAdded(item)
.then((alreadyAdded) => {
if (alreadyAdded) {
notify(t("meeting.already_added", { app_name: APP_NAME }));
event.completed();
return;
}
return _doInsertMeetingLink(event, session);
})
.catch((err) => {
notify(t("meeting.error.details", { message: err.message }));
event.completed();
});
}
function _doInsertMeetingLink(event, session) {
createRoom(session)
.then((data) => {
const { url, message } = buildMeetingMessage(data);
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
const { url, text } = buildMeetingMessage(data, isWeb);
const item = Office.context.mailbox.item;
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
return new Promise((resolve, reject) => {
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
notify(`Erreur de lecture : ${getResult.error.message}`);
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
notify(t("meeting.error.details", { message: setResult.error.message }));
resolve();
return;
}
const newBody = getResult.value + message;
item.body.setAsync(newBody, { coercionType: Office.CoercionType.Html }, (setResult) => {
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
notify(`Erreur d'insertion : ${setResult.error.message}`);
resolve();
return;
}
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
notify(t("meeting.link_inserted"));
resolve();
return;
}
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
notify("Lien de réunion inséré !");
resolve();
return;
item.location.setAsync(url, (locationResult) => {
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
notify(t("meeting.error.details", { message: locationResult.error.message }));
} else {
notify(t("meeting.link_inserted"));
}
item.location.setAsync(url, (locationResult) => {
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
notify(`Erreur de localisation : ${locationResult.error.message}`);
} else {
notify("Lien de réunion inséré !");
}
resolve();
});
resolve();
});
});
});
@@ -79,11 +96,11 @@ function connect(event) {
});
},
onTimeout: () => {
notify("Connexion expirée, veuillez réessayer.");
notify(t("meeting.error.auth"));
event.completed();
},
onError: (err) => {
notify("Une erreur est survenue, veuillez ré-essayer");
notify(t("meeting.error.retry"));
event.completed();
},
});
@@ -99,7 +116,7 @@ function connect(event) {
});
})
.catch((err) => {
notify(`Erreur : ${err.message}`);
notify(t("meeting.error.details", { message: err.message }));
event.completed();
});
}
+48
View File
@@ -0,0 +1,48 @@
const { APP_NAME } = require("../common");
const i18nextModule = require("i18next");
const i18next = i18nextModule.default || i18nextModule;
const fr = require("../locales/fr/translation.json");
const en = require("../locales/en/translation.json");
const de = require("../locales/de/translation.json");
async function initI18n() {
const lng = typeof Office !== "undefined" ? Office.context.displayLanguage : navigator.language;
await i18next.init({
lng,
fallbackLng: "fr",
interpolation: { escapeValue: false },
resources: {
fr: { translation: fr },
en: { translation: en },
de: { translation: de },
},
});
}
function t(key, vars) {
return i18next.t(key, vars);
}
function translateUI() {
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
el.textContent = t(key, { app_name: APP_NAME });
});
document.querySelectorAll("[data-i18n-attr]").forEach((el) => {
const pairs = el.getAttribute("data-i18n-attr").split(",");
pairs.forEach((pair) => {
const [attr, key] = pair.split(":");
el.setAttribute(attr, t(key, { app_name: APP_NAME }));
});
});
document.querySelectorAll("[data-i18n-aria]").forEach((el) => {
el.setAttribute("aria-label", t(el.getAttribute("data-i18n-aria")));
});
}
module.exports = { initI18n, t, translateUI };
+4
View File
@@ -1,7 +1,11 @@
const BASE_URL = window.__APP_CONFIG__?.BASE_URL || "https://meet.127.0.0.1.nip.io";
const APP_NAME = window.__APP_CONFIG__?.APP_NAME || "LaSuite Meet";
const ENABLE_SOURCE_TRACKING = window.__APP_CONFIG__?.ENABLE_SOURCE_TRACKING === "true";
const FEEDBACK_FORM = window.__APP_CONFIG__?.FEEDBACK_FORM || null;
module.exports = {
BASE_URL,
APP_NAME,
ENABLE_SOURCE_TRACKING,
FEEDBACK_FORM
};
@@ -0,0 +1,107 @@
const { BASE_URL } = require("./index");
/**
* Returns a promise that resolves to true if a meeting link is already present
*/
function isMeetingAlreadyAdded(item) {
return Promise.all([_checkBody(item), _checkLocation(item)]).then(
([inBody, inLocation]) => inBody || inLocation
);
}
function _checkBody(item) {
return new Promise((resolve) => {
item.body.getAsync(Office.CoercionType.Text, (result) => {
if (result.status !== Office.AsyncResultStatus.Succeeded) {
resolve(false);
return;
}
resolve(_containsMeetingUrl(result.value));
});
});
}
function _checkLocation(item) {
// Location only exists on appointments
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
return Promise.resolve(false);
}
return new Promise((resolve) => {
item.location.getAsync((result) => {
if (result.status !== Office.AsyncResultStatus.Succeeded) {
resolve(false);
return;
}
resolve(_containsMeetingUrl(result.value));
});
});
}
function _containsMeetingUrl(text) {
if (!text) return false;
return text.includes(BASE_URL);
}
function removeMeetingLink(item) {
return Promise.all([_removeFromBody(item), _removeFromLocation(item)]);
}
function _removeFromBody(item) {
return new Promise((resolve) => {
item.body.getAsync(Office.CoercionType.Html, (result) => {
if (result.status !== Office.AsyncResultStatus.Succeeded) {
resolve();
return;
}
const cleaned = _cleanBody(result.value || "");
if (cleaned === null) {
resolve();
return;
}
item.body.setAsync(cleaned, { coercionType: Office.CoercionType.Html }, () => resolve());
});
});
}
function _removeFromLocation(item) {
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
return Promise.resolve();
}
return new Promise((resolve) => {
item.location.getAsync((result) => {
if (
result.status === Office.AsyncResultStatus.Succeeded &&
_containsMeetingUrl(result.value)
) {
item.location.setAsync("", () => resolve());
} else {
resolve();
}
});
});
}
const SEPARATOR = /─{10,}/;
/**
* Returns cleaned HTML, or null if no meeting block was found.
*/
function _cleanBody(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
const hits = [];
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
while (walker.nextNode()) {
if (SEPARATOR.test(walker.currentNode.nodeValue)) hits.push(walker.currentNode);
}
if (hits.length < 2) return null;
const range = doc.createRange();
range.setStartBefore(hits[0]);
range.setEndAfter(hits[hits.length - 1]);
range.deleteContents();
return doc.documentElement.outerHTML;
}
module.exports = { isMeetingAlreadyAdded, removeMeetingLink };
+43 -16
View File
@@ -1,4 +1,5 @@
const { APP_NAME } = require("./index");
const { APP_NAME, ENABLE_SOURCE_TRACKING } = require("./index");
const { t } = require("./i18n");
function _formatPin(pin) {
if (!pin) return "";
@@ -20,33 +21,59 @@ function _formatPhone(phone) {
return clean;
}
function _appendTrackingParams(url) {
if (!ENABLE_SOURCE_TRACKING) return url;
const u = new URL(url);
u.searchParams.set("from", "outlook-addin");
return u.toString();
}
// todo - escape html / link
function buildMeetingMessage(data) {
function buildMeetingMessage(data, isWeb) {
if (!data?.url) {
throw new Error("buildMeetingMessage: missing url in data");
}
const url = data.url;
const url = _appendTrackingParams(data.url);
const phone = _formatPhone(data.telephony?.phone_number);
const pin = _formatPin(data.telephony?.pin_code);
const telephonyBlock =
phone && pin
? `
let textLines = "";
let phoneLines = [];
Ou appelez (audio uniquement)
(FR) ${phone}
Code : ${pin}`
: "";
const join = t("meeting_message.join", { app_name: APP_NAME });
const phoneOnly = t("meeting_message.phone_only");
const phoneFr = t("meeting_message.phone_fr", { phone });
const pinCode = t("meeting_message.pin_code", { pin });
const message = `<pre style="font-family:inherit; font-size:inherit; border:none; background:none; margin:16px 0;">
────────────────────────────────────────
Rejoindre la réunion ${APP_NAME}
if (isWeb) {
phoneLines = phone && pin ? [`<br><br>${phoneOnly}`, `<br>${phoneFr}`, `<br>${pinCode}`] : [];
<a href="${url}">${url}</a>${telephonyBlock}
────────────────────────────────────────</pre>`;
textLines = [
"<br><br>────────────────────────────────────────",
`<br>${join}`,
`<br><br><a href="${url}" target="_blank">${url}</a>`,
...phoneLines,
"<br>────────────────────────────────────────<br>",
];
return { url, message };
} else {
phoneLines = phone && pin ? [`\n\n${phoneOnly}`, `\n${phoneFr}`, `\n${pinCode}`] : [];
textLines = [
"\n\n────────────────────────────────────────",
`\n${join}`,
`\n\n${url}`,
...phoneLines,
"\n────────────────────────────────────────\n",
];
}
const text = textLines.join("");
return { url, text };
}
module.exports = { buildMeetingMessage };
@@ -0,0 +1,40 @@
{
"app": {
"sideload": "Laden Sie das Add-In.",
"loading": "Wird geladen..."
},
"unauth": {
"intro": "Fügen Sie Ihren Outlook-Terminen ganz einfach einen {{app_name}}-Besprechungslink hinzu.",
"proconnect_btn": "Aanmelden met ProConnect",
"proconnect_link": "Wat is ProConnect?",
"proconnect_link_title": "Wat is ProConnect? - nieuw venster"
},
"success": {
"close_window": "Falls sich dieses Fenster nicht automatisch schließt, schließen Sie es bitte manuell."
},
"auth": {
"disconnect": "Abmelden"
},
"meeting": {
"already_added": "Es wurde bereits ein {{app_name}}-Meeting hinzugefügt.",
"link_inserted": "Besprechungslink erfolgreich eingefügt",
"generating": "Wird erstellt...",
"add_meeting": "{{app_name}}-Besprechung hinzufügen",
"remove_meeting": "{{app_name}}-Besprechung entfernen",
"removing": "Wird entfernt...",
"error": {
"auth": "Ihre Sitzung ist abgelaufen. Bitte versuchen Sie es erneut.",
"retry": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"details": "Fehler: {{message}}"
}
},
"meeting_message": {
"join": "An der {{app_name}}-Besprechung teilnehmen",
"phone_only": "Oder per Telefon teilnehmen (nur Audio)",
"phone_fr": "(FR) {{phone}}",
"pin_code": "Code {{pin}}"
},
"footer": {
"feedback": "Teilen Sie uns Ihr Feedback mit"
}
}
@@ -0,0 +1,40 @@
{
"app": {
"sideload": "Please load the add-in.",
"loading": "Loading..."
},
"unauth": {
"intro": "Easily add a {{app_name}} meeting link to your Outlook events.",
"proconnect_btn": "Sign in with ProConnect",
"proconnect_link": "What is ProConnect?",
"proconnect_link_title": "What is ProConnect? - new window"
},
"success": {
"close_window": "If this window does not close automatically, please close it manually."
},
"auth": {
"disconnect": "Sign out"
},
"meeting": {
"already_added": "A {{app_name}} meeting has already been added.",
"link_inserted": "Meeting link inserted successfully",
"generating": "Generating...",
"add_meeting": "Add a {{app_name}} meeting",
"remove_meeting": "Remove the {{app_name}} meeting",
"removing": "Removing...",
"error": {
"auth": "Your session has expired. Please try again.",
"retry": "An error occurred. Please try again.",
"details": "Error: {{message}}"
}
},
"meeting_message": {
"join": "Join the {{app_name}} meeting",
"phone_only": "Or call in (audio only)",
"phone_fr": "(FR) {{phone}}",
"pin_code": "Code {{pin}}"
},
"footer": {
"feedback": "Share your feedback"
}
}
@@ -0,0 +1,40 @@
{
"app": {
"sideload": "Veuillez charger le complément.",
"loading": "Chargement..."
},
"unauth": {
"intro": "Ajoutez facilement un lien de réunion {{app_name}} à vos événements Outlook.",
"proconnect_btn": "S'identifier avec ProConnect",
"proconnect_link": "Qu'est-ce que ProConnect ?",
"proconnect_link_title": "Qu'est-ce que ProConnect ? - nouvelle fenêtre"
},
"success": {
"close_window": "Si cette fenêtre ne se ferme pas toute seule, veuillez la fermer manuellement."
},
"auth": {
"disconnect": "Se déconnecter"
},
"meeting": {
"already_added": "Une réunion {{app_name}} a déjà été ajoutée.",
"link_inserted": "Lien de réunion inséré avec succès",
"generating": "Génération...",
"add_meeting": "Ajouter une réunion {{app_name}}",
"remove_meeting": "Supprimer la réunion {{app_name}}",
"removing": "Suppression en cours...",
"error": {
"auth": "Connexion expirée, veuillez réessayer.",
"retry": "Une erreur est survenue, veuillez ré-essayer",
"details": "Erreur : {{message}}"
}
},
"meeting_message": {
"join": "Rejoindre la réunion {{app_name}}",
"phone_only": "Ou appelez (audio uniquement)",
"phone_fr": "(FR) {{phone}}",
"pin_code": "Code {{pin}}"
},
"footer": {
"feedback": "Partagez-nous vos retours"
}
}
+3 -2
View File
@@ -9,10 +9,10 @@
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
</head>
<body>
<div id="sideload-msg">Veuillez charger le complément.</div>
<div id="sideload-msg" data-i18n="app.sideload"></div>
<div class="spinner-container"
role="progressbar"
aria-label="Chargement..."
data-i18n-aria="app.loading"
>
<svg class="spinner-svg"
viewBox="0 0 28 28"
@@ -40,5 +40,6 @@
</svg>
</span>
</div>
<p id="close-msg" style="display: none; text-align: center; font-size: 13px; color: #666; margin-top: 16px;" data-i18n="success.close_window"></p>
</body>
</html>
+29 -14
View File
@@ -1,20 +1,35 @@
const { applyAppName } = require("../common/helpers");
const { exchangeSession } = require("../common/api");
const { consume } = require("../common/transitToken");
const { initI18n, translateUI } = require("../common/i18n");
applyAppName();
(async () => {
await initI18n();
const transitToken = consume();
applyAppName();
translateUI();
if (!transitToken) {
console.error("Transit token not found in sessionStorage");
window.close();
} else {
exchangeSession(transitToken)
.catch((e) => {
console.error(`Error occured: ${e}`);
})
.finally(() => {
window.close();
});
}
const transitToken = consume();
if (!transitToken) {
console.error("Transit token not found in sessionStorage");
window.close();
} else {
exchangeSession(transitToken)
.then(() => {
document.querySelector(".spinner-container").style.display = "none";
document.querySelector("#close-msg").style.display = "block";
})
.catch((e) => {
console.error(`Error occured: ${e}`);
})
.finally(() => {
// NOTE: doesn't work with the desktop client — the browser considers
// this window wasn't opened by this script (it was opened externally),
// so it blocks window.close() for security reasons. The "#close-msg"
// shown above is the fallback for that case.g
window.close();
});
}
})();
+48 -4
View File
@@ -115,15 +115,39 @@ button {
background-color: #f5f5f5;
}
/* ── Danger button (remove meeting) ── */
#btn-remove {
background-color: #CA3632; /* error.400 */
color: #FFFFFF;
border: none;
}
#btn-remove:hover {
background-color: #EE6A66; /* error.600 */
}
#btn-remove:active {
background-color: #F28D8A; /* error.700 */
color: #F6AFAD; /* error.200 */
}
#btn-remove:disabled {
background-color: #F6AFAD; /* error.800 */
color: #FAD2D1; /* error.900 */
cursor: not-allowed;
}
/* ── Version ── */
#version-tag {
position: fixed;
bottom: 8px;
left: 8px;
right: 8px;
display: inline-flex;
display: flex;
justify-content: space-between;
align-items: center;
gap: 4px;
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #6b7280;
@@ -131,9 +155,29 @@ button {
pointer-events: none;
}
#feedback-link {
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #6b7280;
text-decoration: underline;
pointer-events: all; /* override parent's pointer-events: none */
cursor: pointer;
}
#feedback-link:hover {
color: #374151;
}
#footer-right {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.version-badge {
background: #fef3c7;
color: #92400e;
background: #EEF1F4;
color: #2845C1;
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
+25 -16
View File
@@ -10,47 +10,56 @@
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
</head>
<body>
<div id="sideload-msg">Veuillez charger le complément.</div>
<div id="sideload-msg" data-i18n="app.sideload"></div>
<div id="app-body">
<!-- Loading -->
<div id="view-loading">
<p class="intro-text">Chargement...</p>
<p class="intro-text" data-i18n="app.loading"></p>
</div>
<!-- Unauthenticated -->
<div id="view-unauth" style="display:none;">
<p class="intro-text">
<span>Ajoutez facilement un lien de réunion <span data-app-name></span> à vos événements Outlook.</span>
<span data-i18n="unauth.intro"></span>
</p>
<hr class="divider" />
<button class="proconnect-button" id="btn-connect">
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
<span class="proconnect-sr-only" data-i18n="unauth.proconnect_btn"></span>
</button>
<p>
<a
href="https://www.proconnect.gouv.fr/"
target="_blank"
rel="noopener noreferrer"
title="Quest-ce que ProConnect ? - nouvelle fenêtre"
>
Quest-ce que ProConnect ?
</a>
<a href="https://www.proconnect.gouv.fr/"
target="_blank"
rel="noopener noreferrer"
data-i18n-attr="title:unauth.proconnect_link_title"
data-i18n="unauth.proconnect_link"
></a>
</p>
</div>
<!-- Authenticated -->
<div id="view-auth" style="display:none;">
<div id="btn-container">
<button id="btn-generate">Ajouter une réunion <span data-app-name></span></button>
<button id="btn-disconnect">Se déconnecter</button>
<!-- shown when no meeting is present -->
<button id="btn-generate" data-i18n="meeting.add_meeting"></button>
<!-- shown when a meeting is already present -->
<button id="btn-remove" style="display:none;" data-i18n="meeting.remove_meeting"></button>
<button id="btn-disconnect" data-i18n="auth.disconnect"></button>
</div>
</div>
</div>
<footer id="version-tag">
<span class="version-badge">alpha</span>
<span class="version-number">0.0.1</span>
<a id="feedback-link"
style="display:none;"
target="_blank"
rel="noopener noreferrer"
data-i18n="footer.feedback"
></a>
<div id="footer-right">
<span class="version-badge">beta</span>
<span class="version-number">0.0.2</span>
</div>
</footer>
</body>
</html>
+113 -42
View File
@@ -1,22 +1,77 @@
const { APP_NAME } = require("../common");
/* global Office */
const { APP_NAME, FEEDBACK_FORM } = require("../common");
const { applyAppName } = require("../common/helpers");
const { initSession, createRoom } = require("../common/api");
const { startPolling } = require("../common/polling");
const { openTransitDialog } = require("../common/transitDialog");
const { loadSession, saveSession, clearSession } = require("../common/session");
const { buildMeetingMessage } = require("../common/messageBuilder");
const { initI18n, t, translateUI } = require("../common/i18n");
const { isMeetingAlreadyAdded, removeMeetingLink } = require("../common/meetingDetector");
// ── Views ────────────────────────────────────────────────────
// todo - support loading view while polling
// todo - support error view
function showView(name) {
document.getElementById("view-loading").style.display = "none";
document.getElementById("view-unauth").style.display = "none";
document.getElementById("view-auth").style.display = "none";
document.getElementById(`view-${name}`).style.display = "block";
if (name === "auth") {
_refreshMeetingButtonState();
}
}
// ── Button state ─────────────────────────────────────────────
function _showAddButton() {
document.getElementById("btn-generate").style.display = "block";
document.getElementById("btn-remove").style.display = "none";
}
function _showRemoveButton() {
document.getElementById("btn-generate").style.display = "none";
document.getElementById("btn-remove").style.display = "block";
}
function _setButtonLoading() {
const btn = document.getElementById("btn-generate");
btn.disabled = true;
btn.textContent = t("meeting.generating");
}
function _setButtonIdle() {
const btn = document.getElementById("btn-generate");
btn.disabled = false;
btn.textContent = t("meeting.add_meeting", { app_name: APP_NAME });
}
function _setRemoveLoading() {
const btn = document.getElementById("btn-remove");
btn.disabled = true;
btn.textContent = t("meeting.removing");
}
function _setRemoveIdle() {
const btn = document.getElementById("btn-remove");
btn.disabled = false;
btn.textContent = t("meeting.remove_meeting", { app_name: APP_NAME });
}
function _refreshMeetingButtonState() {
const item = Office.context.mailbox.item;
if (!item) return;
isMeetingAlreadyAdded(item).then((alreadyAdded) => {
if (alreadyAdded) {
_showRemoveButton();
} else {
_showAddButton();
}
});
}
// ── Auth ─────────────────────────────────────────────────────
function connect() {
initSession()
.then((data) => {
@@ -47,22 +102,11 @@ function disconnect() {
clearSession().finally(() => showView("unauth"));
}
function _setButtonLoading() {
const btn = document.getElementById("btn-generate");
btn.disabled = true;
btn.textContent = "Génération...";
}
function _setButtonIdle() {
const btn = document.getElementById("btn-generate");
btn.disabled = false;
btn.textContent = `Ajouter une réunion ${APP_NAME}`;
}
// ── Meeting ──────────────────────────────────────────────────
function generateMeetingLink() {
const session = loadSession();
if (!session?.access_token) {
console.error("Session introuvable. Veuillez vous reconnecter.");
showView("unauth");
return;
}
@@ -71,36 +115,28 @@ function generateMeetingLink() {
createRoom(session)
.then((data) => {
const { url, message } = buildMeetingMessage(data);
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
const { url, text } = buildMeetingMessage(data, isWeb);
const item = Office.context.mailbox.item;
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
return new Promise((resolve, reject) => {
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
reject(getResult.error);
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
reject(setResult.error);
return;
}
item.body.setAsync(
getResult.value + message,
{ coercionType: Office.CoercionType.Html },
(setResult) => {
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
reject(setResult.error);
return;
}
// ─── If calendar event, also set location ──────────────
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
item.location.setAsync(url, () => resolve());
return;
}
resolve();
}
);
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
item.location.setAsync(url, () => resolve());
return;
}
resolve();
});
});
})
.then(() => {
_showRemoveButton();
})
.catch((err) => {
console.error(err);
})
@@ -109,7 +145,41 @@ function generateMeetingLink() {
});
}
Office.onReady((info) => {
function removeMeetingLinkFromItem() {
const session = loadSession();
if (!session?.access_token) {
showView("unauth");
return;
}
_setRemoveLoading();
const item = Office.context.mailbox.item;
removeMeetingLink(item)
.then(() => {
_showAddButton();
})
.catch((err) => {
console.error(err);
})
.finally(() => {
_setRemoveIdle();
});
}
// ── Init ─────────────────────────────────────────────────────
Office.onReady(async (info) => {
await initI18n();
translateUI();
if (FEEDBACK_FORM) {
const link = document.getElementById("feedback-link");
link.href = FEEDBACK_FORM;
link.style.display = "inline";
}
if (info.host === Office.HostType.Outlook) {
applyAppName();
document.getElementById("sideload-msg").style.display = "none";
@@ -117,10 +187,11 @@ Office.onReady((info) => {
document.getElementById("btn-connect").onclick = connect;
document.getElementById("btn-disconnect").onclick = disconnect;
document.getElementById("btn-generate").onclick = generateMeetingLink;
document.getElementById("btn-remove").onclick = removeMeetingLinkFromItem;
const session = loadSession();
if (session?.state === "authenticated" && session?.access_token) {
showView("auth");
showView("auth"); // this already calls _refreshMeetingButtonState internally
} else {
showView("unauth");
}
+2 -2
View File
@@ -10,11 +10,11 @@
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
</head>
<body>
<div id="sideload-msg">Veuillez charger le complément.</div>
<div id="sideload-msg" data-i18n="app.sideload"></div>
<div
class="spinner-container"
role="progressbar"
aria-label="Chargement..."
data-i18n-aria="app.loading"
>
<svg
class="spinner-svg"
+6 -1
View File
@@ -2,6 +2,7 @@ const { applyAppName } = require("../common/helpers");
const { URLS } = require("../common/urls");
const { save } = require("../common/transitToken");
const { DIALOG_SIGNALS } = require("../common/transitDialog");
const { initI18n, translateUI } = require("../common/i18n");
// Initiate the authentication flow, then return to the success page
function getAuthenticateUrl() {
@@ -10,7 +11,11 @@ function getAuthenticateUrl() {
return url.toString();
}
Office.onReady(function (info) {
Office.onReady(async function (info) {
await initI18n();
translateUI();
if (info.host === Office.HostType.Outlook) {
applyAppName();
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.19.0"
version = "1.21.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.5.13",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]]
name = "agents"
version = "1.19.0"
version = "1.21.0"
source = { virtual = "." }
dependencies = [
{ name = "livekit-agents" },
+6
View File
@@ -411,6 +411,12 @@ class Base(Configuration):
"transcription_destination": values.Value(
None, environ_name="FRONTEND_TRANSCRIPTION_DESTINATION", environ_prefix=None
),
"max_participants_for_sound": values.PositiveIntegerValue(
5, environ_name="FRONTEND_MAX_PARTICIPANTS_FOR_SOUND", environ_prefix=None
),
"auto_mute_on_join_threshold": values.PositiveIntegerValue(
50, environ_name="FRONTEND_AUTO_MUTE_ON_JOIN_THRESHOLD", environ_prefix=None
),
}
# Mail
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project]
name = "meet"
version = "1.19.0"
version = "1.21.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1187,7 +1187,7 @@ wheels = [
[[package]]
name = "meet"
version = "1.19.0"
version = "1.21.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2
View File
@@ -45,6 +45,8 @@ USER root
# Security patches for known CVEs
RUN apk update && apk upgrade \
libcrypto3>=3.5.7-r0 \
libssl3>=3.5.7-r0 \
musl \
musl-utils \
zlib>=1.3.2-r0 \
+25 -13
View File
@@ -1,16 +1,17 @@
{
"name": "meet",
"version": "1.19.0",
"version": "1.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.19.0",
"version": "1.21.0",
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4",
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2",
@@ -33,7 +34,7 @@
"react": "18.3.1",
"react-aria-components": "1.14.0",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"react-i18next": "17.0.8",
"use-sound": "5.0.0",
"valtio": "2.3.2",
"wouter": "3.10.0"
@@ -338,9 +339,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1446,6 +1447,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@libreaudio/la-call": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@libreaudio/la-call/-/la-call-0.1.4.tgz",
"integrity": "sha512-yaqlYGpvZbfpr1h2p1jSGOKPUy7NsfpoJN9gXvHmJu5MB8nN/qfx6LQ2a0hbua3H8rargFKROOv4VZ5WuTKNsA==",
"license": "GPL-3.0+"
},
"node_modules/@livekit/components-core": {
"version": "0.12.13",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
@@ -11235,17 +11242,19 @@
}
},
"node_modules/react-i18next": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz",
"integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==",
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
"@babel/runtime": "^7.29.2",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
"i18next": ">= 26.2.0",
"react": ">= 16.8.0",
"typescript": "^5 || ^6"
},
"peerDependenciesMeta": {
"react-dom": {
@@ -11253,6 +11262,9 @@
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.19.0",
"version": "1.21.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -17,6 +17,7 @@
"dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11",
"@libreaudio/la-call": "0.1.4",
"@fontsource/opendyslexic": "5.2.5",
"@livekit/components-react": "2.9.21",
"@livekit/components-styles": "1.2.0",
@@ -40,7 +41,7 @@
"react": "18.3.1",
"react-aria-components": "1.14.0",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"react-i18next": "17.0.8",
"use-sound": "5.0.0",
"valtio": "2.3.2",
"wouter": "3.10.0"
+2
View File
@@ -55,6 +55,8 @@ export interface ApiConfig {
default_sources: Source[]
}
transcription_destination?: string
max_participants_for_sound: number
auto_mute_on_join_threshold: number
}
const fetchConfig = (): Promise<ApiConfig> => {
+19 -3
View File
@@ -5,6 +5,16 @@ import { type ApiUser } from './ApiUser'
import { useMemo } from 'react'
import { useConfig } from '@/api/useConfig'
const SILENT_LOGIN_PARAM = 'silentLogin'
const isSilentLoginDisabledByUrl = () => {
if (typeof window === 'undefined') return false
const value = new URLSearchParams(window.location.search).get(
SILENT_LOGIN_PARAM
)
return value === 'false'
}
/**
* returns info about currently logged-in user
*
@@ -17,16 +27,22 @@ export const useUser = (
) => {
const { data, isLoading: isConfigLoading } = useConfig()
const disabledByUrl = useMemo(() => isSilentLoginDisabledByUrl(), [])
const options = useMemo(() => {
if (isConfigLoading) return
if (data?.is_silent_login_enabled !== true) {
const silentDisabled =
data?.is_silent_login_enabled !== true || disabledByUrl
if (silentDisabled) {
return {
...opts,
...opts.fetchUserOptions,
attemptSilent: false,
}
}
return opts.fetchUserOptions
}, [data, opts, isConfigLoading])
}, [data, opts, isConfigLoading, disabledByUrl])
const query = useQuery({
queryKey: [keys.user],
@@ -14,9 +14,11 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { Emoji } from '@/features/reactions/types'
import { useReactions } from '@/features/reactions/hooks/useReactions'
import { NotificationProvider } from './NotificationProvider'
import { useConfig } from '@/api/useConfig'
export const MainNotificationToast = () => {
const room = useRoomContext()
const { data } = useConfig()
const { triggerNotificationSound } = useNotificationSound()
const { t } = useTranslation('notifications')
const announce = useScreenReaderAnnounce()
@@ -135,12 +137,21 @@ export const MainNotificationToast = () => {
}
}, [room, handleEmoji])
const triggerNotificationSoundIfRoomIsSmall = useCallback(
(type: NotificationType) => {
if (!data) return
if (room.numParticipants >= data.max_participants_for_sound) return
triggerNotificationSound(type)
},
[room, data, triggerNotificationSound]
)
useEffect(() => {
const showJoinNotification = (participant: Participant) => {
if (isMobileBrowser()) {
return
}
triggerNotificationSound(NotificationType.ParticipantJoined)
triggerNotificationSoundIfRoomIsSmall(NotificationType.ParticipantJoined)
toastQueue.add(
{
participant,
@@ -155,7 +166,7 @@ export const MainNotificationToast = () => {
return () => {
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
}
}, [room, triggerNotificationSound])
}, [room, triggerNotificationSoundIfRoomIsSmall])
useEffect(() => {
const removeParticipantNotifications = (participant: Participant) => {
@@ -1,4 +1,5 @@
export enum NotificationType {
AutoMuteLargeRoom = 'autoMuteLargeRoom',
ParticipantJoined = 'participantJoined',
HandRaised = 'handRaised',
ParticipantMuted = 'participantMuted',
@@ -0,0 +1,54 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { type ToastProps } from './Toast'
import { VStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { Button } from '@/primitives'
import { css } from '@/styled-system/css'
import { StyledToastContainer } from './StyledToastContainer'
import { useRoomContext } from '@livekit/components-react'
export function ToastAutoMuteLargeRoom({
state,
...props
}: Readonly<ToastProps>) {
const room = useRoomContext()
const { t } = useTranslation('notifications', {
keyPrefix: 'autoMuteLargeRoom',
})
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const toast = props.toast
const handleDismiss = async () => {
room.localParticipant
.setMicrophoneEnabled(true)
.finally(() => state.close(toast.key))
}
return (
<StyledToastContainer {...toastProps} ref={ref}>
<VStack
justify="start"
alignItems="self-start"
{...contentProps}
maxWidth="370px"
gap="0.75rem"
padding={14}
>
<p>{t('auto')}</p>
<Button
size="sm"
variant="text"
className={css({
color: 'primary.300',
})}
onPress={() => handleDismiss()}
>
{t('dismiss')}
</Button>
</VStack>
</StyledToastContainer>
)
}
@@ -13,6 +13,7 @@ import { ToastAnyRecording } from './ToastAnyRecording'
import { ToastRecordingSaving } from './ToastRecordingSaving'
import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
import { ToastRecordingRequest } from './ToastRecordingRequest'
import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -45,6 +46,11 @@ const renderToast = (
case NotificationType.LowerHand:
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
case NotificationType.AutoMuteLargeRoom:
return (
<ToastAutoMuteLargeRoom key={toast.key} toast={toast} state={state} />
)
case NotificationType.TranscriptionStarted:
case NotificationType.TranscriptionStopped:
case NotificationType.TranscriptionLimitReached:
@@ -5,6 +5,15 @@ import type { Participant } from 'livekit-client'
import type { NotificationPayload } from './NotificationPayload'
import type { RecordingMode } from '@/features/recording'
export const notifyAutoMutedOnJoin = () => {
toastQueue.add(
{
type: NotificationType.AutoMuteLargeRoom,
},
{ timeout: NotificationDuration.ALERT }
)
}
export const showLowerHandToast = (
participant: Participant,
onClose: () => void
@@ -6,5 +6,5 @@ export const useIsMetadataCollectorEnabled = () => {
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.metadataCollector)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
return (featureEnabled && isAnalyticsEnabled) || true
return !isAnalyticsEnabled || Boolean(featureEnabled)
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import {
@@ -33,6 +33,9 @@ import { useIsMobile } from '@/utils/useIsMobile'
import { navigateTo } from '@/navigation/navigateTo'
import { connectionObserverStore } from '@/stores/connectionObserver'
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
import { useSnapshot } from 'valtio'
import { userPreferencesStore } from '@/stores/userPreferences'
export const Conference = ({
roomId,
@@ -57,6 +60,8 @@ export const Conference = ({
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
const userPreferencesSnap = useSnapshot(userPreferencesStore)
const {
mutateAsync: createRoom,
status: createStatus,
@@ -173,6 +178,8 @@ export const Conference = ({
const isMobile = useIsMobile()
const hasAutoMutedRef = useRef(false)
/*
* Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility
* (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor
@@ -228,6 +235,19 @@ export const Conference = ({
onError={(e) => {
posthog.captureException(e)
}}
onConnected={async () => {
if (!apiConfig) return
if (
userPreferencesSnap.is_auto_mute_large_room_enabled &&
!hasAutoMutedRef.current &&
userConfig.audioEnabled &&
room.numParticipants > apiConfig.auto_mute_on_join_threshold
) {
hasAutoMutedRef.current = true
await room.localParticipant.setMicrophoneEnabled(false)
notifyAutoMutedOnJoin()
}
}}
onDisconnected={(e) => {
const metadata = {
room_id: roomId,
@@ -216,6 +216,14 @@ export const Join = ({
try {
const track = await createLocalAudioTrack({
deviceId: { exact: audioDeviceId },
noiseSuppression: true,
echoCancellation: true,
autoGainControl: true,
voiceIsolation: false,
// Audio quality optimized for voice
sampleRate: 48000, // High quality sample rate
channelCount: 1, // Mono for voice calls (saves bandwidth)
sampleSize: 16, // 16-bit audio
})
setDynamicAudioTrack(track)
} catch (error) {
@@ -400,6 +400,8 @@ export const EffectsConfiguration = ({
config: ProcessorConfig
isSelected: boolean
tooltip: string
ariaLabel: string
ariaDeleteLabel: string
file: ApiFileItem
}[]
}>(() => {
@@ -426,7 +428,9 @@ export const EffectsConfiguration = ({
const id = deriveIdFromProcessorConfig(config)
return {
id,
tooltip: t(`blur.light.${selectedId === id ? 'clear' : 'apply'}`),
tooltip: t(
`blur.${item.key}.${selectedId === id ? 'clear' : 'apply'}`
),
radius: item.radius,
isSelected: selectedId === id,
Icon: item.icon,
@@ -443,9 +447,11 @@ export const EffectsConfiguration = ({
}
const id = deriveIdFromProcessorConfig(config)
const isSelected = selectedId === id
const prefix = isSelected ? 'selectedLabel' : 'apply'
const backgroundName = t(`virtual.presets.descriptions.${index}`)
const ariaLabel = `${t(`virtual.presets.${prefix}`)} ${backgroundName}`
const ariaLabelPrefix = t(
isSelected ? `virtual.selectedLabel` : `virtual.apply`
)
const ariaLabel = `${ariaLabelPrefix} ${backgroundName}`
return {
tooltip: backgroundName,
@@ -467,13 +473,22 @@ export const EffectsConfiguration = ({
}
const id = deriveIdFromProcessorConfig(config)
const isSelected = selectedId === id
const ariaLabel = t(
isSelected
? `virtual.personal.selectedLabel`
: `virtual.personal.apply`
)
const ariaDeleteLabel = t('virtual.personal.deleteLabel')
return {
tooltip: file.title,
id,
config,
isSelected: selectedId === id,
isSelected,
file,
ariaLabel,
ariaDeleteLabel,
}
}),
}
@@ -552,6 +567,7 @@ export const EffectsConfiguration = ({
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
borderRadius: '8px',
}}
aria-hidden={true}
/>
) : (
<div
@@ -664,7 +680,7 @@ export const EffectsConfiguration = ({
})}
>
<H
lvl={2}
lvl={3}
style={{
marginBottom: '0.4rem',
}}
@@ -680,6 +696,7 @@ export const EffectsConfiguration = ({
paddingBottom: '0.5rem',
flexWrap: 'wrap',
})}
role="list"
>
{createFileMutation.isPending &&
fileBeingUploadedObjectUrlRef.current && (
@@ -721,48 +738,54 @@ export const EffectsConfiguration = ({
className={
'hoverGroup ' + css({ position: 'relative' })
}
role="listitem"
>
<VisualOnlyTooltip tooltip={option.tooltip}>
<ToggleButton
variant="bigSquare"
aria-label={option.tooltip}
isDisabled={processorOptions.isDisabled}
onChange={getHandleSelectChangeFile(option.file)}
isSelected={option.isSelected}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${option.file.url!})`,
}}
data-attr={`toggle-virtual-${option.file.id}`}
/>
</VisualOnlyTooltip>
<Button
className={
'hoverGroupChild ' +
css({
position: 'absolute',
top: '-8px',
right: '-8px',
transition: 'opacity 0.2s ease-in-out',
})
}
size={'xs'}
variant={'tertiary'}
onClick={() => {
if (option.isSelected) {
// we remove the current effect
toggleEffect(option.config)
<div role="group" aria-label={option.file.title}>
<VisualOnlyTooltip tooltip={option.tooltip}>
<ToggleButton
variant="bigSquare"
aria-label={option.ariaLabel}
isDisabled={processorOptions.isDisabled}
onChange={getHandleSelectChangeFile(
option.file
)}
isSelected={option.isSelected}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${option.file.url!})`,
}}
data-attr={`toggle-virtual-${option.file.id}`}
/>
</VisualOnlyTooltip>
<Button
className={
'hoverGroupChild ' +
css({
position: 'absolute',
top: '-8px',
right: '-8px',
transition: 'opacity 0.2s ease-in-out',
})
}
deleteFileMutation.mutate({
fileId: option.file.id,
})
}}
isDisabled={deleteFileMutation.isPending}
>
<RiDeleteBinLine size={16} />
</Button>
aria-label={option.ariaDeleteLabel}
size={'xs'}
variant={'tertiary'}
onClick={() => {
if (option.isSelected) {
// we remove the current effect
toggleEffect(option.config)
}
deleteFileMutation.mutate({
fileId: option.file.id,
})
}}
isDisabled={deleteFileMutation.isPending}
>
<RiDeleteBinLine size={16} />
</Button>
</div>
</div>
)
)}
@@ -775,9 +798,13 @@ export const EffectsConfiguration = ({
>
<ToggleButton
variant="bigSquare"
aria-label={
uploadNotPossibleSnap.imageBackgroundConfig.label
}
aria-label={`${t(
deriveIdFromProcessorConfig(
uploadNotPossibleSnap.imageBackgroundConfig
) === selectedId
? `virtual.selectedLabel`
: `virtual.apply`
)} ${uploadNotPossibleSnap.imageBackgroundConfig.label}`}
isDisabled={
processorOptions.isDisabled ||
createFileMutation.isPending
@@ -857,7 +884,7 @@ export const EffectsConfiguration = ({
})}
>
<H
lvl={2}
lvl={3}
style={{
marginBottom: '0.4rem',
}}
@@ -872,24 +899,27 @@ export const EffectsConfiguration = ({
paddingBottom: '0.5rem',
flexWrap: 'wrap',
})}
role="list"
>
{processorOptions.virtualBackgrounds.map((option) => (
<VisualOnlyTooltip key={option.id} tooltip={option.tooltip}>
<ToggleButton
variant="bigSquare"
aria-label={option.ariaLabel}
isDisabled={processorOptions.isDisabled}
onChange={() => toggleEffect(option.config)}
isSelected={option.isSelected}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${option.thumbnailPath})`,
}}
data-attr={`toggle-virtual-preset-${option.index}`}
/>
</VisualOnlyTooltip>
<div role="listitem" key={option.id}>
<VisualOnlyTooltip tooltip={option.tooltip}>
<ToggleButton
variant="bigSquare"
aria-label={option.ariaLabel}
isDisabled={processorOptions.isDisabled}
onChange={() => toggleEffect(option.config)}
isSelected={option.isSelected}
className={css({
bgSize: 'cover',
})}
style={{
backgroundImage: `url(${option.thumbnailPath})`,
}}
data-attr={`toggle-virtual-preset-${option.index}`}
/>
</VisualOnlyTooltip>
</div>
))}
</div>
</div>
@@ -1,14 +1,30 @@
import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
// This is an example how to get the script path using Vite, may be different when using other build tools
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
// Use Jitsi's approach: maintain a global AudioContext variable
// and suspend/resume it as needed to manage audio state
let audioContext: AudioContext
/**
* Lazily load the WASM processor factory.
*
* '@libreaudio/la-call' pulls in a WebAssembly payload, so we defer importing
* it until a processor is actually initialized rather than at module load.
* The import promise is cached so repeated init() calls reuse the same module.
*/
type CreateWasmProcessor =
(typeof import('@libreaudio/la-call'))['createWasmProcessor']
let wasmProcessorPromise: Promise<CreateWasmProcessor> | undefined
function loadWasmProcessor(): Promise<CreateWasmProcessor> {
if (!wasmProcessorPromise) {
wasmProcessorPromise = import('@libreaudio/la-call').then(
(mod) => mod.createWasmProcessor
)
}
return wasmProcessorPromise
}
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
name: string
}
@@ -20,7 +36,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
private source?: MediaStreamTrack
private sourceNode?: MediaStreamAudioSourceNode
private destinationNode?: MediaStreamAudioDestinationNode
private noiseSuppressionNode?: AudioWorkletNode
private noiseSuppressionNode?: AudioNode
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
if (!opts.track) {
@@ -35,16 +51,17 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
await audioContext.resume()
}
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
this.sourceNode = audioContext.createMediaStreamSource(
new MediaStream([this.source])
)
this.noiseSuppressionNode = new AudioWorkletNode(
audioContext,
NoiseSuppressorWorklet_Name
)
const createWasmProcessor = await loadWasmProcessor()
this.noiseSuppressionNode = await createWasmProcessor(audioContext, {
intensity: 90,
})
if (!this.noiseSuppressionNode) {
throw new Error('Failed to create Wasm processor')
}
this.destinationNode = audioContext.createMediaStreamDestination()
@@ -39,6 +39,19 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
fullWidth: true,
}}
/>
<Field
type="switch"
label={t('preferences.autoMuteLargeRoom.label')}
description={t('preferences.autoMuteLargeRoom.description')}
isSelected={userPreferencesSnap.is_auto_mute_large_room_enabled}
onChange={(value) =>
(userPreferencesStore.is_auto_mute_large_room_enabled = value)
}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</TabPanel>
)
}
+16 -1
View File
@@ -14,6 +14,7 @@ import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
import { useLoginHint } from '@/hooks/useLoginHint'
import { logout } from '@/features/auth/utils/logout'
import { useMemo } from 'react'
const Logo = () => (
<img
@@ -84,6 +85,16 @@ const LoginHint = () => {
)
}
const HIDE_LOGIN_PARAM = 'hideLogin'
const isLoginButtonHidden = () => {
if (typeof window === 'undefined') return false
const value = new URLSearchParams(window.location.search).get(
HIDE_LOGIN_PARAM
)
return value === 'true'
}
export const Header = () => {
const { t } = useTranslation()
const isHome = useMatchesRoute('home')
@@ -92,6 +103,9 @@ export const Header = () => {
const isTermsOfService = useMatchesRoute('termsOfService')
const isRoom = useMatchesRoute('room')
const { user, isLoggedIn } = useUser()
const loginButtonDisabledByUrl = useMemo(() => isLoginButtonHidden(), [])
const userLabel = user?.full_name || user?.email
const loggedInTooltip = t('loggedInUserTooltip')
const loggedInAriaLabel = userLabel
@@ -152,7 +166,8 @@ export const Header = () => {
!isHome &&
!isLegalTerms &&
!isAccessibility &&
!isTermsOfService && (
!isTermsOfService &&
!loginButtonDisabledByUrl && (
<>
<LoginButton proConnectHint={false} />
<LoginHint />
@@ -14,6 +14,10 @@
"auto": "Du hast begonnen zu sprechen deine Hand wird daher automatisch gesenkt.",
"dismiss": "Hand oben lassen"
},
"autoMuteLargeRoom": {
"auto": "Sie haben an einer großen Besprechung teilgenommen. Ihr Mikrofon wurde automatisch stummgeschaltet.",
"dismiss": "Stummschaltung aufheben"
},
"reaction": {
"description": "{{name}} hat mit {{emoji}} reagiert"
},
+3
View File
@@ -290,11 +290,14 @@
"apply": "Ersetze deinen Hintergrund:",
"personal": {
"title": "Meine Hintergründe",
"selectedLabel": "Hintergrund abwählen (er ist aktuell angewendet).",
"apply": "Als Hintergrund verwenden.",
"selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll",
"notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.",
"warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.",
"uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.",
"uploadInProgress": "Datei wird hochgeladen…",
"deleteLabel": "Benutzerdefinierten Hintergrund löschen.",
"errors": {
"close": "Schließen",
"file_too_large": {
@@ -12,6 +12,10 @@
"idleDisconnectModal": {
"label": "Meetings ohne Teilnehmende verlassen",
"description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt"
},
"autoMuteLargeRoom": {
"label": "Mikrofon automatisch stummschalten",
"description": "Dein Mikrofon wird automatisch stummgeschaltet, wenn du an einer großen Besprechung teilnimmst."
}
},
"audio": {
@@ -14,6 +14,10 @@
"auto": "It seems you have started speaking, so your hand will be lowered.",
"dismiss": "Keep hand raised"
},
"autoMuteLargeRoom": {
"auto": "You have joined a large meeting. Your microphone has been automatically muted.",
"dismiss": "Unmute"
},
"reaction": {
"description": "{{name}} reacted with {{emoji}}"
},
+3
View File
@@ -290,11 +290,14 @@
"apply": "Replace your background:",
"personal": {
"title": "My backgrounds",
"selectedLabel": "Unselect background (it's currently applied).",
"apply": "Use as background.",
"selectFileTooltip": "Select an image file to use as a personal background",
"notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.",
"warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.",
"uploadLimitReached": "You cannot upload more personal backgrounds.",
"uploadInProgress": "Image is being uploaded…",
"deleteLabel": "Delete background.",
"errors": {
"close": "Close",
"file_too_large": {
+6 -2
View File
@@ -12,6 +12,10 @@
"idleDisconnectModal": {
"label": "Leave calls with no participants",
"description": "Automatically leaves a call after a few minutes if no other participant joins"
},
"autoMuteLargeRoom": {
"label": "Automatically mute my microphone",
"description": "Automatically mute your microphone when joining a large meeting."
}
},
"audio": {
@@ -21,8 +25,8 @@
"disabled": "Microphone disabled"
},
"noiseReduction": {
"label": "Noise reduction",
"heading": "Noise reduction",
"label": "Advanced voice optimization",
"heading": "Audio filters",
"ariaLabel": {
"enable": "Enable noise reduction",
"disable": "Disable noise reduction"
@@ -14,6 +14,10 @@
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
"dismiss": "Laisser la main levée"
},
"autoMuteLargeRoom": {
"auto": "Vous venez de rejoindre une grande réunion. Votre micro a été automatiquement coupé.",
"dismiss": "Rétablir le micro"
},
"reaction": {
"description": "{{name}} a reagi avec {{emoji}}"
},
+3
View File
@@ -290,11 +290,14 @@
"apply": "Remplacer votre arrière plan :",
"personal": {
"title": "Mes arrière-plans",
"selectedLabel": "Ne plus utiliser comme arrière-plan (actuellement actif).",
"apply": "Utiliser comme arrière plan.",
"selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan",
"notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.",
"warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.",
"uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.",
"uploadInProgress": "Image en cours d'envoi…",
"deleteLabel": "Supprimer l'arrière-plan.",
"errors": {
"close": "Fermer",
"file_too_large": {
@@ -12,6 +12,10 @@
"idleDisconnectModal": {
"label": "Quitter les appels sans autre participant",
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
},
"autoMuteLargeRoom": {
"label": "Couper automatiquement mon micro",
"description": "Coupe automatiquement votre micro lorsque vous rejoignez une réunion de grande taille."
}
},
"audio": {
@@ -14,6 +14,10 @@
"auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.",
"dismiss": "Houdt uw hand opgestoken"
},
"autoMuteLargeRoom": {
"auto": "U bent zojuist lid geworden van een grote vergadering. Uw microfoon is automatisch gedempt.",
"dismiss": "Microfoon inschakelen"
},
"reaction": {
"description": "{{name}} reageerde met {{emoji}}"
},
+3
View File
@@ -290,11 +290,14 @@
"apply": "Vervang je achtergrond:",
"personal": {
"title": "Mijn achtergronden",
"selectedLabel": "Deselecteer de achtergrond (deze is momenteel toegepast).",
"apply": "Gebruik als achtergrond.",
"selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond",
"notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.",
"warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.",
"uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.",
"uploadInProgress": "Afbeelding wordt geüpload…",
"deleteLabel": "Aangepaste achtergrond verwijderen.",
"errors": {
"close": "Sluiten",
"file_too_large": {
@@ -12,6 +12,10 @@
"idleDisconnectModal": {
"label": "Verlaat oproepen zonder deelnemers",
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
},
"autoMuteLargeRoom": {
"label": "Mijn microfoon automatisch dempen",
"description": "Uw microfoon wordt automatisch gedempt wanneer u deelneemt aan een grote vergadering."
}
},
"audio": {
@@ -3,10 +3,12 @@ import { STORAGE_KEYS } from '@/utils/storageKeys'
type State = {
is_idle_disconnect_modal_enabled: boolean
is_auto_mute_large_room_enabled: boolean
}
const DEFAULT_STATE = {
is_idle_disconnect_modal_enabled: true,
is_auto_mute_large_room_enabled: true,
}
function getUserPreferencesState(): State {
@@ -23,6 +23,9 @@ frontend:
- name: outlook-addon-manifest
configMap:
name: outlook-addon-manifest
- name: frontend-nginx-config
configMap:
name: frontend-nginx-config
extraVolumeMounts:
- name: outlook-addon-config
@@ -33,9 +36,18 @@ frontend:
mountPath: /usr/share/nginx/html/addons/outlook/manifest.xml
subPath: manifest.xml
readOnly: true
- name: frontend-nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
readOnly: true
outlookAddon:
enabled: true
baseUrl: "https://meet.127.0.0.1.nip.io"
enableSourceTracking: true
feedbackForm: "https://tests.com/"
appName: "Visio"
id: "a025f0f6-757a-4790-97f3-99c66c4a5795"
frontendNginxConfig:
enabled: true
@@ -53,3 +53,6 @@ agentSubtitles:
items:
- key: cacert.pem
path: cert.pem
frontendNginxConfig:
enabled: false
@@ -0,0 +1,94 @@
{{- if .Values.frontendNginxConfig.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend-nginx-config
namespace: {{ .Release.Namespace }}
data:
default.conf: |
server {
listen 8080;
server_name localhost;
server_tokens off;
root /usr/share/nginx/html;
location = /.well-known/windows-app-web-link {
default_type application/json;
alias /usr/share/nginx/html/.well-known/windows-app-web-link;
add_header Content-Disposition "attachment; filename=windows-app-web-link";
}
# Manifest — fetched, never iframed
location = /addons/outlook/manifest.xml {
alias /usr/share/nginx/html/addons/outlook/manifest.xml;
add_header Access-Control-Allow-Origin "*";
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header X-Frame-Options "DENY";
add_header Content-Security-Policy "frame-ancestors 'none'";
}
location = /addons/outlook/assets/ {
return 404;
}
location ~* ^/addons/outlook/assets/(.+\.(?:css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot))/?$ {
root /usr/share/nginx/html;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable" always;
add_header Access-Control-Allow-Origin "*";
add_header Vary "Origin" always;
}
location = /addons/outlook/ {
return 404;
}
location ~ ^/addons/outlook(/.*)?$ {
alias /usr/share/nginx/html/addons/outlook$1;
error_page 404 =200 /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache" always;
add_header Expires 0 always;
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
set $nonce $request_id;
set $csp "default-src 'self'; upgrade-insecure-requests; ";
set $csp "${csp}frame-ancestors ${ms_domains}; ";
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
set $csp "${csp}img-src 'self' data:; ";
set $csp "${csp}font-src 'self' data:; ";
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
set $csp "${csp}frame-src 'none'; ";
set $csp "${csp}object-src 'none'; ";
set $csp "${csp}base-uri 'none'; ";
add_header Content-Security-Policy $csp;
sub_filter 'NONCE_PLACEHOLDER' $nonce;
sub_filter_once off;
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
# Serve static files
location / {
try_files $uri $uri/ /index.html;
# Add no-cache headers
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
add_header Expires 0;
}
# Optionally, handle 404 errors by redirecting to index.html
error_page 404 =200 /index.html;
}
{{- end }}
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.22
version: 0.0.23
+32 -13
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
<Id>{{ .id }}</Id>
<Version>0.0.1.0</Version>
<Version>0.0.2.0</Version>
<ProviderName>{{ .appName }}</ProviderName>
<DefaultLocale>fr-FR</DefaultLocale>
<DisplayName DefaultValue="{{ .appName }}"/>
@@ -87,9 +87,9 @@
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Add.16x16"/>
<bt:Image size="32" resid="Add.32x32"/>
<bt:Image size="80" resid="Add.80x80"/>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromMail</FunctionName>
@@ -126,9 +126,9 @@
<Description resid="GenerateLink.Tooltip"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Add.16x16"/>
<bt:Image size="32" resid="Add.32x32"/>
<bt:Image size="80" resid="Add.80x80"/>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
<bt:Image size="80" resid="Icon.80x80"/>
</Icon>
<Action xsi:type="ExecuteFunction">
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
@@ -175,15 +175,34 @@
<bt:Url id="Taskpane.Url" DefaultValue="{{ .baseUrl }}/taskpane.html"/>
</bt:Urls>
<bt:ShortStrings>
<!-- Default (French) -->
<bt:String id="GroupLabel" DefaultValue="{{ .appName }}"/>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien {{ .appName }}"/>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien {{ .appName }}">
<bt:Override Locale="en-US" Value="Add a {{ .appName }} link"/>
<bt:Override Locale="de-DE" Value="{{ .appName }}-Link hinzufügen"/>
</bt:String>
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
<bt:Override Locale="en-US" Value="Open settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
</bt:String>
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
<bt:Override Locale="en-US" Value="Settings"/>
<bt:Override Locale="de-DE" Value="Einstellungen"/>
</bt:String>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}."/>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion {{ .appName }} et l'insère dans l'événement."/>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}."/>
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion {{ .appName }} et l'insère dans l'événement.">
<bt:Override Locale="de-DE" Value="Generiert einen {{ .appName }}-Besprechungslink und fügt ihn in den Termin ein."/>
<bt:Override Locale="en-US" Value="Generates a {{ .appName }} meeting link and inserts it into the item."/>
</bt:String>
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
</bt:String>
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
</bt:String>
</bt:LongStrings>
</Resources>
</VersionOverrides>
@@ -8,6 +8,8 @@ data:
config.js: |
window.__APP_CONFIG__ = {
BASE_URL: {{ .Values.frontend.outlookAddon.baseUrl | quote }},
APP_NAME: {{ .Values.frontend.outlookAddon.appName | quote }}
APP_NAME: {{ .Values.frontend.outlookAddon.appName | quote }},
ENABLE_SOURCE_TRACKING: {{ .Values.frontend.outlookAddon.enableSourceTracking | quote }},
FEEDBACK_FORM: {{ .Values.frontend.outlookAddon.feedbackForm | quote }}
};
{{- end }}
+5
View File
@@ -448,12 +448,17 @@ frontend:
## @param frontend.outlookAddon.enabled Enable the Outlook add-in ConfigMap (config.js + manifest.xml)
## @param frontend.outlookAddon.appName Display name used in the Outlook add-in (manifest + runtime config)
## @param frontend.outlookAddon.baseUrl Public base URL where the add-in is served (no trailing slash)
## @param frontend.outlookAddon.enableSourceTracking Enable appending ?from=outlook-addon to the URL
## @param frontend.outlookAddon.feedbackForm Add a link to the footer opening a feedback form
outlookAddon:
enabled: false
appName: ""
baseUrl: ""
enableSourceTracking: false
feedbackForm: ""
id: ""
## @section posthog
posthog:
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.19.0",
"version": "1.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.19.0",
"version": "1.21.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.19.0",
"version": "1.21.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.19.0",
"version": "1.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.19.0",
"version": "1.21.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.19.0",
"version": "1.21.0",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.19.0"
version = "1.21.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+10
View File
@@ -233,6 +233,7 @@ def format_transcript(
recording_datetime: str | None,
owner_timezone: str | None,
download_link: str | None,
form_link: str | None,
) -> tuple[str, str]:
"""Format a transcription into readable content with a title.
@@ -250,6 +251,7 @@ def format_transcript(
recording_datetime=recording_datetime,
owner_timezone=owner_timezone,
download_link=download_link,
form_link=form_link,
)
@@ -346,6 +348,13 @@ def process_audio_transcribe_summarize_v2(
task_id,
)
form_base_url = settings.transcription_satisfaction_form_base_url
form_link = (
f"{form_base_url}?room_id={room}"
if (form_base_url and metadata_filename is not None)
else None
)
# Format output
content, title = format_transcript(
transcription,
@@ -355,6 +364,7 @@ def process_audio_transcribe_summarize_v2(
recording_start_at,
owner_timezone,
download_link,
form_link,
)
submit_content(content, title, email, sub)
+1
View File
@@ -122,6 +122,7 @@ class Settings(BaseSettings):
# Summary related settings
is_summary_enabled: bool = True
transcription_satisfaction_form_base_url: Optional[str] = None
# Sentry
sentry_is_enabled: bool = False
+4
View File
@@ -25,6 +25,10 @@ Einige Punkte, die wir Ihnen empfehlen zu überprüfen:
download_header_template=(
"\n*[Laden Sie hier Ihre Aufnahme herunter (externer Link)]({download_link})*\n"
),
form_footer_template=(
"\n\n*[Teilen Sie uns mit, wie Ihnen diese Transkription gefallen hat "
"(externer Link)]({form_link})*\n"
),
hallucination_replacement_text="[Text konnte nicht transkribiert werden]",
document_default_title="Transkription",
document_title_template=(
+4
View File
@@ -25,6 +25,10 @@ A few things we recommend you check:
download_header_template=(
"\n*[Download your recording (external link)]({download_link})*\n"
),
form_footer_template=(
"\n\n*[Tell us what you thought of this transcription "
"(external link)]({form_link})*\n"
),
hallucination_replacement_text="[Unable to transcribe text]",
document_default_title="Transcription",
document_title_template=(
+3
View File
@@ -25,6 +25,9 @@ Quelques points que nous vous conseillons de vérifier :
download_header_template=(
"\n*[Télécharger votre enregistrement Audio]({download_link})*\n"
),
form_footer_template=(
"\n\n*[Donnez nous votre avis sur cette transcription]({form_link})*\n"
),
hallucination_replacement_text="[Texte impossible à transcrire]",
document_default_title="Transcription",
document_title_template=(
+4
View File
@@ -25,6 +25,10 @@ Een paar punten die wij u aanraden te controleren:
download_header_template=(
"\n*[Download hier je opname (externe link)]({download_link})*\n"
),
form_footer_template=(
"\n\n*[Laat ons weten wat je van deze transcriptie vond "
"(externe link)]({form_link})*\n"
),
hallucination_replacement_text="[Tekst kon niet worden getranscribeerd]",
document_default_title="Transcriptie",
document_title_template=(
@@ -10,6 +10,7 @@ class LocaleStrings:
# transcript_formatter.py
empty_transcription: str
download_header_template: str
form_footer_template: str
hallucination_replacement_text: str
document_default_title: str
document_title_template: str
@@ -38,13 +38,14 @@ class TranscriptFormatter:
return None
def format(
def format( # noqa: PLR0913
self,
transcription,
room: str | None = None,
recording_datetime: str | None = None,
owner_timezone: str | None = None,
download_link: str | None = None,
form_link: str | None = None,
) -> Tuple[str, str]:
"""Format transcription into the final document and its title."""
segments = self._get_segments(transcription)
@@ -55,6 +56,8 @@ class TranscriptFormatter:
content = self._format_speaker(segments)
content = self._remove_hallucinations(content)
content = self._add_header(content, download_link)
if form_link:
content = self._add_footer(content, form_link)
title = self._generate_title(room, recording_datetime, owner_timezone)
@@ -93,9 +96,14 @@ class TranscriptFormatter:
header = self._locale.download_header_template.format(
download_link=download_link
)
content = header + content
return header + content
return content
def _add_footer(self, content, form_link: str | None):
if not form_link:
return content
footer = self._locale.form_footer_template.format(form_link=form_link)
return content + footer
def _generate_title(
self,
+1
View File
@@ -27,6 +27,7 @@ class TestTasks:
"owner_timezone": "UTC",
"language": None,
"download_link": "https://example.com/file.mp4",
"form_link": "https://satisfaction.com?room_id=test",
},
)
-1850
View File
File diff suppressed because it is too large Load Diff