feat(mentions): allow teams to be mentioned

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2025-01-21 18:17:36 +01:00
parent 0a6162d315
commit e193de59f6
16 changed files with 410 additions and 21 deletions

View file

@ -71,6 +71,8 @@ class SearchPlugin implements ISearchPlugin {
$emailAttendees = [];
/** @var list<Attendee> $guestAttendees */
$guestAttendees = [];
/** @var array<string, string> $teamIds */
$teamIds = [];
if ($this->room->getType() === Room::TYPE_ONE_TO_ONE) {
// Add potential leavers of one-to-one rooms again.
@ -92,6 +94,8 @@ class SearchPlugin implements ISearchPlugin {
$cloudIds[$attendee->getActorId()] = $attendee->getDisplayName();
} elseif ($attendee->getActorType() === Attendee::ACTOR_GROUPS) {
$groupIds[$attendee->getActorId()] = $attendee->getDisplayName();
} elseif ($attendee->getActorType() === Attendee::ACTOR_CIRCLES) {
$teamIds[$attendee->getActorId()] = $attendee->getDisplayName();
}
}
}
@ -101,6 +105,7 @@ class SearchPlugin implements ISearchPlugin {
$this->searchGuests($search, $guestAttendees, $searchResult);
$this->searchEmails($search, $emailAttendees, $searchResult);
$this->searchFederatedUsers($search, $cloudIds, $searchResult);
$this->searchTeams($search, $teamIds, $searchResult);
return false;
}
@ -352,6 +357,58 @@ class SearchPlugin implements ISearchPlugin {
$searchResult->addResultSet($type, $matches, $exactMatches);
}
/**
* @param string $search
* @param array<string, Attendee> $attendees
* @param ISearchResult $searchResult
*/
/**
* @param array<string|int, string> $teams
*/
protected function searchTeams(string $search, array $teams, ISearchResult $searchResult): void {
$search = strtolower($search);
$type = new SearchResultType('teams');
$matches = $exactMatches = [];
foreach ($teams as $teamId => $displayName) {
if ($displayName === '') {
continue;
}
$teamId = (string)$teamId;
if ($searchResult->hasResult($type, $teamId)) {
continue;
}
if ($search === '') {
$matches[] = $this->createTeamResult($teamId, $displayName);
continue;
}
if (strtolower($teamId) === $search) {
$exactMatches[] = $this->createTeamResult($teamId, $displayName);
continue;
}
if (stripos($teamId, $search) !== false) {
$matches[] = $this->createTeamResult($teamId, $displayName);
continue;
}
if (strtolower($displayName) === $search) {
$exactMatches[] = $this->createTeamResult($teamId, $displayName);
continue;
}
if (stripos($displayName, $search) !== false) {
$matches[] = $this->createTeamResult($teamId, $displayName);
}
}
$searchResult->addResultSet($type, $matches, $exactMatches);
}
protected function createResult(string $type, string $uid, string $name): array {
if ($type === 'user' && $name === '') {
$name = $this->userManager->getDisplayName($uid) ?? $uid;
@ -401,4 +458,14 @@ class SearchPlugin implements ISearchPlugin {
return $data;
}
protected function createTeamResult(string $actorId, string $name): array {
return [
'label' => $name,
'value' => [
'shareType' => 'team',
'shareWith' => 'team/' . $actorId,
],
];
}
}

View file

@ -112,6 +112,7 @@ class Notifier {
public function getUsersToNotify(Room $chat, IComment $comment, array $alreadyNotifiedUsers, ?Participant $participant = null): array {
$usersToNotify = $this->getMentionedUsers($comment);
$usersToNotify = $this->getMentionedGroupMembers($chat, $comment, $usersToNotify);
$usersToNotify = $this->getMentionedTeamMembers($chat, $comment, $usersToNotify);
$usersToNotify = $this->addMentionAllToList($chat, $usersToNotify, $participant);
$usersToNotify = $this->removeAlreadyNotifiedUsers($usersToNotify, $alreadyNotifiedUsers);
@ -532,6 +533,57 @@ class Notifier {
return $list;
}
/**
* @param Room $chat
* @param IComment $comment
* @param array $list
* @psalm-param array<int, array{type: string, id: string, reason: string, sourceId?: string}> $list
* @return array[]
* @psalm-return array<int, array{type: string, id: string, reason: string, sourceId?: string}>
*/
private function getMentionedTeamMembers(Room $chat, IComment $comment, array $list): array {
$mentions = $comment->getMentions();
if (empty($mentions)) {
return [];
}
$alreadyMentionedUserIds = array_filter(
array_map(static fn (array $entry) => $entry['type'] === Attendee::ACTOR_USERS ? $entry['id'] : null, $list),
static fn ($userId) => $userId !== null
);
$alreadyMentionedUserIds = array_flip($alreadyMentionedUserIds);
foreach ($mentions as $mention) {
if ($mention['type'] !== 'team') {
continue;
}
try {
$this->participantService->getParticipantByActor($chat, Attendee::ACTOR_CIRCLES, $mention['id']);
} catch (ParticipantNotFoundException) {
continue;
}
$members = $this->participantService->getCircleMembers($mention['id']);
if (empty($members)) {
continue;
}
foreach ($members as $member) {
$list[] = [
'id' => $member->getUserId(),
'type' => Attendee::ACTOR_USERS,
'reason' => 'team',
'sourceId' => $mention['id'],
];
$alreadyMentionedUserIds[$member->getUserId()] = true;
}
}
return $list;
}
/**
* Creates a notification for the given chat message comment and mentioned
* user ID.

View file

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OCA\Talk\Chat\Parser;
use OCA\Circles\CirclesManager;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Events\MessageParseEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
@ -17,6 +18,7 @@ use OCA\Talk\Model\Message;
use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
use OCP\App\IAppManager;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
@ -25,14 +27,20 @@ use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUserManager;
use OCP\Server;
/**
* Helper class to get a rich message from a plain text message.
* @template-implements IEventListener<Event>
*/
class UserMention implements IEventListener {
/** @var array<string, string> */
protected array $circleNames = [];
/** @var array<string, string> */
protected array $circleLinks = [];
public function __construct(
protected IAppManager $appManager,
protected ICommentsManager $commentsManager,
protected IUserManager $userManager,
protected IGroupManager $groupManager,
@ -117,7 +125,7 @@ class UserMention implements IEventListener {
$mention['type'] === 'email' ||
$mention['type'] === 'group' ||
// $mention['type'] === 'federated_group' ||
// $mention['type'] === 'team' ||
$mention['type'] === 'team' ||
// $mention['type'] === 'federated_team' ||
$mention['type'] === 'federated_user') {
$search = $mention['type'] . '/' . $mention['id'];
@ -135,7 +143,7 @@ class UserMention implements IEventListener {
&& !str_starts_with($search, 'email/')
&& !str_starts_with($search, 'group/')
// && !str_starts_with($search, 'federated_group/')
// && !str_starts_with($search, 'team/')
&& !str_starts_with($search, 'team/')
// && !str_starts_with($search, 'federated_team/')
&& !str_starts_with($search, 'federated_user/')) {
$message = str_replace('@' . $search, '{' . $mentionParameterId . '}', $message);
@ -213,6 +221,8 @@ class UserMention implements IEventListener {
'id' => $mention['id'],
'name' => $displayName,
];
} elseif ($mention['type'] === 'team') {
$messageParameters[$mentionParameterId] = $this->getCircle($mention['id']);
} else {
try {
$displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']);
@ -256,4 +266,47 @@ class UserMention implements IEventListener {
throw new \InvalidArgumentException('Unknown room type');
}
}
protected function getCircle(string $circleId): array {
if (!$this->appManager->isEnabledForUser('circles')) {
return [
'type' => 'highlight',
'id' => $circleId,
'name' => $circleId,
];
}
if (!isset($this->circleNames[$circleId])) {
$this->loadCircleDetails($circleId);
}
if (!isset($this->circleNames[$circleId])) {
return [
'type' => 'highlight',
'id' => $circleId,
'name' => $circleId,
];
}
return [
'type' => 'circle',
'id' => $circleId,
'name' => $this->circleNames[$circleId],
'link' => $this->circleLinks[$circleId],
];
}
protected function loadCircleDetails(string $circleId): void {
try {
$circlesManager = Server::get(CirclesManager::class);
$circlesManager->startSuperSession();
$circle = $circlesManager->getCircle($circleId);
$this->circleNames[$circleId] = $circle->getDisplayName();
$this->circleLinks[$circleId] = $circle->getUrl();
} catch (\Exception) {
} finally {
$circlesManager?->stopSession();
}
}
}

View file

@ -1147,7 +1147,7 @@ class RoomController extends AEnvironmentAwareOCSController {
* Add a participant to a room
*
* @param string $newParticipant New participant
* @param 'users'|'groups'|'circles'|'emails'|'federated_users'|'phones' $source Source of the participant
* @param 'users'|'groups'|'circles'|'emails'|'federated_users'|'phones'|'teams' $source Source of the participant
* @return DataResponse<Http::STATUS_OK, array{type?: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_NOT_IMPLEMENTED, array{error: 'ban'|'cloud-id'|'federation'|'moderator'|'new-participant'|'outgoing'|'reach-remote'|'room-type'|'sip'|'source'|'trusted-servers'}, array{}>
*
* 200: Participant successfully added
@ -1215,7 +1215,7 @@ class RoomController extends AEnvironmentAwareOCSController {
}
$this->participantService->addGroup($this->room, $group, $participants);
} elseif ($source === 'circles') {
} elseif ($source === 'circles' || $source === 'teams') {
if (!$this->appManager->isEnabledForUser('circles')) {
return new DataResponse(['error' => 'new-participant'], Http::STATUS_BAD_REQUEST);
}

View file

@ -8,6 +8,7 @@ declare(strict_types=1);
namespace OCA\Talk\Notification;
use OCA\Circles\CirclesManager;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Chat\ChatManager;
@ -28,6 +29,7 @@ use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Webinary;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\ICommentsManager;
@ -47,6 +49,7 @@ use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use OCP\Notification\UnknownNotificationException;
use OCP\RichObjectStrings\Definitions;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
@ -57,12 +60,17 @@ class Notifier implements INotifier {
protected array $rooms = [];
/** @var Participant[][] */
protected array $participants = [];
/** @var array<string, string> */
protected array $circleNames = [];
/** @var array<string, string> */
protected array $circleLinks = [];
protected ICommentsManager $commentManager;
public function __construct(
protected IFactory $lFactory,
protected IURLGenerator $url,
protected Config $config,
protected IAppManager $appManager,
protected IUserManager $userManager,
protected IGroupManager $groupManager,
protected GuestManager $guestManager,
@ -262,7 +270,7 @@ class Notifier implements INotifier {
}
return $this->parseCall($notification, $room, $l);
}
if ($subject === 'reply' || $subject === 'mention' || $subject === 'mention_direct' || $subject === 'mention_group' || $subject === 'mention_all' || $subject === 'chat' || $subject === 'reaction' || $subject === 'reminder') {
if ($subject === 'reply' || $subject === 'mention' || $subject === 'mention_direct' || $subject === 'mention_group' || $subject === 'mention_team' || $subject === 'mention_all' || $subject === 'chat' || $subject === 'reaction' || $subject === 'reminder') {
if ($participant instanceof Participant &&
$room->getLobbyState() !== Webinary::LOBBY_NONE &&
!($participant->getPermissions() & Attendee::PERMISSIONS_LOBBY_IGNORE)) {
@ -774,6 +782,9 @@ class Notifier implements INotifier {
];
$subject = $l->t('{user} mentioned group {group} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_team') {
$richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']);
$subject = $l->t('{user} mentioned team {team} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_all') {
$subject = $l->t('{user} mentioned everyone in conversation {call}');
} else {
@ -789,6 +800,9 @@ class Notifier implements INotifier {
];
$subject = $l->t('A deleted user mentioned group {group} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_team') {
$richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']);
$subject = $l->t('A deleted user mentioned team {team} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_all') {
$subject = $l->t('A deleted user mentioned everyone in conversation {call}');
} else {
@ -806,6 +820,9 @@ class Notifier implements INotifier {
];
$subject = $l->t('{guest} (guest) mentioned group {group} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_team') {
$richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']);
$subject = $l->t('{guest} (guest) mentioned team {team} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_all') {
$subject = $l->t('{guest} (guest) mentioned everyone in conversation {call}');
} else {
@ -821,6 +838,9 @@ class Notifier implements INotifier {
];
$subject = $l->t('A guest mentioned group {group} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_team') {
$richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']);
$subject = $l->t('A guest mentioned team {team} in conversation {call}');
} elseif ($notification->getSubject() === 'mention_all') {
$subject = $l->t('A guest mentioned everyone in conversation {call}');
} else {
@ -1247,4 +1267,47 @@ class Notifier implements INotifier {
return $notification;
}
protected function getCircle(string $circleId): array {
if (!$this->appManager->isEnabledForUser('circles')) {
return [
'type' => 'highlight',
'id' => $circleId,
'name' => $circleId,
];
}
if (!isset($this->circleNames[$circleId])) {
$this->loadCircleDetails($circleId);
}
if (!isset($this->circleNames[$circleId])) {
return [
'type' => 'highlight',
'id' => $circleId,
'name' => $circleId,
];
}
return [
'type' => 'circle',
'id' => $circleId,
'name' => $this->circleNames[$circleId],
'link' => $this->circleLinks[$circleId],
];
}
protected function loadCircleDetails(string $circleId): void {
try {
$circlesManager = Server::get(CirclesManager::class);
$circlesManager->startSuperSession();
$circle = $circlesManager->getCircle($circleId);
$this->circleNames[$circleId] = $circle->getDisplayName();
$this->circleLinks[$circleId] = $circle->getUrl();
} catch (\Exception) {
} finally {
$circlesManager?->stopSession();
}
}
}

View file

@ -719,16 +719,43 @@ class ParticipantService {
$circlesManager->startSession($federatedUser);
try {
$circle = $circlesManager->getCircle($circleId);
$circlesManager->stopSession();
return $circle;
return $circlesManager->getCircle($circleId);
} catch (\Exception $e) {
} finally {
$circlesManager->stopSession();
}
$circlesManager->stopSession();
throw new ParticipantNotFoundException('Circle not found or not a member');
}
/**
* @param string $circleId
* @param string $userId
* @return Member[]
* @throws ParticipantNotFoundException
*/
public function getCircleMembers(string $circleId): array {
try {
$circlesManager = Server::get(CirclesManager::class);
} catch (\Exception) {
throw new ParticipantNotFoundException('Circle not found');
}
$circlesManager->startSuperSession();
try {
$circle = $circlesManager->getCircle($circleId);
} catch (\Exception) {
throw new ParticipantNotFoundException('Circle not found');
} finally {
$circlesManager->stopSession();
}
$members = $circle->getInheritedMembers();
return array_filter($members, static function (Member $member) {
return $member->getUserType() === Member::TYPE_USER;
});
}
/**
* @param Room $room
* @param Circle $circle

View file

@ -13791,7 +13791,8 @@
"circles",
"emails",
"federated_users",
"phones"
"phones",
"teams"
],
"description": "Source of the participant"
}

View file

@ -13949,7 +13949,8 @@
"circles",
"emails",
"federated_users",
"phones"
"phones",
"teams"
],
"description": "Source of the participant"
}

View file

@ -328,6 +328,7 @@ export const MENTION = {
// Parsed to another types
FEDERATED_USER: 'federated_user',
GROUP: 'group',
TEAM: 'team',
},
}

View file

@ -7227,7 +7227,7 @@ export interface operations {
* @default users
* @enum {string}
*/
source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones";
source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones" | "teams";
};
};
};

View file

@ -6811,7 +6811,7 @@ export interface operations {
* @default users
* @enum {string}
*/
source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones";
source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones" | "teams";
};
};
};

View file

@ -1970,7 +1970,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
/**
* @Then /^user "([^"]*)" adds (user|group|email|circle|federated_user|phone) "([^"]*)" to room "([^"]*)" with (\d+) \((v4)\)$/
* @Then /^user "([^"]*)" adds (user|group|email|circle|federated_user|phone|team) "([^"]*)" to room "([^"]*)" with (\d+) \((v4)\)$/
*
* @param string $user
* @param string $newType
@ -1990,6 +1990,10 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
if ($newType === 'circle' || $newType === 'team') {
$newId = self::$createdTeams[$this->currentServer][$newId];
}
$this->sendRequest(
'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants',
new TableNode([
@ -2309,6 +2313,12 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$message = str_replace('{$LOCAL_URL}', $this->localServerUrl, $message);
$message = str_replace('{$LOCAL_REMOTE_URL}', $this->localRemoteServerUrl, $message);
$message = str_replace('{$REMOTE_URL}', $this->remoteServerUrl, $message);
if (str_contains($message, '@"TEAM_ID(')) {
$result = preg_match('/TEAM_ID\(([^)]+)\)/', $message, $matches);
if ($result) {
$message = str_replace($matches[0], 'team/' . self::$createdTeams[$this->currentServer][$matches[1]], $message);
}
}
if ($message === '413 Payload Too Large') {
$message .= "\n" . str_repeat('1', 32000);
@ -3499,6 +3509,10 @@ class FeatureContext implements Context, SnippetAcceptingContext {
if (str_ends_with($row['mentionId'], '@{$REMOTE_URL}')) {
$row['mentionId'] = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $row['mentionId']);
}
if ($row['source'] === 'teams') {
[, $teamId] = explode('/', $row['mentionId'], 2);
$row['id'] = $row['mentionId'] = 'team/' . self::getTeamIdForLabel($this->currentServer, $teamId);
}
if (array_key_exists('avatar', $row)) {
Assert::assertMatchesRegularExpression('/' . self::$identifierToToken[$row['avatar']] . '\/avatar/', $mentions[$key]['avatar']);
unset($row['avatar']);
@ -3948,10 +3962,23 @@ class FeatureContext implements Context, SnippetAcceptingContext {
private function assertNotifications($notifications, TableNode $formData) {
Assert::assertCount(count($formData->getHash()), $notifications, 'Notifications count does not match:' . "\n" . json_encode($notifications, JSON_PRETTY_PRINT));
Assert::assertEquals($formData->getHash(), array_map(function ($notification, $expectedNotification) {
$expectedNotifications = array_map(function (array $expectedNotification): array {
if (str_contains($expectedNotification['object_id'], '/')) {
[$roomToken, $message] = explode('/', $expectedNotification['object_id']);
$result = preg_match('/TEAM_ID\(([^)]+)\)/', $message, $matches);
if ($result) {
$message = str_replace($matches[0], 'team/' . self::$createdTeams[$this->currentServer][$matches[1]], $message);
}
$expectedNotification['object_id'] = $roomToken . '/' . $message;
}
return $expectedNotification;
}, $formData->getHash());
Assert::assertEquals($expectedNotifications, array_map(function ($notification, $expectedNotification) {
$data = [];
if (isset($expectedNotification['object_id'])) {
if (strpos($notification['object_id'], '/') !== false) {
if (str_contains($notification['object_id'], '/')) {
[$roomToken, $message] = explode('/', $notification['object_id']);
$messageText = self::$messageIdToText[$message] ?? 'UNKNOWN_MESSAGE';
@ -3976,6 +4003,10 @@ class FeatureContext implements Context, SnippetAcceptingContext {
if ($result && isset(self::$identifierToToken[$matches[1]])) {
$data['message'] = str_replace(self::$identifierToToken[$matches[1]], $matches[0], $data['message']);
}
$result = preg_match('/TEAM_ID\(([^)]+)\)/', $expectedNotification['message'], $matches);
if ($result) {
$data['message'] = str_replace($matches[0], self::$createdTeams[$this->currentServer][$matches[1]], $data['message']);
}
}
if (isset($expectedNotification['object_type'])) {
$data['object_type'] = (string)$notification['object_type'];
@ -3985,7 +4016,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
return $data;
}, $notifications, $formData->getHash()), json_encode($notifications, JSON_PRETTY_PRINT));
}, $notifications, $expectedNotifications), json_encode($notifications, JSON_PRETTY_PRINT));
}
/**
@ -5232,7 +5263,4 @@ class FeatureContext implements Context, SnippetAcceptingContext {
Assert::assertEquals($statusCode, $response->getStatusCode(), $message);
}
}
}

View file

@ -364,6 +364,82 @@ Feature: chat/notifications
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
Scenario: Team-mention when recipient is online in the group room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant2" to team "1234"
When user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds team "1234" to room "room" with 200 (v4)
Given user "participant2" joins room "room" with 200 (v4)
When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
| spreed | chat | room/Hi @"TEAM_ID(1234)" bye | participant1-displayname mentioned team 1234 in conversation room |
Scenario: Team-mention when team is not a member of the room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant2" to team "1234"
When user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds user "participant2" to room "room" with 200 (v4)
Given user "participant2" joins room "room" with 200 (v4)
When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
Scenario: Team-mention when recipient is offline in the group room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant2" to team "1234"
When user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds team "1234" to room "room" with 200 (v4)
# Join and leave to clear the invite notification
Given user "participant2" joins room "room" with 200 (v4)
Given user "participant2" leaves room "room" with 200 (v4)
When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
| spreed | chat | room/Hi @"TEAM_ID(1234)" bye | participant1-displayname mentioned team 1234 in conversation room |
Scenario: Silent team-mention when recipient is offline in the group room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant2" to team "1234"
When user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds team "1234" to room "room" with 200 (v4)
# Join and leave to clear the invite notification
Given user "participant2" joins room "room" with 200 (v4)
Given user "participant2" leaves room "room" with 200 (v4)
When user "participant1" silent sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
Scenario: Team-mention when recipient with disabled notifications in the group room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant2" to team "1234"
When user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds team "1234" to room "room" with 200 (v4)
# Join and leave to clear the invite notification
Given user "participant2" joins room "room" with 200 (v4)
Given user "participant2" leaves room "room" with 200 (v4)
And user "participant2" sets notifications to disabled for room "room" (v4)
When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201
Then user "participant2" has the following notifications
| app | object_type | object_id | subject |
Scenario: Replying with all mention types only gives a reply notification
When user "participant1" creates room "room" (v4)
| roomType | 2 |

View file

@ -74,11 +74,14 @@ Feature: chat/mentions
| participant2 | participant2-displayname | users | participant2 |
Scenario: get matched mentions in a group room
Given team "1234" exists
Given add user "participant1" to team "1234"
Given add user "participant3" to team "1234"
When user "participant1" creates room "group room" (v4)
| roomType | 2 |
| roomName | room |
And user "participant1" adds user "participant2" to room "group room" with 200 (v4)
And user "participant1" adds user "participant3" to room "group room" with 200 (v4)
And user "participant1" adds team "1234" to room "group room" with 200 (v4)
Then user "participant1" gets the following candidate mentions in room "group room" for "part" with 200
| id | label | source | mentionId |
| participant2 | participant2-displayname | users | participant2 |
@ -91,6 +94,15 @@ Feature: chat/mentions
| id | label | source | mentionId |
| participant1 | participant1-displayname | users | participant1 |
| participant2 | participant2-displayname | users | participant2 |
And user "participant1" gets the following candidate mentions in room "group room" for "1234" with 200
| id | label | source | mentionId |
| 1234 | 1234 | teams | team/1234 |
And user "participant2" gets the following candidate mentions in room "group room" for "1234" with 200
| id | label | source | mentionId |
| 1234 | 1234 | teams | team/1234 |
And user "participant3" gets the following candidate mentions in room "group room" for "1234" with 200
| id | label | source | mentionId |
| 1234 | 1234 | teams | team/1234 |
Scenario: get unmatched mentions in a group room
When user "participant1" creates room "group room" (v4)

View file

@ -17,6 +17,7 @@ use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
use OCP\App\IAppManager;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Federation\ICloudId;
@ -28,6 +29,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class UserMentionTest extends TestCase {
protected IAppManager&MockObject $appManager;
protected ICommentsManager&MockObject $commentsManager;
protected IUserManager&MockObject $userManager;
protected IGroupManager&MockObject $groupManager;
@ -42,6 +44,7 @@ class UserMentionTest extends TestCase {
public function setUp(): void {
parent::setUp();
$this->appManager = $this->createMock(IAppManager::class);
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
@ -52,6 +55,7 @@ class UserMentionTest extends TestCase {
$this->l = $this->createMock(IL10N::class);
$this->parser = new UserMention(
$this->appManager,
$this->commentsManager,
$this->userManager,
$this->groupManager,

View file

@ -26,6 +26,7 @@ use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\ParticipantService;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\Federation\ICloudIdManager;
@ -49,6 +50,7 @@ class NotifierTest extends TestCase {
protected IFactory&MockObject $lFactory;
protected IURLGenerator&MockObject $url;
protected Config&MockObject $config;
protected IAppManager&MockObject $appManager;
protected IUserManager&MockObject $userManager;
protected IGroupManager&MockObject $groupManager;
protected GuestManager&MockObject $guestManager;
@ -75,6 +77,7 @@ class NotifierTest extends TestCase {
$this->lFactory = $this->createMock(IFactory::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->config = $this->createMock(Config::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->guestManager = $this->createMock(GuestManager::class);
@ -98,6 +101,7 @@ class NotifierTest extends TestCase {
$this->lFactory,
$this->url,
$this->config,
$this->appManager,
$this->userManager,
$this->groupManager,
$this->guestManager,