Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 getReactionsForMessages(Participant $participant, array $messageIds): array {
return $this->commentsManager->retrieveReactionsByActor(
$participant->getAttendee()->getActorType(),
$participant->getAttendee()->getActorId(),
$messageIds
);
}

/**
* @param Room $chat
* @param string $messageId
Expand Down
62 changes: 43 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,45 @@ public function receiveMessages(int $lookIntoFuture,
];
}

/**
* Gather information to expose $message['reactions']['self']
*/
$messageIdsWithReactions = array_map(
static fn (array $message) => $message['id'],
array_filter($messages, static fn (array $message) => !empty($message['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']))
);

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

$idsWithReactions = array_unique(array_merge($messageIdsWithReactions, $parentIdsWithReactions));

$reactionsById = $this->reactionManager->getReactionsForMessages($this->participant, $idsWithReactions);
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;
}
}
}
}

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

$newLastKnown = end($comments);
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