Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* 🌉 **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.
]]></description>

<version>24.0.0-dev.2</version>
<version>24.0.0-dev.24</version>
<licence>agpl</licence>

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

<dependencies>
<nextcloud min-version="34" max-version="34" />
<nextcloud min-version="33" max-version="34" />
</dependencies>

<background-jobs>
Expand Down
3 changes: 3 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@
* `config => call => live-transcription-target-language-id` (local) - User defined string value with the id of the target language to use for live translations

## 24
* `conversation-categories` (local) - Whether the user can create custom categories to organize conversations in the sidebar
* `config => conversations => sort-order` (local) - User selected sort order for conversations (`activity` or `alphabetical`)
* `config => conversations => group-mode` (local) - User selected grouping mode for conversations (`none` or `type-first`)
* `react-permission` - When permission 256 is required to add reactions (previously handled by the chat permission)
* `config => permissions => max-default` - Maximum value for default permissions (510 with react-permission, 254 without)
* `config => permissions => max-custom` - Maximum value for custom permissions (511 with react-permission, 255 without)
Expand Down
6 changes: 6 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class Capabilities implements IPublicCapability {
'federated-shared-items',
'scheduled-messages',
'conversation-presets',
'conversation-categories',
];

public const CONDITIONAL_FEATURES = [
Expand Down Expand Up @@ -163,6 +164,7 @@ class Capabilities implements IPublicCapability {
'sensitive-conversations',
'scheduled-messages',
'conversation-presets',
'conversation-categories',
];

public const LOCAL_CONFIGS = [
Expand Down Expand Up @@ -193,6 +195,8 @@ class Capabilities implements IPublicCapability {
'conversations' => [
'can-create',
'list-style',
'sort-order',
'group-mode',
'description-length',
],
'federation' => [
Expand Down Expand Up @@ -294,6 +298,8 @@ public function getCapabilities(): array {
'can-create' => $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user),
'force-passwords' => $this->talkConfig->isPasswordEnforced(),
'list-style' => $this->talkConfig->getConversationsListStyle($user?->getUID()),
'sort-order' => $this->talkConfig->getConversationsSortOrder($user?->getUID()),
'group-mode' => $this->talkConfig->getConversationsGroupMode($user?->getUID()),
'description-length' => Room::DESCRIPTION_MAXIMUM_LENGTH,
'retention-event' => max(0, $this->appConfig->getAppValueInt('retention_event_rooms', 28)),
'retention-phone' => max(0, $this->appConfig->getAppValueInt('retention_phone_rooms', 7)),
Expand Down
44 changes: 44 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,50 @@ public function getChatStyle(?string $userId): string {
return UserPreference::CHAT_STYLE_SPLIT;
}

/**
* User setting for conversations sort order
*
* @param ?string $userId
* @return UserPreference::CONVERSATIONS_SORT_ORDER_*
*/
public function getConversationsSortOrder(?string $userId): string {
if ($userId !== null) {
$userSetting = $this->config->getUserValue(
$userId,
'spreed',
UserPreference::CONVERSATIONS_SORT_ORDER,
);

if (in_array($userSetting, [UserPreference::CONVERSATIONS_SORT_ORDER_ACTIVITY, UserPreference::CONVERSATIONS_SORT_ORDER_ALPHABETICAL], true)) {
return $userSetting;
}
}

return UserPreference::CONVERSATIONS_SORT_ORDER_ACTIVITY;
}

/**
* User setting for conversations group mode
*
* @param ?string $userId
* @return UserPreference::CONVERSATIONS_GROUP_MODE_*
*/
public function getConversationsGroupMode(?string $userId): string {
if ($userId !== null) {
$userSetting = $this->config->getUserValue(
$userId,
'spreed',
UserPreference::CONVERSATIONS_GROUP_MODE,
);

if (in_array($userSetting, [UserPreference::CONVERSATIONS_GROUP_MODE_NONE, UserPreference::CONVERSATIONS_GROUP_MODE_TYPE_FIRST], true)) {
return $userSetting;
}
}

return UserPreference::CONVERSATIONS_GROUP_MODE_NONE;
}

/**
* User setting falling back to admin defined app config
*/
Expand Down
154 changes: 154 additions & 0 deletions lib/Controller/ConversationCategoryController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Controller;

use OCA\Talk\Model\ConversationCategory;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Service\ConversationCategoryService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;

/**
* @psalm-import-type TalkConversationCategory from ResponseDefinitions
*/
class ConversationCategoryController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
protected ConversationCategoryService $categoryService,
protected ?string $userId,
) {
parent::__construct($appName, $request);
}

/**
* Get all conversation categories for the current user
*
* Required capability: `conversation-categories`
*
* @return DataResponse<Http::STATUS_OK, list<TalkConversationCategory>, array{}>
*
* 200: Categories returned
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/categories', requirements: [
'apiVersion' => '(v4)',
])]
public function getCategories(): DataResponse {
$categories = $this->categoryService->getCategories($this->userId);
return new DataResponse(array_values(array_map([$this, 'formatCategory'], $categories)));
}

/**
* Create a new conversation category
*
* Required capability: `conversation-categories`
*
* @param string $name Name of the category
* @return DataResponse<Http::STATUS_CREATED, TalkConversationCategory, array{}>
*
* 201: Category created
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/categories', requirements: [
'apiVersion' => '(v4)',
])]
public function createCategory(string $name): DataResponse {
$category = $this->categoryService->createCategory($this->userId, $name);
return new DataResponse($this->formatCategory($category), Http::STATUS_CREATED);
}

/**
* Update a conversation category
*
* Required capability: `conversation-categories`
*
* @param int $categoryId ID of the category
* @param string $name New name for the category
* @return DataResponse<Http::STATUS_OK, TalkConversationCategory, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Category updated
* 404: Category not found
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/categories/{categoryId}', requirements: [
'apiVersion' => '(v4)',
'categoryId' => '\d+',
])]
public function updateCategory(int $categoryId, string $name): DataResponse {
try {
$category = $this->categoryService->updateCategory($categoryId, $this->userId, $name);
return new DataResponse($this->formatCategory($category));
} catch (DoesNotExistException) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
}

/**
* Delete a conversation category
*
* Required capability: `conversation-categories`
*
* @param int $categoryId ID of the category
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Category deleted
* 404: Category not found
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/categories/{categoryId}', requirements: [
'apiVersion' => '(v4)',
'categoryId' => '\d+',
])]
public function deleteCategory(int $categoryId): DataResponse {
try {
$this->categoryService->deleteCategory($categoryId, $this->userId);
return new DataResponse(null);
} catch (DoesNotExistException) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
}

/**
* Reorder conversation categories
*
* Required capability: `conversation-categories`
*
* @param list<int> $orderedIds Ordered list of category IDs
* @return DataResponse<Http::STATUS_OK, list<TalkConversationCategory>, array{}>
*
* 200: Categories reordered
*/
#[NoAdminRequired]
#[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/categories/reorder', requirements: [
'apiVersion' => '(v4)',
])]
public function reorderCategories(array $orderedIds): DataResponse {
$this->categoryService->reorderCategories($this->userId, $orderedIds);
$categories = $this->categoryService->getCategories($this->userId);
return new DataResponse(array_values(array_map([$this, 'formatCategory'], $categories)));
}

/**
* @return TalkConversationCategory
*/
protected function formatCategory(ConversationCategory $category): array {
return [
'id' => (string)$category->getId(),
'name' => $category->getName(),
'sortOrder' => $category->getSortOrder(),
];
}
}
22 changes: 22 additions & 0 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,28 @@ public function unarchiveConversation(): DataResponse {
return new DataResponse($this->formatRoom($this->room, $this->participant));
}

/**
* Assign a conversation category
*
* Required capability: `conversation-categories`
*
* @param list<string> $categoryIds IDs of categories to assign (empty array to unassign all)
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>
*
* 200: Conversation categories updated
*/
#[NoAdminRequired]
#[FederationSupported]
#[RequireLoggedInParticipant]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/category', requirements: [
'apiVersion' => '(v4)',
'token' => '[a-z0-9]{4,30}',
])]
public function assignToCategory(array $categoryIds = []): DataResponse {
$this->participantService->assignConversationToCategories($this->participant, $categoryIds);
return new DataResponse($this->formatRoom($this->room, $this->participant));
}

/**
* Mark a conversation as important (still sending notifications while on DND)
*
Expand Down
68 changes: 68 additions & 0 deletions lib/Migration/Version24000Date20260313120000.php
Comment thread
Antreesy marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

class Version24000Date20260313120000 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('talk_conversation_categories')) {
$table = $schema->createTable('talk_conversation_categories');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'unsigned' => true,
'length' => 20,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('name', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('sort_order', Types::INTEGER, [
'notnull' => true,
'default' => 0,
]);
$table->addColumn('collapsed', Types::BOOLEAN, [
'notnull' => false,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['user_id'], 'tcs_user_id');
}

$attendeesTable = $schema->getTable('talk_attendees');
if (!$attendeesTable->hasColumn('category_ids')) {
$attendeesTable->addColumn('category_ids', Types::TEXT, [
'notnull' => false,
'default' => null,
]);
}

return $schema;
}
}
4 changes: 4 additions & 0 deletions lib/Model/Attendee.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
* @method bool isImportant()
* @method void setSensitive(bool $sensitive)
* @method bool isSensitive()
* @method void setCategoryIds(?string $categoryIds)
* @method ?string getCategoryIds()
* @internal
* @method int getPermissions()
* @method void setAccessToken(string $accessToken)
Expand Down Expand Up @@ -148,6 +150,7 @@ class Attendee extends Entity {
protected bool $hasUnreadThreads = false;
protected bool $hasUnreadThreadMentions = false;
protected bool $hasUnreadThreadDirects = false;
protected ?string $categoryIds = null;
protected int $hiddenPinnedId = 0;
protected int $hasScheduledMessages = 0;

Expand Down Expand Up @@ -181,6 +184,7 @@ public function __construct() {
$this->addType('hasUnreadThreads', Types::BOOLEAN);
$this->addType('hasUnreadThreadMentions', Types::BOOLEAN);
$this->addType('hasUnreadThreadDirects', Types::BOOLEAN);
$this->addType('categoryIds', Types::STRING);
$this->addType('hiddenPinnedId', Types::BIGINT);
$this->addType('hasScheduledMessages', Types::INTEGER);

Expand Down
1 change: 1 addition & 0 deletions lib/Model/AttendeeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ public function createAttendeeFromRow(array $row): Attendee {
'archived' => (bool)$row['archived'],
'important' => (bool)$row['important'],
'sensitive' => (bool)$row['sensitive'],
'category_ids' => $row['category_ids'] ?? null,
'has_unread_threads' => (bool)$row['has_unread_threads'],
'has_unread_thread_mentions' => (bool)$row['has_unread_thread_mentions'],
'has_unread_thread_directs' => (bool)$row['has_unread_thread_directs'],
Expand Down
Loading
Loading