diff --git a/docs/capabilities.md b/docs/capabilities.md index 80f1c872b2..e9a377011a 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -201,3 +201,4 @@ ## 23 * `pinned-messages` - Whether messages can be pinned * `federated-shared-items` - Whether shared items endpoints can be called in a federated conversation +* `config => chat => style` (local) - User selected chat style (split or unified for now) diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 21ac028d73..a46a19711b 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -176,6 +176,7 @@ class Capabilities implements IPublicCapability { 'has-translation-task-providers', 'typing-privacy', 'summary-threshold', + 'style', ], 'conversations' => [ 'can-create', @@ -263,6 +264,7 @@ class Capabilities implements IPublicCapability { 'has-translation-task-providers' => false, 'typing-privacy' => Participant::PRIVACY_PUBLIC, 'summary-threshold' => max(1, $this->appConfig->getAppValueInt('summary_threshold', 100)), + 'style' => $this->talkConfig->getChatStyle($user?->getUID()), ], 'conversations' => [ 'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user), diff --git a/lib/Config.php b/lib/Config.php index 53c99f1e2c..636416da4b 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -757,6 +757,28 @@ class Config { return UserPreference::CONVERSATION_LIST_STYLE_TWO_LINES; } + /** + * User setting for chat style + * + * @param ?string $userId + * @return UserPreference::CHAT_STYLE_* + */ + public function getChatStyle(?string $userId): string { + if ($userId !== null) { + $userSetting = $this->config->getUserValue( + $userId, + 'spreed', + UserPreference::CHAT_STYLE, + UserPreference::CHAT_STYLE_SPLIT + ); + + if (in_array($userSetting, [UserPreference::CHAT_STYLE_SPLIT, UserPreference::CHAT_STYLE_UNIFIED], true)) { + return $userSetting; + } + } + return UserPreference::CHAT_STYLE_SPLIT; + } + /** * User setting falling back to admin defined app config */ diff --git a/lib/ConfigLexicon.php b/lib/ConfigLexicon.php index 6b1aa3ccca..4602544229 100644 --- a/lib/ConfigLexicon.php +++ b/lib/ConfigLexicon.php @@ -31,6 +31,7 @@ class ConfigLexicon implements ILexicon { public function getUserConfigs(): array { return [ new Entry(UserPreference::PLAY_SOUNDS, ValueType::BOOL, true), + new Entry(UserPreference::CHAT_STYLE, ValueType::STRING, UserPreference::CHAT_STYLE_SPLIT), ]; } } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index b55aaabd51..f8c8028475 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -524,6 +524,7 @@ namespace OCA\Talk; * has-translation-task-providers: bool, * typing-privacy: int, * summary-threshold: positive-int, + * style: 'split'|'unified', * }, * conversations: array{ * can-create: bool, diff --git a/lib/Settings/BeforePreferenceSetEventListener.php b/lib/Settings/BeforePreferenceSetEventListener.php index f228baecfa..b25536be9b 100644 --- a/lib/Settings/BeforePreferenceSetEventListener.php +++ b/lib/Settings/BeforePreferenceSetEventListener.php @@ -81,6 +81,9 @@ class BeforePreferenceSetEventListener implements IEventListener { if ($key === UserPreference::CONVERSATIONS_LIST_STYLE) { return $value === UserPreference::CONVERSATION_LIST_STYLE_TWO_LINES || $value === UserPreference::CONVERSATION_LIST_STYLE_COMPACT; } + if ($key === UserPreference::CHAT_STYLE) { + return $value === UserPreference::CHAT_STYLE_SPLIT || $value === UserPreference::CHAT_STYLE_UNIFIED; + } return false; } diff --git a/lib/Settings/UserPreference.php b/lib/Settings/UserPreference.php index ab5d9594f6..ab88c46691 100644 --- a/lib/Settings/UserPreference.php +++ b/lib/Settings/UserPreference.php @@ -13,6 +13,7 @@ class UserPreference { public const BLUR_VIRTUAL_BACKGROUND = 'blur_virtual_background'; public const CALLS_START_WITHOUT_MEDIA = 'calls_start_without_media'; public const CONVERSATIONS_LIST_STYLE = 'conversations_list_style'; + public const CHAT_STYLE = 'chat_style'; public const PLAY_SOUNDS = 'play_sounds'; public const TYPING_PRIVACY = 'typing_privacy'; public const READ_STATUS_PRIVACY = 'read_status_privacy'; @@ -20,4 +21,7 @@ class UserPreference { public const CONVERSATION_LIST_STYLE_TWO_LINES = 'two-lines'; public const CONVERSATION_LIST_STYLE_COMPACT = 'compact'; + + public const CHAT_STYLE_SPLIT = 'split'; + public const CHAT_STYLE_UNIFIED = 'unified'; } diff --git a/openapi-administration.json b/openapi-administration.json index ee71cda388..97284c754a 100644 --- a/openapi-administration.json +++ b/openapi-administration.json @@ -230,7 +230,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -255,6 +256,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-backend-recording.json b/openapi-backend-recording.json index 31a0c91d34..516d85f7d4 100644 --- a/openapi-backend-recording.json +++ b/openapi-backend-recording.json @@ -163,7 +163,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -188,6 +189,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-backend-signaling.json b/openapi-backend-signaling.json index 0c38db9d61..94aa669b4f 100644 --- a/openapi-backend-signaling.json +++ b/openapi-backend-signaling.json @@ -163,7 +163,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -188,6 +189,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index 99c622f241..18185dc434 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -206,7 +206,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -231,6 +232,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-bots.json b/openapi-bots.json index 38f5faf059..74ff534c86 100644 --- a/openapi-bots.json +++ b/openapi-bots.json @@ -163,7 +163,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -188,6 +189,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-federation.json b/openapi-federation.json index 4081cd3f1f..ff4a4a6999 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -206,7 +206,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -231,6 +232,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi-full.json b/openapi-full.json index 37b0d01026..3afe12982c 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -364,7 +364,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -389,6 +390,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/openapi.json b/openapi.json index f7c5be1459..b6c8d71ba4 100644 --- a/openapi.json +++ b/openapi.json @@ -323,7 +323,8 @@ "has-translation-providers", "has-translation-task-providers", "typing-privacy", - "summary-threshold" + "summary-threshold", + "style" ], "properties": { "max-length": { @@ -348,6 +349,13 @@ "type": "integer", "format": "int64", "minimum": 1 + }, + "style": { + "type": "string", + "enum": [ + "split", + "unified" + ] } } }, diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts index 9773b370c0..326ea4cc6c 100644 --- a/src/__mocks__/capabilities.ts +++ b/src/__mocks__/capabilities.ts @@ -159,6 +159,7 @@ export const mockedCapabilities: Capabilities = { 'has-translation-task-providers': true, 'typing-privacy': 0, 'summary-threshold': 100, + style: 'split', }, conversations: { 'can-create': true, diff --git a/src/components/SettingsDialog/SettingsDialog.vue b/src/components/SettingsDialog/SettingsDialog.vue index dbdb53f549..df32ec59a2 100644 --- a/src/components/SettingsDialog/SettingsDialog.vue +++ b/src/components/SettingsDialog/SettingsDialog.vue @@ -55,11 +55,20 @@ v-if="!isGuest && supportConversationsListStyle" id="talk_appearance" :name="t('spreed', 'Appearance & Sounds')"> - + + + + + { const startWithoutMedia = ref(getTalkConfig('local', 'call', 'start-without-media')) const blurVirtualBackgroundEnabled = ref(getTalkConfig('local', 'call', 'blur-virtual-background')) const conversationsListStyle = ref(getTalkConfig('local', 'conversations', 'list-style')) + const chatStyle = ref(getTalkConfig('local', 'chat', 'style') ?? 'split') const attachmentFolder = ref(loadState('spreed', 'attachment_folder', '')) const attachmentFolderFreeSpace = ref(loadState('spreed', 'attachment_folder_free_space', 0)) @@ -105,6 +108,16 @@ export const useSettingsStore = defineStore('settings', () => { attachmentFolder.value = value } + /** + * Update the conversations list style setting for the user + * + * @param value - new selected state + */ + async function updateChatStyle(value: CHAT_STYLE_OPTIONS) { + await setChatStyle(value) + chatStyle.value = value + } + return { readStatusPrivacy, typingStatusPrivacy, @@ -114,6 +127,7 @@ export const useSettingsStore = defineStore('settings', () => { conversationsListStyle, attachmentFolder, attachmentFolderFreeSpace, + chatStyle, updateReadStatusPrivacy, updateTypingStatusPrivacy, @@ -122,5 +136,6 @@ export const useSettingsStore = defineStore('settings', () => { updateStartWithoutMedia, updateConversationsListStyle, updateAttachmentFolder, + updateChatStyle, } }) diff --git a/src/types/openapi/openapi-administration.ts b/src/types/openapi/openapi-administration.ts index b34a866904..9c3ccd6a02 100644 --- a/src/types/openapi/openapi-administration.ts +++ b/src/types/openapi/openapi-administration.ts @@ -249,6 +249,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-backend-recording.ts b/src/types/openapi/openapi-backend-recording.ts index 117aca838f..283f3227ad 100644 --- a/src/types/openapi/openapi-backend-recording.ts +++ b/src/types/openapi/openapi-backend-recording.ts @@ -83,6 +83,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-backend-signaling.ts b/src/types/openapi/openapi-backend-signaling.ts index 2185cd3473..3b92cc0dac 100644 --- a/src/types/openapi/openapi-backend-signaling.ts +++ b/src/types/openapi/openapi-backend-signaling.ts @@ -69,6 +69,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 2c42f836fe..f465b58780 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -184,6 +184,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-bots.ts b/src/types/openapi/openapi-bots.ts index 1c80ac17f3..7bcb2d8078 100644 --- a/src/types/openapi/openapi-bots.ts +++ b/src/types/openapi/openapi-bots.ts @@ -87,6 +87,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 1d6414ee9f..ff302c9885 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -195,6 +195,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 7a724159d4..20053457d8 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -2391,6 +2391,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index b598fcb5dc..1dfd1afc8f 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1869,6 +1869,8 @@ export type components = { "typing-privacy": number; /** Format: int64 */ "summary-threshold": number; + /** @enum {string} */ + style: "split" | "unified"; }; conversations: { "can-create": boolean; diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index 2dd7d3d8e1..18d5277ce1 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -104,6 +104,11 @@ class CapabilitiesTest extends TestCase { ->method('isBreakoutRoomsEnabled') ->willReturn(false); + $this->talkConfig->expects($this->once()) + ->method('getChatStyle') + ->with(null) + ->willReturn('split'); + $this->serverConfig->expects($this->any()) ->method('getAppValue') ->willReturnMap([ @@ -181,6 +186,7 @@ class CapabilitiesTest extends TestCase { 'has-translation-task-providers' => false, 'typing-privacy' => 0, 'summary-threshold' => 100, + 'style' => 'split', ], 'conversations' => [ 'can-create' => false, @@ -247,6 +253,11 @@ class CapabilitiesTest extends TestCase { ->with('uid') ->willReturn('/Talk'); + $this->talkConfig->expects($this->once()) + ->method('getChatStyle') + ->with('uid') + ->willReturn('split'); + $this->talkConfig->expects($this->once()) ->method('isNotAllowedToCreateConversations') ->with($user) @@ -353,6 +364,7 @@ class CapabilitiesTest extends TestCase { 'has-translation-task-providers' => false, 'typing-privacy' => 0, 'summary-threshold' => 100, + 'style' => 'split', ], 'conversations' => [ 'can-create' => $canCreate,