Merge pull request #15053 from nextcloud/feat/noid/sensitive-conversations

feat(conversations): Add "sensitive conversations" that hide the last message
This commit is contained in:
Joas Schilling 2025-05-15 17:23:43 +02:00 committed by GitHub
commit a8ef63638b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 794 additions and 10 deletions

View file

@ -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>22.0.0-dev.6</version>
<version>22.0.0-dev.7</version>
<licence>agpl</licence>
<author>Anna Larch</author>

View file

@ -89,6 +89,10 @@ return [
['name' => 'Room#markConversationAsImportant', 'url' => '/api/{apiVersion}/room/{token}/important', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::markConversationAsUnimportant() */
['name' => 'Room#markConversationAsUnimportant', 'url' => '/api/{apiVersion}/room/{token}/important', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::markConversationAsSensitive() */
['name' => 'Room#markConversationAsSensitive', 'url' => '/api/{apiVersion}/room/{token}/sensitive', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::markConversationAsInsensitive() */
['name' => 'Room#markConversationAsInsensitive', 'url' => '/api/{apiVersion}/room/{token}/sensitive', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::verifyDialInPin() */
['name' => 'Room#verifyDialInPin', 'url' => '/api/{apiVersion}/room/{token}/pin/{pin}', 'verb' => 'GET', 'requirements' => array_merge($requirementsWithToken, [
'pin' => '\d{7,32}',

View file

@ -191,3 +191,4 @@
* `dashboard-event-rooms` (local) - Whether Talk APIs offer functionality for Dashboard requests
* `mutual-calendar-events` (local) - Whether Talk APIs offer mutual calendar events for 1:1 rooms
* `upcoming-reminders` (local) - Whether the API to list upcoming reminders exists
* `sensitive-conversations` (local) - Whether sensitive conversations are supported

View file

@ -121,6 +121,7 @@ class Capabilities implements IPublicCapability {
'dashboard-event-rooms',
'mutual-calendar-events',
'upcoming-reminders',
'sensitive-conversations',
];
public const CONDITIONAL_FEATURES = [

View file

@ -1757,6 +1757,40 @@ class RoomController extends AEnvironmentAwareOCSController {
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
* Mark a conversation as sensitive (no last message is visible / no push preview is shown)
*
* Required capability: `sensitive-conversations`
*
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>
*
* 200: Conversation was marked as sensitive
*/
#[NoAdminRequired]
#[FederationSupported]
#[RequireLoggedInParticipant]
public function markConversationAsSensitive(): DataResponse {
$this->participantService->markConversationAsSensitive($this->participant);
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
* Mark a conversation as insensitive (last message is visible / push preview is shown)
*
* Required capability: `sensitive-conversations`
*
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>
*
* 200: Conversation was marked as insensitive
*/
#[NoAdminRequired]
#[FederationSupported]
#[RequireLoggedInParticipant]
public function markConversationAsInsensitive(): DataResponse {
$this->participantService->markConversationAsInsensitive($this->participant);
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
* Join a room
*

View file

@ -265,9 +265,12 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge
protected function prepareRoom(Room $room, string $userId): WidgetItem {
$participant = $this->participantService->getParticipant($room, $userId);
$attendee = $participant->getAttendee();
$subtitle = '';
if ($room->getLastMessageId() && $room->isFederatedConversation()) {
if ($attendee->isSensitive()) {
// Don't leak sensitive last messages on dashboard
} elseif ($room->getLastMessageId() && $room->isFederatedConversation()) {
try {
$cachedMessage = $this->pcmService->findByRemote(
$room->getRemoteServer(),
@ -285,7 +288,6 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge
$subtitle = $this->getSubtitleFromMessage($message);
}
$attendee = $participant->getAttendee();
if ($room->getCallFlag() !== Participant::FLAG_DISCONNECTED) {
$subtitle = $this->l10n->t('Call in progress');
} elseif (($room->isFederatedConversation() && $attendee->getLastMentionMessage())

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 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;
class Version21001Date20250509153510 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();
$table = $schema->getTable('talk_attendees');
if (!$table->hasColumn('sensitive')) {
$table->addColumn('sensitive', Types::BOOLEAN, [
'default' => 0,
'notnull' => false,
]);
}
return $schema;
}
}

View file

@ -44,6 +44,8 @@ use OCP\DB\Types;
* @method bool isArchived()
* @method void setImportant(bool $important)
* @method bool isImportant()
* @method void setSensitive(bool $important)
* @method bool isSensitive()
* @internal
* @method int getPermissions()
* @method void setAccessToken(string $accessToken)
@ -116,6 +118,7 @@ class Attendee extends Entity {
protected int $notificationCalls = 0;
protected bool $archived = false;
protected bool $important = false;
protected bool $sensitive = false;
protected int $lastJoinedCall = 0;
protected int $lastReadMessage = 0;
protected int $lastMentionMessage = 0;
@ -141,6 +144,7 @@ class Attendee extends Entity {
$this->addType('favorite', Types::BOOLEAN);
$this->addType('archived', Types::BOOLEAN);
$this->addType('important', Types::BOOLEAN);
$this->addType('sensitive', Types::BOOLEAN);
$this->addType('notificationLevel', Types::INTEGER);
$this->addType('notificationCalls', Types::INTEGER);
$this->addType('lastJoinedCall', Types::INTEGER);

View file

@ -310,6 +310,7 @@ class AttendeeMapper extends QBMapper {
'last_attendee_activity' => (int)$row['last_attendee_activity'],
'archived' => (bool)$row['archived'],
'important' => (bool)$row['important'],
'sensitive' => (bool)$row['sensitive'],
]);
}
}

View file

@ -78,6 +78,7 @@ class SelectHelper {
->addSelect($alias . 'last_attendee_activity')
->addSelect($alias . 'archived')
->addSelect($alias . 'important')
->addSelect($alias . 'sensitive')
->selectAlias($alias . 'id', 'a_id');
}

View file

@ -626,7 +626,7 @@ class Notifier implements INotifier {
}
$parsedMessage = str_replace($placeholders, $replacements, $message->getMessage());
if (!$this->notificationManager->isPreparingPushNotification()) {
if (!$this->notificationManager->isPreparingPushNotification() && !$participant->getAttendee()->isSensitive()) {
$notification->setParsedMessage($parsedMessage);
$notification->setRichMessage($message->getMessage(), $message->getMessageParameters());
@ -639,7 +639,44 @@ class Notifier implements INotifier {
'call' => $richSubjectCall,
];
if ($this->notificationManager->isPreparingPushNotification()) {
if ($participant->getAttendee()->isSensitive()) {
// Prevent message preview and conversation name in sensitive conversations
if ($this->notificationManager->isPreparingPushNotification()) {
$translatedPrivateConversation = $l->t('Private conversation');
if ($notification->getSubject() === 'reaction') {
// TRANSLATORS Someone reacted in a private conversation
$subject = $translatedPrivateConversation . "\n" . $l->t('Someone reacted');
} elseif ($notification->getSubject() === 'chat') {
// TRANSLATORS You received a new message in a private conversation
$subject = $translatedPrivateConversation . "\n" . $l->t('New message');
} elseif ($notification->getSubject() === 'reminder') {
// TRANSLATORS Reminder for a message in a private conversation
$subject = $translatedPrivateConversation . "\n" . $l->t('Reminder');
} elseif (str_starts_with($notification->getSubject(), 'mention_')) {
// TRANSLATORS Someone mentioned you in a private conversation
$subject = $translatedPrivateConversation . "\n" . $l->t('Someone mentioned you');
} else {
// TRANSLATORS There's a notification in a private conversation
$subject = $translatedPrivateConversation . "\n" . $l->t('Notification');
}
} else {
if ($notification->getSubject() === 'reaction') {
$subject = $l->t('Someone reacted in a private conversation');
} elseif ($notification->getSubject() === 'chat') {
$subject = $l->t('You received a message in a private conversation');
} elseif ($notification->getSubject() === 'reminder') {
$subject = $l->t('Reminder in a private conversation');
} elseif (str_starts_with($notification->getSubject(), 'mention_')) {
$subject = $l->t('Someone mentioned you in a private conversation');
} else {
$subject = $l->t('Notification in a private conversation');
}
}
$richSubjectParameters = [];
} elseif ($this->notificationManager->isPreparingPushNotification()) {
$shortenMessage = $this->shortenJsonEncodedMultibyteSave($parsedMessage, 100);
if ($shortenMessage !== $parsedMessage) {
$shortenMessage .= '…';

View file

@ -360,6 +360,8 @@ namespace OCA\Talk;
* isArchived: bool,
* // Required capability: `important-conversations`
* isImportant: bool,
* // Required capability: `sensitive-conversations`
* isSensitive: bool,
* }
*
* @psalm-type TalkDashboardEventAttachment = array{

View file

@ -344,6 +344,26 @@ class ParticipantService {
$this->attendeeMapper->update($attendee);
}
/**
* @param Participant $participant
*/
public function markConversationAsSensitive(Participant $participant): void {
$attendee = $participant->getAttendee();
$attendee->setSensitive(true);
$attendee->setLastAttendeeActivity($this->timeFactory->getTime());
$this->attendeeMapper->update($attendee);
}
/**
* @param Participant $participant
*/
public function markConversationAsInsensitive(Participant $participant): void {
$attendee = $participant->getAttendee();
$attendee->setSensitive(false);
$attendee->setLastAttendeeActivity($this->timeFactory->getTime());
$this->attendeeMapper->update($attendee);
}
/**
* @param RoomService $roomService
* @param Room $room

View file

@ -146,6 +146,7 @@ class RoomFormatter {
'mentionPermissions' => Room::MENTION_PERMISSIONS_EVERYONE,
'isArchived' => false,
'isImportant' => false,
'isSensitive' => false,
];
if ($room->isFederatedConversation()) {
@ -231,6 +232,7 @@ class RoomFormatter {
'mentionPermissions' => $room->getMentionPermissions(),
'isArchived' => $attendee->isArchived(),
'isImportant' => $attendee->isImportant(),
'isSensitive' => $attendee->isSensitive(),
]);
if ($room->isFederatedConversation()) {
@ -391,6 +393,7 @@ class RoomFormatter {
}
}
$skipLastMessage = $skipLastMessage || $attendee->isSensitive();
$lastMessage = $skipLastMessage ? null : $room->getLastMessage();
if (!$room->isFederatedConversation() && $lastMessage instanceof IComment) {
$lastMessageData = $this->formatLastMessage(

View file

@ -616,7 +616,8 @@
"unreadMentionDirect",
"unreadMessages",
"isArchived",
"isImportant"
"isImportant",
"isSensitive"
],
"properties": {
"actorId": {
@ -901,6 +902,10 @@
"isImportant": {
"type": "boolean",
"description": "Required capability: `important-conversations`"
},
"isSensitive": {
"type": "boolean",
"description": "Required capability: `sensitive-conversations`"
}
}
},

View file

@ -670,7 +670,8 @@
"unreadMentionDirect",
"unreadMessages",
"isArchived",
"isImportant"
"isImportant",
"isSensitive"
],
"properties": {
"actorId": {
@ -955,6 +956,10 @@
"isImportant": {
"type": "boolean",
"description": "Required capability: `important-conversations`"
},
"isSensitive": {
"type": "boolean",
"description": "Required capability: `sensitive-conversations`"
}
}
},

View file

@ -1465,7 +1465,8 @@
"unreadMentionDirect",
"unreadMessages",
"isArchived",
"isImportant"
"isImportant",
"isSensitive"
],
"properties": {
"actorId": {
@ -1750,6 +1751,10 @@
"isImportant": {
"type": "boolean",
"description": "Required capability: `important-conversations`"
},
"isSensitive": {
"type": "boolean",
"description": "Required capability: `sensitive-conversations`"
}
}
},
@ -17517,6 +17522,170 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/sensitive": {
"post": {
"operationId": "room-mark-conversation-as-sensitive",
"summary": "Mark a conversation as sensitive (no last message is visible / no push preview is shown)",
"description": "Required capability: `sensitive-conversations`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Conversation was marked as sensitive",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Room"
}
}
}
}
}
}
}
}
}
},
"delete": {
"operationId": "room-mark-conversation-as-insensitive",
"summary": "Mark a conversation as insensitive (last message is visible / push preview is shown)",
"description": "Required capability: `sensitive-conversations`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Conversation was marked as insensitive",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Room"
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/notify": {
"post": {
"operationId": "room-set-notification-level",

View file

@ -1370,7 +1370,8 @@
"unreadMentionDirect",
"unreadMessages",
"isArchived",
"isImportant"
"isImportant",
"isSensitive"
],
"properties": {
"actorId": {
@ -1655,6 +1656,10 @@
"isImportant": {
"type": "boolean",
"description": "Required capability: `important-conversations`"
},
"isSensitive": {
"type": "boolean",
"description": "Required capability: `sensitive-conversations`"
}
}
},
@ -17422,6 +17427,170 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/sensitive": {
"post": {
"operationId": "room-mark-conversation-as-sensitive",
"summary": "Mark a conversation as sensitive (no last message is visible / no push preview is shown)",
"description": "Required capability: `sensitive-conversations`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Conversation was marked as sensitive",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Room"
}
}
}
}
}
}
}
}
}
},
"delete": {
"operationId": "room-mark-conversation-as-insensitive",
"summary": "Mark a conversation as insensitive (last message is visible / push preview is shown)",
"description": "Required capability: `sensitive-conversations`",
"tags": [
"room"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Conversation was marked as insensitive",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/Room"
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/notify": {
"post": {
"operationId": "room-set-notification-level",

View file

@ -66,6 +66,7 @@ describe('Conversation.vue', () => {
displayName: 'conversation one',
isFavorite: false,
isArchived: false,
isSensitive: false,
lastMessage: {
actorId: 'user-id-alice',
actorDisplayName: 'Alice Wonderland',
@ -199,6 +200,22 @@ describe('Conversation.vue', () => {
const wrapper = testConversationLabel(item, /^Alice:\s+filename.jpg$/)
expect(wrapper.findComponent({ name: 'FileIcon' }).exists()).toBeTruthy()
})
test('hides subname for sensitive conversations', () => {
item.isSensitive = true
const wrapper = shallowMount(Conversation, {
localVue,
store,
propsData: {
isSearchResult: false,
item,
},
})
const el = wrapper.find('.conversation__subname')
expect(el.exists()).toBe(false)
})
})
describe('unread messages counter', () => {

View file

@ -38,7 +38,7 @@
</template>
<span class="text"> {{ item.displayName }} </span>
</template>
<template v-if="!compact" #subname>
<template v-if="!compact && !item.isSensitive" #subname>
<span class="conversation__subname" :title="conversationInformation.title">
<span v-if="conversationInformation.actor"
class="conversation__subname-actor">
@ -322,6 +322,7 @@ export default {
canDeleteConversation: false,
canLeaveConversation: false,
hasCall: false,
isSensitive: false,
}
},
},

View file

@ -494,6 +494,8 @@ export type components = {
isArchived: boolean;
/** @description Required capability: `important-conversations` */
isImportant: boolean;
/** @description Required capability: `sensitive-conversations` */
isSensitive: boolean;
};
RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"];
};

View file

@ -521,6 +521,8 @@ export type components = {
isArchived: boolean;
/** @description Required capability: `important-conversations` */
isImportant: boolean;
/** @description Required capability: `sensitive-conversations` */
isSensitive: boolean;
};
RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"];
};

View file

@ -1295,6 +1295,30 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/sensitive": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Mark a conversation as sensitive (no last message is visible / no push preview is shown)
* @description Required capability: `sensitive-conversations`
*/
post: operations["room-mark-conversation-as-sensitive"];
/**
* Mark a conversation as insensitive (last message is visible / push preview is shown)
* @description Required capability: `sensitive-conversations`
*/
delete: operations["room-mark-conversation-as-insensitive"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/notify": {
parameters: {
query?: never;
@ -2685,6 +2709,8 @@ export type components = {
isArchived: boolean;
/** @description Required capability: `important-conversations` */
isImportant: boolean;
/** @description Required capability: `sensitive-conversations` */
isSensitive: boolean;
};
RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"];
RoomWithInvalidInvitations: components["schemas"]["Room"] & {
@ -8882,6 +8908,68 @@ export interface operations {
};
};
};
"room-mark-conversation-as-sensitive": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Conversation was marked as sensitive */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
};
};
"room-mark-conversation-as-insensitive": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Conversation was marked as insensitive */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
};
};
"room-set-notification-level": {
parameters: {
query?: never;

View file

@ -1295,6 +1295,30 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/sensitive": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Mark a conversation as sensitive (no last message is visible / no push preview is shown)
* @description Required capability: `sensitive-conversations`
*/
post: operations["room-mark-conversation-as-sensitive"];
/**
* Mark a conversation as insensitive (last message is visible / push preview is shown)
* @description Required capability: `sensitive-conversations`
*/
delete: operations["room-mark-conversation-as-insensitive"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/notify": {
parameters: {
query?: never;
@ -2147,6 +2171,8 @@ export type components = {
isArchived: boolean;
/** @description Required capability: `important-conversations` */
isImportant: boolean;
/** @description Required capability: `sensitive-conversations` */
isSensitive: boolean;
};
RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"];
RoomWithInvalidInvitations: components["schemas"]["Room"] & {
@ -8344,6 +8370,68 @@ export interface operations {
};
};
};
"room-mark-conversation-as-sensitive": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Conversation was marked as sensitive */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
};
};
"room-mark-conversation-as-insensitive": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Conversation was marked as insensitive */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
};
};
"room-set-notification-level": {
parameters: {
query?: never;

View file

@ -490,6 +490,9 @@ class FeatureContext implements Context, SnippetAcceptingContext {
if (isset($expectedRoom['isArchived'])) {
$data['isArchived'] = (int)$room['isArchived'];
}
if (isset($expectedRoom['isSensitive'])) {
$data['isSensitive'] = (int)$room['isSensitive'];
}
if (isset($expectedRoom['participantType'])) {
$data['participantType'] = (string)$room['participantType'];
}
@ -4372,6 +4375,21 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$this->assertStatusCode($this->response, $statusCode);
}
#[When('/^user "([^"]*)" marks room "([^"]*)" as (sensitive|insensitive) with (\d+) \((v4)\)$/')]
public function userMarksConversationSensitive(string $user, string $identifier, string $action, int $statusCode, string $apiVersion): void {
$httpMethod = 'POST';
if ($action === 'insensitive') {
$httpMethod = 'DELETE';
}
$this->setCurrentUser($user);
$this->sendRequest(
$httpMethod, '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/sensitive',
);
$this->assertStatusCode($this->response, $statusCode);
}
public function sendRequestFullUrl(string $verb, string $fullUrl, TableNode|array|string|null $body = null, array $headers = [], array $options = []): void {
$client = new Client();
$options = array_merge($options, ['cookies' => $this->getUserCookieJar($this->currentUser)]);

View file

@ -0,0 +1,52 @@
Feature: conversation-5/sensitive
Background:
Given user "participant1" exists
Given user "participant2" exists
Scenario: Mark as (in-)sensitive
Given user "participant1" creates room "group room" (v4)
| roomType | 3 |
| roomName | room |
When user "participant1" creates room "one-to-one room" (v4)
| roomType | 1 |
| invite | participant2 |
And user "participant1" sends message "Message 1" to room "group room" with 201
And user "participant1" sends message "Message 2" to room "one-to-one room" with 201
And user "participant1" is participant of the following unordered rooms (v4)
| id | name | lastMessage | isSensitive |
| group room | room | Message 1 | 0 |
| one-to-one room | participant2 | Message 2 | 0 |
And user "participant1" marks room "one-to-one room" as sensitive with 200 (v4)
And user "participant1" marks room "group room" as sensitive with 200 (v4)
And user "participant1" is participant of the following unordered rooms (v4)
| id | name | lastMessage | isSensitive |
| group room | room | UNSET | 1 |
| one-to-one room | participant2 | UNSET | 1 |
And user "participant1" marks room "one-to-one room" as insensitive with 200 (v4)
And user "participant1" marks room "group room" as insensitive with 200 (v4)
And user "participant1" is participant of the following unordered rooms (v4)
| id | name | lastMessage | isSensitive |
| group room | room | Message 1 | 0 |
| one-to-one room | participant2 | Message 2 | 0 |
Scenario: Message preview hidden in sensitive rooms for notifications
When user "participant1" creates room "one-to-one room" (v4)
| roomType | 1 |
| invite | participant2 |
And user "participant2" creates room "one-to-one room" with 200 (v4)
| roomType | 1 |
| invite | participant1 |
And user "participant2" marks room "one-to-one room" as sensitive with 200 (v4)
And user "participant1" sends message "Secret message" to room "one-to-one room" with 201
And user "participant1" sends message "Secret mention for @participant2" to room "one-to-one room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject | message |
| spreed | chat | one-to-one room | Someone mentioned you in a private conversation | |
| spreed | chat | one-to-one room | You received a message in a private conversation | |
When user "participant2" marks room "one-to-one room" as insensitive with 200 (v4)
And user "participant1" sends message "Nonsecret message" to room "one-to-one room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject | message |
| spreed | chat | one-to-one room/Nonsecret message | participant1-displayname sent you a private message | Nonsecret message |
| spreed | chat | one-to-one room/Secret mention for @participant2 | participant1-displayname mentioned you in a private conversation | Secret mention for @participant2-displayname |
| spreed | chat | one-to-one room/Secret message | participant1-displayname sent you a private message | Secret message |

View file

@ -85,6 +85,20 @@ Feature: integration/dashboard-server
And user "participant1" unarchives room "one-to-one room" with 200 (v4)
And user "participant1" unarchives room "group room" with 200 (v4)
And user "participant1" unarchives room "call room" with 200 (v4)
And user "participant1" marks room "one-to-one room" as sensitive with 200 (v4)
Then user "participant1" sees the following entries for dashboard widgets "spreed" (v1)
| title | subtitle | link | iconUrl | sinceId | overlayIconUrl |
| call room | Call in progress | call room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| lobby room with bypass | You were mentioned | lobby room with bypass | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| group room | You were mentioned | group room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| participant2-displayname | | one-to-one room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
Then user "participant1" sees the following entries for dashboard widgets "spreed" (v2)
| title | subtitle | link | iconUrl | sinceId | overlayIconUrl |
| call room | Call in progress | call room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| lobby room with bypass | You were mentioned | lobby room with bypass | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| group room | You were mentioned | group room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
| participant2-displayname | | one-to-one room | {$BASE_URL}ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar{version} | | |
And user "participant1" marks room "one-to-one room" as insensitive with 200 (v4)
And user "participant2" set the message expiration to 3 of room "one-to-one room" with 200 (v4)
And user "participant2" sends message "Message 3" to room "one-to-one room" with 201
And user "participant2" set the message expiration to 3 of room "group room" with 200 (v4)

View file

@ -430,6 +430,7 @@ class ChatManagerTest extends TestCase {
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
@ -494,6 +495,7 @@ class ChatManagerTest extends TestCase {
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())
@ -580,6 +582,7 @@ class ChatManagerTest extends TestCase {
'last_attendee_activity' => 0,
'archived' => 0,
'important' => 0,
'sensitive' => 0,
]);
$chat = $this->createMock(Room::class);
$chat->expects($this->any())