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,