Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`messageParameters` | array | Message parameters for `message` (see [Rich Object String](https://github.com/nextcloud/server/issues/1706))
`parent` | array | **Optional:** See `Parent data` below
`reactions` | array | **Optional:** An array map with relation between reaction emoji and total count of reactions with this emoji
`reactions`.`self` | array | **Optional:** When the user reacted the reactions array will have an entry `self` with the list of emojis the user reacted with

#### Parent data

Expand Down
28 changes: 28 additions & 0 deletions lib/Chat/CommentsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,32 @@ public function getCommentsById(array $ids): array {

return $comments;
}

/**
* @param string $actorType
* @param string $actorId
* @param string[] $messageIds
* @return array
* @psalm-return array<int, string[]>
*/
public function retrieveReactionsByActor(string $actorType, string $actorId, array $messageIds): array {
$commentIds = array_map('intval', $messageIds);

$query = $this->dbConn->getQueryBuilder();
$query->select('*')
->from('reactions')
->where($query->expr()->eq('actor_type', $query->createNamedParameter($actorType)))
->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId)))
->andWhere($query->expr()->in('parent_id', $query->createNamedParameter($commentIds, IQueryBuilder::PARAM_INT_ARRAY)));

$reactions = [];
$result = $query->executeQuery();
while ($row = $result->fetch()) {
$reactions[(int) $row['parent_id']] ??= [];
$reactions[(int) $row['parent_id']][] = $row['reaction'];
}
$result->closeCursor();

return $reactions;
}
}
14 changes: 14 additions & 0 deletions lib/Chat/ReactionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ public function retrieveReactionMessages(Room $chat, Participant $participant, i
return $reactions;
}

/**
* @param Participant $participant
* @param array $messageIds
* @return array[]
* @psalm-return array<int, string[]>
*/
public function getReactionsByActorForMessages(Participant $participant, array $messageIds): array {
return $this->commentsManager->retrieveReactionsByActor(
$participant->getAttendee()->getActorType(),
$participant->getAttendee()->getActorId(),
$messageIds
);
}

/**
* @param Room $chat
* @param string $messageId
Expand Down
69 changes: 50 additions & 19 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Chat\ReactionManager;
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
use OCA\Talk\Model\Attachment;
Expand Down Expand Up @@ -62,44 +63,26 @@

class ChatController extends AEnvironmentAwareController {
private ?string $userId;

private IUserManager $userManager;

private IAppManager $appManager;

private ChatManager $chatManager;

private ReactionManager $reactionManager;
private ParticipantService $participantService;

private SessionService $sessionService;

protected AttachmentService $attachmentService;

private GuestManager $guestManager;

/** @var string[] */
protected array $guestNames;

private MessageParser $messageParser;

private IManager $autoCompleteManager;

private IUserStatusManager $statusManager;

protected MatterbridgeManager $matterbridgeManager;

private SearchPlugin $searchPlugin;

private ISearchResult $searchResult;

protected ITimeFactory $timeFactory;

protected IEventDispatcher $eventDispatcher;

protected IValidator $richObjectValidator;

protected ITrustedDomainHelper $trustedDomainHelper;

private IL10N $l;

public function __construct(string $appName,
Expand All @@ -108,6 +91,7 @@ public function __construct(string $appName,
IUserManager $userManager,
IAppManager $appManager,
ChatManager $chatManager,
ReactionManager $reactionManager,
ParticipantService $participantService,
SessionService $sessionService,
AttachmentService $attachmentService,
Expand All @@ -129,6 +113,7 @@ public function __construct(string $appName,
$this->userManager = $userManager;
$this->appManager = $appManager;
$this->chatManager = $chatManager;
$this->reactionManager = $reactionManager;
$this->participantService = $participantService;
$this->sessionService = $sessionService;
$this->attachmentService = $attachmentService;
Expand Down Expand Up @@ -516,6 +501,8 @@ public function receiveMessages(int $lookIntoFuture,
];
}

$messages = $this->loadSelfReactions($messages, $commentIdToIndex);

$response = new DataResponse($messages, Http::STATUS_OK);

$newLastKnown = end($comments);
Expand All @@ -540,6 +527,50 @@ public function receiveMessages(int $lookIntoFuture,
return $response;
}

protected function loadSelfReactions(array $messages, array $commentIdToIndex): array {
// Get message ids with reactions
$messageIdsWithReactions = array_map(
static fn (array $message) => $message['id'],
array_filter($messages, static fn (array $message) => !empty($message['reactions']))
);

// Get parents with reactions
$parentsWithReactions = array_map(
static fn (array $message) => ['parent' => $message['parent']['id'], 'message' => $message['id']],
array_filter($messages, static fn (array $message) => !empty($message['parent']['reactions']))
);

// Create a map, so we can translate the parent's $messageId to the correct child entries
$parentMap = $parentIdsWithReactions = [];
foreach ($parentsWithReactions as $entry) {
$parentMap[(int) $entry['parent']] ??= [];
$parentMap[(int) $entry['parent']][] = (int) $entry['message'];
$parentIdsWithReactions[] = (int) $entry['parent'];
}

// Unique list for the query
$idsWithReactions = array_unique(array_merge($messageIdsWithReactions, $parentIdsWithReactions));
$reactionsById = $this->reactionManager->getReactionsByActorForMessages($this->participant, $idsWithReactions);

// Inject the reactions self into the $messages array
foreach ($reactionsById as $messageId => $reactions) {
if (isset($messages[$commentIdToIndex[$messageId]])) {
$messages[$commentIdToIndex[$messageId]]['reactions']['self'] = $reactions;
}

// Add the self part also to potential parent elements
if (isset($parentMap[$messageId])) {
foreach ($parentMap[$messageId] as $mid) {
if (isset($messages[$commentIdToIndex[$mid]])) {
$messages[$commentIdToIndex[$mid]]['parent']['reactions']['self'] = $reactions;
}
}
}
}

return $messages;
}

/**
* @NoAdminRequired
* @RequireParticipant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,12 @@ describe('Message.vue', () => {

store = new Store(testStoreConfig)

const messagePropsWithReactions = Object.assign({}, messageProps)
messagePropsWithReactions.reactions = {
'👍': 1,
self: ['👍'],
}

const wrapper = shallowMount(Message, {
localVue,
store,
Expand Down Expand Up @@ -848,10 +854,16 @@ describe('Message.vue', () => {

store = new Store(testStoreConfig)

const messagePropsWithReactions = Object.assign({}, messageProps)
messagePropsWithReactions.reactions = {
'❤️': 1,
self: ['❤️'],
}

const wrapper = shallowMount(Message, {
localVue,
store,
propsData: messageProps,
propsData: messagePropsWithReactions,
})

// Click reaction button upon having already reacted
Expand Down
26 changes: 19 additions & 7 deletions src/components/MessagesList/MessagesGroup/Message/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ the main body of the message as well as a quote.
<button v-if="simpleReactions[reaction]!== 0"
slot="trigger"
class="reaction-button"
:class="{'reaction-button__has-reacted': userHasReacted(reaction)}"
@click="handleReactionClick(reaction)">
<span class="reaction-button__emoji">{{ reaction }}</span>
<span> {{ simpleReactions[reaction] }}</span>
Expand Down Expand Up @@ -345,6 +346,11 @@ export default {
type: [String, Number],
default: 0,
},

reactions: {
type: [Array, Object],
default: () => { return {} },
},
},

data() {
Expand Down Expand Up @@ -562,7 +568,12 @@ export default {
},

simpleReactions() {
return this.messageObject.reactions
const reactions = Object.assign({}, this.messageObject.reactions)
if (reactions?.self) {
// Remove the self entry for the rendering
delete reactions.self
}
return reactions
},

detailedReactions() {
Expand Down Expand Up @@ -599,6 +610,10 @@ export default {
},

methods: {
userHasReacted(reaction) {
return this.reactions?.self && this.reactions.self.indexOf(reaction) !== -1
},

lastReadMessageVisibilityChanged(isVisible) {
if (isVisible) {
this.seen = true
Expand Down Expand Up @@ -663,13 +678,8 @@ export default {
},

async handleReactionClick(clickedEmoji) {
if (!this.detailedReactionsLoaded) {
await this.getReactions()
}
// Check if current user has already added this reaction to the message
const currentUserHasReacted = this.$store.getters.userHasReacted(this.$store.getters.getActorType(), this.$store.getters.getActorId(), this.token, this.id, clickedEmoji)

if (!currentUserHasReacted) {
if (!this.userHasReacted(clickedEmoji)) {
this.$store.dispatch('addReactionToMessage', {
token: this.token,
messageId: this.id,
Expand Down Expand Up @@ -892,7 +902,9 @@ export default {
margin: 0 4px 0 0;
}

&__has-reacted,
&:hover {
border-color: var(--color-primary-element);
background-color: var(--color-primary-element-lighter);
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,12 @@ const mutations = {
}
const reactionCount = state.messages[token][messageId].reactions[reaction] + 1
Vue.set(state.messages[token][messageId].reactions, reaction, reactionCount)

if (!state.messages[token][messageId].reactions.self) {
Vue.set(state.messages[token][messageId].reactions, 'self', [reaction])
} else {
state.messages[token][messageId].reactions.self.push(reaction)
}
},

// Decreases reaction count for a particular reaction on a message
Expand All @@ -364,6 +370,13 @@ const mutations = {
if (state.messages[token][messageId].reactions[reaction] <= 0) {
Vue.delete(state.messages[token][messageId].reactions, reaction)
}

if (state.messages[token][messageId].reactions.self) {
const i = state.messages[token][messageId].reactions.self.indexOf(reaction)
if (i !== -1) {
Vue.delete(state.messages[token][messageId].reactions, 'self', i)
}
}
},
}

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -2267,7 +2267,7 @@ public function userReactWithOnMessageToRoomWith(string $user, string $action, s
/**
* @Given /^user "([^"]*)" retrieve reactions "([^"]*)" of message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*/
public function userRetrieveReactionsOfMessageInRoomWith(string $user, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', TableNode $formData): void {
public function userRetrieveReactionsOfMessageInRoomWith(string $user, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void {
$token = self::$identifierToToken[$identifier];
$messageId = self::$textToMessageId[$message];
$this->setCurrentUser($user);
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/features/reaction/react.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ Feature: reaction/react
| actorType | actorId | actorDisplayName | reaction |
| users | participant1 | participant1-displayname | 👍 |
| users | participant2 | participant2-displayname | 👍 |
And user "participant1" react with "🚀" on message "Message 1" to room "room" with 201
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters | reactions |
| room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":2} |
| room | users | participant1 | participant1-displayname | Message 1 | [] | {"👍":2,"🚀":1,"self":["👍","🚀"]} |
Then user "participant1" sees the following system messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | systemMessage |
| room | users | participant1 | participant1-displayname | reaction |
| room | users | participant1 | participant1-displayname | reaction |
| room | users | participant2 | participant2-displayname | reaction |
| room | users | participant1 | participant1-displayname | user_added |
| room | users | participant1 | participant1-displayname | conversation_created |
Expand Down
5 changes: 5 additions & 0 deletions tests/php/Controller/ChatControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCA\Talk\Chat\AutoComplete\SearchPlugin;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Chat\ReactionManager;
use OCA\Talk\Controller\ChatController;
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
Expand Down Expand Up @@ -63,6 +64,8 @@ class ChatControllerTest extends TestCase {
private $appManager;
/** @var ChatManager|MockObject */
protected $chatManager;
/** @var ReactionManager|MockObject */
protected $reactionManager;
/** @var ParticipantService|MockObject */
protected $participantService;
/** @var SessionService|MockObject */
Expand Down Expand Up @@ -109,6 +112,7 @@ public function setUp(): void {
$this->userManager = $this->createMock(IUserManager::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->chatManager = $this->createMock(ChatManager::class);
$this->reactionManager = $this->createMock(ReactionManager::class);
$this->participantService = $this->createMock(ParticipantService::class);
$this->sessionService = $this->createMock(SessionService::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
Expand Down Expand Up @@ -145,6 +149,7 @@ private function recreateChatController() {
$this->userManager,
$this->appManager,
$this->chatManager,
$this->reactionManager,
$this->participantService,
$this->sessionService,
$this->attachmentService,
Expand Down