Skip to content

Commit ba23a61

Browse files
Rikdekkerclaude
andcommitted
feat(sidebar): Add conversation sections and sort options
Allow users to organize conversations into custom sections with drag-and-drop reordering, and sort conversations by recent activity, alphabetical order, or groups first. Resolves #12025 Signed-off-by: Rikdekker <rik@shalution.nl> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cda8acc commit ba23a61

26 files changed

Lines changed: 1490 additions & 27 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
# Changelog
66
All notable changes to this project will be documented in this file.
77

8+
## Unreleased
9+
### Added
10+
- Allow users to organize conversations into custom sections with drag-and-drop reordering
11+
[#12025](https://github.com/nextcloud/spreed/issues/12025)
12+
- Add sort options for conversations: recent activity, alphabetical, and groups first
13+
[#12025](https://github.com/nextcloud/spreed/issues/12025)
14+
815
## 23.0.1 – 2026-02-18
916
### Fixed
1017
- fix(sharing): Fix type error when a share is loaded before the user loaded all their shares

appinfo/info.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
1919
]]></description>
2020

21-
<version>24.0.0-dev.2</version>
21+
<version>23.0.8</version>
2222
<licence>agpl</licence>
2323

2424
<author>Anna Larch</author>
@@ -62,7 +62,7 @@
6262
<screenshot>https://raw.githubusercontent.com/nextcloud/spreed/main/docs/video-verfication.png</screenshot>
6363

6464
<dependencies>
65-
<nextcloud min-version="34" max-version="34" />
65+
<nextcloud min-version="33" max-version="33" />
6666
</dependencies>
6767

6868
<background-jobs>

docs/capabilities.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@
209209
* `config => call => live-translation` - Whether live translation is supported in calls
210210
* `config => call => live-transcription-target-language-id` (local) - User defined string value with the id of the target language to use for live translations
211211

212+
## 23.x (Conversation sections and sort options)
213+
* `conversation-sections` (local) - Whether the user can create custom sections to organize conversations in the sidebar. See [Conversation sections API](conversation-sections.md) for details.
214+
* `config => conversations => sort-mode` (local) - User selected sort mode for conversations (`activity`, `alphabetical`, or `type-first`)
215+
212216
## 24
213217
* `react-permission` - When permission 256 is required to add reactions (previously handled by the chat permission)
214218
* `config => permissions => max-default` - Maximum value for default permissions (510 with react-permission, 254 without)

docs/conversation-sections.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Conversation sections API
2+
3+
* API v4: Base endpoint `/ocs/v2.php/apps/spreed/api/v4`: since Nextcloud 33
4+
5+
Conversation sections allow users to organize their conversations into custom groups in the sidebar. Each user manages their own sections independently. Favorites are always shown at the top, unsectioned conversations appear in an "Other" group at the bottom.
6+
7+
## Capabilities
8+
9+
* `conversation-sections` - Whether the server supports custom conversation sections
10+
11+
## Section object
12+
13+
| field | type | Description |
14+
|-------------|--------|-----------------------------------------------------|
15+
| `id` | int | Unique identifier for the section |
16+
| `name` | string | Display name of the section |
17+
| `sortOrder` | int | Position of the section in the list (0-based) |
18+
| `collapsed` | bool | Whether the section is collapsed (stored on client) |
19+
20+
## Get all sections
21+
22+
* Method: `GET`
23+
* Endpoint: `/sections`
24+
* Response:
25+
- Status code:
26+
+ `200 OK`
27+
28+
- Data: Array of section objects, ordered by `sortOrder`
29+
30+
## Create a section
31+
32+
* Method: `POST`
33+
* Endpoint: `/sections`
34+
* Data:
35+
36+
| field | type | Description |
37+
|--------|--------|------------------------------|
38+
| `name` | string | Name of the new section |
39+
40+
* Response:
41+
- Status code:
42+
+ `201 Created`
43+
44+
- Data: The created section object
45+
46+
## Update a section
47+
48+
* Method: `PUT`
49+
* Endpoint: `/sections/{sectionId}`
50+
* Data:
51+
52+
| field | type | Description |
53+
|--------|--------|------------------------------|
54+
| `name` | string | New name for the section |
55+
56+
* Response:
57+
- Status code:
58+
+ `200 OK`
59+
+ `404 Not Found` when the section does not exist or belongs to another user
60+
61+
- Data: The updated section object
62+
63+
## Delete a section
64+
65+
* Method: `DELETE`
66+
* Endpoint: `/sections/{sectionId}`
67+
* Response:
68+
- Status code:
69+
+ `200 OK`
70+
+ `404 Not Found` when the section does not exist or belongs to another user
71+
72+
!!! note
73+
74+
When a section is deleted, all conversations assigned to that section are automatically unassigned (moved to the "Other" group).
75+
76+
## Reorder sections
77+
78+
* Method: `PUT`
79+
* Endpoint: `/sections/reorder`
80+
* Data:
81+
82+
| field | type | Description |
83+
|--------------|------------|--------------------------------------------------|
84+
| `orderedIds` | list\<int> | Ordered list of all section IDs in desired order |
85+
86+
* Response:
87+
- Status code:
88+
+ `200 OK`
89+
90+
- Data: Array of all section objects with updated `sortOrder` values
91+
92+
## Assign a conversation to a section
93+
94+
* Method: `POST`
95+
* Endpoint: `/room/{token}/section`
96+
* Data:
97+
98+
| field | type | Description |
99+
|-------------|------|--------------------------------------------------------|
100+
| `sectionId` | int | ID of the section to assign, or `0` to unassign |
101+
102+
* Response:
103+
- Status code:
104+
+ `200 OK`
105+
+ `404 Not Found` when the conversation or section does not exist
106+
107+
!!! note
108+
109+
Favorite and archived conversations cannot be assigned to a section. The `sectionId` is returned as part of the conversation object in the [conversation API](conversation.md).
110+
111+
## Sort modes
112+
113+
Conversations within each section can be sorted using a client-side sort mode stored in browser storage:
114+
115+
| Mode | Behavior |
116+
|------------------|-------------------------------------------------------------------------|
117+
| `activity` | Sort by most recent activity (default) |
118+
| `alphabetical` | Sort alphabetically by display name |
119+
| `type-first` | Group conversations first, then one-to-one, each sub-group by activity |
120+
121+
The sort mode applies within each section independently. Section order is determined by the `sortOrder` field and can be changed via the reorder endpoint.

lib/Capabilities.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class Capabilities implements IPublicCapability {
131131
'federated-shared-items',
132132
'scheduled-messages',
133133
'conversation-presets',
134+
'conversation-sections',
134135
];
135136

136137
public const CONDITIONAL_FEATURES = [
@@ -163,6 +164,7 @@ class Capabilities implements IPublicCapability {
163164
'sensitive-conversations',
164165
'scheduled-messages',
165166
'conversation-presets',
167+
'conversation-sections',
166168
];
167169

168170
public const LOCAL_CONFIGS = [
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Talk\Controller;
11+
12+
use OCA\Talk\Model\ConversationSection;
13+
use OCA\Talk\ResponseDefinitions;
14+
use OCA\Talk\Service\ConversationSectionService;
15+
use OCP\AppFramework\Db\DoesNotExistException;
16+
use OCP\AppFramework\Http;
17+
use OCP\AppFramework\Http\Attribute\ApiRoute;
18+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
19+
use OCP\AppFramework\Http\DataResponse;
20+
use OCP\AppFramework\OCSController;
21+
use OCP\IRequest;
22+
23+
/**
24+
* @psalm-import-type TalkConversationSection from ResponseDefinitions
25+
*/
26+
class ConversationSectionController extends OCSController {
27+
public function __construct(
28+
string $appName,
29+
IRequest $request,
30+
protected ConversationSectionService $sectionService,
31+
protected ?string $userId,
32+
) {
33+
parent::__construct($appName, $request);
34+
}
35+
36+
/**
37+
* Get all conversation sections for the current user
38+
*
39+
* Required capability: `conversation-sections`
40+
*
41+
* @return DataResponse<Http::STATUS_OK, list<TalkConversationSection>, array{}>
42+
*
43+
* 200: Sections returned
44+
*/
45+
#[NoAdminRequired]
46+
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/sections', requirements: [
47+
'apiVersion' => '(v4)',
48+
])]
49+
public function getSections(): DataResponse {
50+
$sections = $this->sectionService->getSections($this->userId);
51+
return new DataResponse(array_values(array_map([$this, 'formatSection'], $sections)));
52+
}
53+
54+
/**
55+
* Create a new conversation section
56+
*
57+
* Required capability: `conversation-sections`
58+
*
59+
* @param string $name Name of the section
60+
* @return DataResponse<Http::STATUS_CREATED, TalkConversationSection, array{}>
61+
*
62+
* 201: Section created
63+
*/
64+
#[NoAdminRequired]
65+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sections', requirements: [
66+
'apiVersion' => '(v4)',
67+
])]
68+
public function createSection(string $name): DataResponse {
69+
$section = $this->sectionService->createSection($this->userId, $name);
70+
return new DataResponse($this->formatSection($section), Http::STATUS_CREATED);
71+
}
72+
73+
/**
74+
* Update a conversation section
75+
*
76+
* Required capability: `conversation-sections`
77+
*
78+
* @param int $sectionId ID of the section
79+
* @param string $name New name for the section
80+
* @return DataResponse<Http::STATUS_OK, TalkConversationSection, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
81+
*
82+
* 200: Section updated
83+
* 404: Section not found
84+
*/
85+
#[NoAdminRequired]
86+
#[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/sections/{sectionId}', requirements: [
87+
'apiVersion' => '(v4)',
88+
'sectionId' => '\d+',
89+
])]
90+
public function updateSection(int $sectionId, string $name): DataResponse {
91+
try {
92+
$section = $this->sectionService->updateSection($sectionId, $this->userId, $name);
93+
return new DataResponse($this->formatSection($section));
94+
} catch (DoesNotExistException) {
95+
return new DataResponse(null, Http::STATUS_NOT_FOUND);
96+
}
97+
}
98+
99+
/**
100+
* Delete a conversation section
101+
*
102+
* Required capability: `conversation-sections`
103+
*
104+
* @param int $sectionId ID of the section
105+
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
106+
*
107+
* 200: Section deleted
108+
* 404: Section not found
109+
*/
110+
#[NoAdminRequired]
111+
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/sections/{sectionId}', requirements: [
112+
'apiVersion' => '(v4)',
113+
'sectionId' => '\d+',
114+
])]
115+
public function deleteSection(int $sectionId): DataResponse {
116+
try {
117+
$this->sectionService->deleteSection($sectionId, $this->userId);
118+
return new DataResponse(null);
119+
} catch (DoesNotExistException) {
120+
return new DataResponse(null, Http::STATUS_NOT_FOUND);
121+
}
122+
}
123+
124+
/**
125+
* Reorder conversation sections
126+
*
127+
* Required capability: `conversation-sections`
128+
*
129+
* @param list<int> $orderedIds Ordered list of section IDs
130+
* @return DataResponse<Http::STATUS_OK, list<TalkConversationSection>, array{}>
131+
*
132+
* 200: Sections reordered
133+
*/
134+
#[NoAdminRequired]
135+
#[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/sections/reorder', requirements: [
136+
'apiVersion' => '(v4)',
137+
])]
138+
public function reorderSections(array $orderedIds): DataResponse {
139+
$this->sectionService->reorderSections($this->userId, $orderedIds);
140+
$sections = $this->sectionService->getSections($this->userId);
141+
return new DataResponse(array_values(array_map([$this, 'formatSection'], $sections)));
142+
}
143+
144+
/**
145+
* @return TalkConversationSection
146+
*/
147+
protected function formatSection(ConversationSection $section): array {
148+
return [
149+
'id' => $section->getId(),
150+
'name' => $section->getName(),
151+
'sortOrder' => $section->getSortOrder(),
152+
'collapsed' => $section->isCollapsed(),
153+
];
154+
}
155+
}

lib/Controller/RoomController.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,6 +1888,28 @@ public function unarchiveConversation(): DataResponse {
18881888
return new DataResponse($this->formatRoom($this->room, $this->participant));
18891889
}
18901890

1891+
/**
1892+
* Assign a conversation to a section
1893+
*
1894+
* Required capability: `conversation-sections`
1895+
*
1896+
* @param int|null $sectionId ID of the section, or null to unassign
1897+
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>
1898+
*
1899+
* 200: Conversation section updated
1900+
*/
1901+
#[NoAdminRequired]
1902+
#[FederationSupported]
1903+
#[RequireLoggedInParticipant]
1904+
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/section', requirements: [
1905+
'apiVersion' => '(v4)',
1906+
'token' => '[a-z0-9]{4,30}',
1907+
])]
1908+
public function assignToSection(?int $sectionId = null): DataResponse {
1909+
$this->participantService->assignConversationToSection($this->participant, $sectionId);
1910+
return new DataResponse($this->formatRoom($this->room, $this->participant));
1911+
}
1912+
18911913
/**
18921914
* Mark a conversation as important (still sending notifications while on DND)
18931915
*

0 commit comments

Comments
 (0)