Merge pull request #16313 from nextcloud/feat/noid/split-chat-backend

feat(splitview): add option to toggle between chat views
This commit is contained in:
Dorra 2025-11-17 17:17:43 +01:00 committed by GitHub
commit a3d644d89e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 195 additions and 14 deletions

View file

@ -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)

View file

@ -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),

View file

@ -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
*/

View file

@ -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),
];
}
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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';
}

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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"
]
}
}
},

View file

@ -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,

View file

@ -55,11 +55,20 @@
v-if="!isGuest && supportConversationsListStyle"
id="talk_appearance"
:name="t('spreed', 'Appearance & Sounds')">
<NcFormBox>
<NcFormBoxSwitch
:model-value="conversationsListStyle"
:label="t('spreed', 'Compact conversations list')"
:disabled="appearanceLoading"
@update:model-value="toggleConversationsListStyle" />
<!-- FIXME: remove v-if after implementing split view -->
<NcFormBoxSwitch
v-if="false"
:model-value="chatSplitViewEnabled"
:label="t('spreed', 'Show your chat in split view')"
:disabled="chatAppearanceLoading"
@update:model-value="toggleChatStyle" />
</NcFormBox>
<NcFormBox>
<NcFormBoxSwitch
@ -167,7 +176,7 @@ import NcHotkeyList from '@nextcloud/vue/components/NcHotkeyList'
import NcKbd from '@nextcloud/vue/components/NcKbd'
import IconFolderOpenOutline from 'vue-material-design-icons/FolderOpenOutline.vue'
import IconMicrophoneOutline from 'vue-material-design-icons/MicrophoneOutline.vue'
import { CONVERSATION, PRIVACY } from '../../constants.ts'
import { CHAT_STYLE, CONVERSATION, PRIVACY } from '../../constants.ts'
import { getTalkConfig, getTalkVersion } from '../../services/CapabilitiesManager.ts'
import { useCustomSettings } from '../../services/SettingsAPI.ts'
import { useActorStore } from '../../stores/actor.ts'
@ -228,6 +237,7 @@ export default {
return {
showSettings: false,
attachmentFolderLoading: true,
chatAppearanceLoading: false,
appearanceLoading: false,
privacyLoading: false,
playSoundsLoading: false,
@ -267,6 +277,10 @@ export default {
hideMediaSettings() {
return !this.settingsStore.showMediaSettings
},
chatSplitViewEnabled() {
return this.settingsStore.chatStyle === CHAT_STYLE.SPLIT
},
},
mounted() {
@ -350,6 +364,17 @@ export default {
this.appearanceLoading = false
},
async toggleChatStyle(value) {
this.chatAppearanceLoading = true
try {
await this.settingsStore.updateChatStyle(value ? CHAT_STYLE.SPLIT : CHAT_STYLE.UNIFIED)
showSuccess(t('spreed', 'Your personal setting has been saved'))
} catch (exception) {
showError(t('spreed', 'Error while setting personal setting'))
}
this.chatAppearanceLoading = false
},
async togglePlaySounds() {
this.playSoundsLoading = true
try {

View file

@ -47,6 +47,11 @@ export const CHAT = {
FETCH_NEW: 1,
} as const
export const CHAT_STYLE = {
SPLIT: 'split',
UNIFIED: 'unified',
} as const
export const CALL = {
RECORDING: {
OFF: 0,

View file

@ -108,6 +108,14 @@ async function setConversationsListStyle(value: string) {
return setUserConfig('spreed', 'conversations_list_style', value)
}
/**
*
* @param value
*/
async function setChatStyle(value: string) {
return setUserConfig('spreed', 'chat_style', value)
}
/**
* Set user config using provisioning API
*
@ -124,6 +132,7 @@ async function setUserConfig(appId: string, configKey: string, configValue: stri
export {
setAttachmentFolder,
setBlurVirtualBackground,
setChatStyle,
setConversationsListStyle,
setPlaySounds,
setReadStatusPrivacy,

View file

@ -12,6 +12,7 @@ import { getTalkConfig } from '../services/CapabilitiesManager.ts'
import {
setAttachmentFolder,
setBlurVirtualBackground,
setChatStyle,
setConversationsListStyle,
setReadStatusPrivacy,
setStartWithoutMedia,
@ -20,6 +21,7 @@ import {
type PRIVACY_KEYS = typeof PRIVACY[keyof typeof PRIVACY]
type LIST_STYLE_OPTIONS = 'two-lines' | 'compact'
type CHAT_STYLE_OPTIONS = 'split' | 'unified'
/**
* Store for shared items shown in RightSidebar
@ -31,6 +33,7 @@ export const useSettingsStore = defineStore('settings', () => {
const startWithoutMedia = ref<boolean | undefined>(getTalkConfig('local', 'call', 'start-without-media'))
const blurVirtualBackgroundEnabled = ref<boolean | undefined>(getTalkConfig('local', 'call', 'blur-virtual-background'))
const conversationsListStyle = ref<LIST_STYLE_OPTIONS | undefined>(getTalkConfig('local', 'conversations', 'list-style'))
const chatStyle = ref<CHAT_STYLE_OPTIONS | undefined>(getTalkConfig('local', 'chat', 'style') ?? 'split')
const attachmentFolder = ref<string>(loadState('spreed', 'attachment_folder', ''))
const attachmentFolderFreeSpace = ref<number>(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,
}
})

View file

@ -249,6 +249,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -83,6 +83,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -69,6 +69,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -184,6 +184,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -87,6 +87,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -195,6 +195,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -2391,6 +2391,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -1869,6 +1869,8 @@ export type components = {
"typing-privacy": number;
/** Format: int64 */
"summary-threshold": number;
/** @enum {string} */
style: "split" | "unified";
};
conversations: {
"can-create": boolean;

View file

@ -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,