diff --git a/docs/capabilities.md b/docs/capabilities.md
index 7fdd4f639b3..b7a26f7fe16 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -158,3 +158,4 @@
## 20.1
* `archived-conversations` (local) - Conversations can be marked as archived which will hide them from the conversation list by default
+* `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation
diff --git a/docs/settings.md b/docs/settings.md
index c1d0e61848d..6093c41af7e 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -21,11 +21,16 @@
## User settings
-| Key | Capability | Default | Valid values |
-|-----------------------|------------------------------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------|
-| `attachment_folder` | `config => attachments => folder` | Value of app config `default_attachment_folder` | Path owned by the user to store uploads and received shares. It is created if it does not exist. |
-| `read_status_privacy` | `config => chat => read-privacy` | `0` | One of the read-status constants from the [constants list](constants.md#participant-read-status-privacy) |
-| `typing_privacy` | `config => chat => typing-privacy` | `0` | One of the typing privacy constants from the [constants list](constants.md#participant-typing-privacy) |
+**Note:** Settings from `calls_start_without_media` onwards can not be set via above API.
+Instead, the server API `POST /ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}` needs to be used.
+
+| Key | Capability | Default | Valid values |
+|-----------------------------|-----------------------------------------|----------------------------------------------------|----------------------------------------------------------------------------------------------------------|
+| `attachment_folder` | `config => attachments => folder` | Value of app config `default_attachment_folder` | Path owned by the user to store uploads and received shares. It is created if it does not exist. |
+| `read_status_privacy` | `config => chat => read-privacy` | `0` | One of the read-status constants from the [constants list](constants.md#participant-read-status-privacy) |
+| `typing_privacy` | `config => chat => typing-privacy` | `0` | One of the typing privacy constants from the [constants list](constants.md#participant-typing-privacy) |
+| `play_sounds` | | `'yes'` | `'yes'` and `'no'` |
+| `calls_start_without_media` | `config => call => start-without-media` | `''` falling back to app config with the same name | `'yes'` and `'no'` |
## Set SIP settings
@@ -94,6 +99,7 @@ Legend:
| `changelog` | string
`yes` or `no` | `yes` | No | | Whether the changelog conversation is updated with new features on major releases |
| `has_reference_id` | string
`yes` or `no` | `no` | Yes | | Indicator whether the clients can use the reference value to identify their message, will be automatically set to `yes` when the repair steps are executed |
| `hide_signaling_warning` | string
`yes` or `no` | `no` | No | 🖌️ | Flag that allows to suppress the warning that an HPB should be configured |
+| `calls_start_without_media` | string
`yes` or `no` | `no` | Yes | | Whether participants start with enabled or disabled audio and video by default |
| `breakout_rooms` | string
`yes` or `no` | `yes` | Yes | | Whether or not breakout rooms are allowed (Will only prevent creating new breakout rooms. Existing conversations are not modified.) |
| `call_recording` | string
`yes` or `no` | `yes` | Yes | | Enable call recording |
| `call_recording_transcription` | string
`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcripted when a transcription provider is enabled. |
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index eb0eb165f8b..903aa418bf8 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -106,6 +106,7 @@
use OCA\Talk\Search\MessageSearch;
use OCA\Talk\Search\UnifiedSearchCSSLoader;
use OCA\Talk\Search\UnifiedSearchFilterPlugin;
+use OCA\Talk\Settings\BeforePreferenceSetEventListener;
use OCA\Talk\Settings\Personal;
use OCA\Talk\SetupCheck\BackgroundBlurLoading;
use OCA\Talk\SetupCheck\FederationLockCache;
@@ -124,6 +125,7 @@
use OCP\Collaboration\AutoComplete\AutoCompleteFilterEvent;
use OCP\Collaboration\Resources\IProviderManager;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
+use OCP\Config\BeforePreferenceSetEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
@@ -177,6 +179,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareTemplateLoader::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareAuthTemplateLoader::class);
$context->registerEventListener(LoadSidebar::class, FilesTemplateLoader::class);
+ $context->registerEventListener(BeforePreferenceSetEvent::class, BeforePreferenceSetEventListener::class);
// Activity listeners
$context->registerEventListener(AttendeesAddedEvent::class, ActivityListener::class);
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 0cff6f89ae4..24fe017da7d 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -127,6 +127,7 @@ class Capabilities implements IPublicCapability {
'call' => [
'predefined-backgrounds',
'can-upload-background',
+ 'start-without-media',
],
'chat' => [
'read-privacy',
@@ -196,6 +197,7 @@ public function getCapabilities(): array {
'sip-enabled' => $this->talkConfig->isSIPConfigured(),
'sip-dialout-enabled' => $this->talkConfig->isSIPDialOutEnabled(),
'can-enable-sip' => false,
+ 'start-without-media' => $this->talkConfig->getCallsStartWithoutMedia($user?->getUID()),
],
'chat' => [
'max-length' => ChatManager::MAX_CHAT_LENGTH,
diff --git a/lib/Config.php b/lib/Config.php
index 37c4198e619..2019731d976 100644
--- a/lib/Config.php
+++ b/lib/Config.php
@@ -12,6 +12,7 @@
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\RecordingService;
+use OCA\Talk\Settings\UserPreference;
use OCA\Talk\Vendor\Firebase\JWT\JWT;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -662,4 +663,21 @@ public function getGridVideosLimit(): int {
public function getGridVideosLimitEnforced(): bool {
return $this->config->getAppValue('spreed', 'grid_videos_limit_enforced', 'no') === 'yes';
}
+
+ /**
+ * User setting falling back to admin defined app config
+ *
+ * @param ?string $userId
+ * @return bool
+ */
+ public function getCallsStartWithoutMedia(?string $userId): bool {
+ if ($userId !== null) {
+ $userSetting = $this->config->getUserValue($userId, 'spreed', UserPreference::CALLS_START_WITHOUT_MEDIA);
+ if ($userSetting === 'yes' || $userSetting === 'no') {
+ return $userSetting === 'yes';
+ }
+ }
+
+ return $this->appConfig->getAppValueBool('calls_start_without_media');
+ }
}
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index d8b47b9604f..08ff87eff08 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -2436,6 +2436,9 @@ public function getCapabilities(): DataResponse {
if (isset($data['config']['chat']['typing-privacy'])) {
$data['config']['chat']['typing-privacy'] = Participant::PRIVACY_PRIVATE;
}
+ if (isset($data['config']['call']['start-without-media'])) {
+ $data['config']['call']['start-without-media'] = $this->talkConfig->getCallsStartWithoutMedia($this->userId);
+ }
if ($response->getHeaders()['X-Nextcloud-Talk-Hash']) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $response->getHeaders()['X-Nextcloud-Talk-Hash'];
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 1989d90f8ee..6dc4abef085 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -8,19 +8,13 @@
namespace OCA\Talk\Controller;
-use OCA\Files_Sharing\SharedStorage;
-use OCA\Talk\Model\Attendee;
-use OCA\Talk\Participant;
-use OCA\Talk\Service\ParticipantService;
+use OCA\Talk\Settings\BeforePreferenceSetEventListener;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
-use OCP\Files\Folder;
use OCP\Files\IRootFolder;
-use OCP\Files\NotFoundException;
-use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
@@ -35,8 +29,8 @@ public function __construct(
protected IRootFolder $rootFolder,
protected IConfig $config,
protected IGroupManager $groupManager,
- protected ParticipantService $participantService,
protected LoggerInterface $logger,
+ protected BeforePreferenceSetEventListener $preferenceListener,
protected ?string $userId,
) {
parent::__construct($appName, $request);
@@ -54,57 +48,15 @@ public function __construct(
*/
#[NoAdminRequired]
public function setUserSetting(string $key, $value): DataResponse {
- if (!$this->validateUserSetting($key, $value)) {
+ if (!$this->preferenceListener->validatePreference($this->userId, $key, $value)) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$this->config->setUserValue($this->userId, 'spreed', $key, $value);
- if ($key === 'read_status_privacy') {
- $this->participantService->updateReadPrivacyForActor(Attendee::ACTOR_USERS, $this->userId, (int)$value);
- }
-
return new DataResponse();
}
- /**
- * @param string $setting
- * @param int|null|string $value
- * @return bool
- */
- protected function validateUserSetting(string $setting, $value): bool {
- if ($setting === 'attachment_folder') {
- $userFolder = $this->rootFolder->getUserFolder($this->userId);
- try {
- $node = $userFolder->get($value);
- if (!$node instanceof Folder) {
- throw new NotPermittedException('Node is not a directory');
- }
- if ($node->isShared()) {
- throw new NotPermittedException('Folder is shared');
- }
- return !$node->getStorage()->instanceOfStorage(SharedStorage::class);
- } catch (NotFoundException $e) {
- $userFolder->newFolder($value);
- return true;
- } catch (NotPermittedException $e) {
- } catch (\Exception $e) {
- $this->logger->error($e->getMessage(), ['exception' => $e]);
- }
- return false;
- }
-
- if ($setting === 'typing_privacy' || $setting === 'read_status_privacy') {
- return (int)$value === Participant::PRIVACY_PUBLIC ||
- (int)$value === Participant::PRIVACY_PRIVATE;
- }
- if ($setting === 'play_sounds') {
- return $value === 'yes' || $value === 'no';
- }
-
- return false;
- }
-
/**
* Update SIP bridge settings
*
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index 7344e078f6c..7f55a770b35 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -339,6 +339,7 @@
* sip-enabled: bool,
* sip-dialout-enabled: bool,
* can-enable-sip: bool,
+ * start-without-media: bool,
* },
* chat: array{
* max-length: int,
diff --git a/lib/Settings/BeforePreferenceSetEventListener.php b/lib/Settings/BeforePreferenceSetEventListener.php
new file mode 100644
index 00000000000..31196b10014
--- /dev/null
+++ b/lib/Settings/BeforePreferenceSetEventListener.php
@@ -0,0 +1,101 @@
+
+ */
+class BeforePreferenceSetEventListener implements IEventListener {
+ public function __construct(
+ protected IRootFolder $rootFolder,
+ protected ParticipantService $participantService,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof BeforePreferenceSetEvent)) {
+ // Unrelated
+ return;
+ }
+
+ if ($event->getAppId() !== Application::APP_ID) {
+ return;
+ }
+
+ $event->setValid($this->validatePreference(
+ $event->getUserId(),
+ $event->getConfigKey(),
+ $event->getConfigValue(),
+ ));
+ }
+
+ /**
+ * @internal Make private/protected once SettingsController route was removed
+ */
+ public function validatePreference(string $userId, string $key, string $value): bool {
+ if ($key === 'attachment_folder') {
+ return $this->validateAttachmentFolder($userId, $value);
+ }
+
+ // "boolean" yes/no
+ if ($key === UserPreference::CALLS_START_WITHOUT_MEDIA
+ || $key === UserPreference::PLAY_SOUNDS) {
+ return $value === 'yes' || $value === 'no';
+ }
+
+ // "privacy" 0/1
+ if ($key === UserPreference::TYPING_PRIVACY
+ || $key === UserPreference::READ_STATUS_PRIVACY) {
+ $valid = $value === (string)Participant::PRIVACY_PRIVATE || $value === (string)Participant::PRIVACY_PUBLIC;
+
+ if ($valid && $key === 'read_status_privacy') {
+ $this->participantService->updateReadPrivacyForActor(Attendee::ACTOR_USERS, $userId, (int)$value);
+ }
+ return $valid;
+ }
+
+ return false;
+ }
+
+ protected function validateAttachmentFolder(string $userId, string $value): bool {
+ try {
+ $userFolder = $this->rootFolder->getUserFolder($userId);
+ $node = $userFolder->get($value);
+ if (!$node instanceof Folder) {
+ throw new NotPermittedException('Node is not a directory');
+ }
+ if ($node->isShared()) {
+ throw new NotPermittedException('Folder is shared');
+ }
+ return !$node->getStorage()->instanceOfStorage(SharedStorage::class);
+ } catch (NotFoundException) {
+ $userFolder->newFolder($value);
+ return true;
+ } catch (NotPermittedException) {
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ return false;
+ }
+}
diff --git a/lib/Settings/UserPreference.php b/lib/Settings/UserPreference.php
new file mode 100644
index 00000000000..f360172f20e
--- /dev/null
+++ b/lib/Settings/UserPreference.php
@@ -0,0 +1,16 @@
+ false,
'sip-dialout-enabled' => false,
'can-enable-sip' => false,
+ 'start-without-media' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',
@@ -252,6 +253,7 @@ public function testGetCapabilitiesUserAllowed(bool $isNotAllowed, bool $canCrea
'sip-enabled' => false,
'sip-dialout-enabled' => false,
'can-enable-sip' => false,
+ 'start-without-media' => false,
'predefined-backgrounds' => [
'1_office.jpg',
'2_home.jpg',