Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

↩️ Reply to messages #2000

Merged
merged 9 commits into from
Jul 30, 2019
2 changes: 2 additions & 0 deletions docs/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`actorDisplayName` | string | Display name of the message author
`timestamp` | int | Timestamp in seconds and UTC time zone
`systemMessage` | string | empty for normal chat message or the type of the system message (untranslated)
`messageType` | string | Currently known types are `comment`, `system` and `command`
`message` | string | Message string with placeholders (see [Rich Object String](https://github.com/nextcloud/server/issues/1706))
`messageParameters` | array | Message parameters for `message` (see [Rich Object String](https://github.com/nextcloud/server/issues/1706))

Expand All @@ -53,6 +54,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
------|------|------------
`message` | string | The message the user wants to say
`actorDisplayName` | string | Guest display name (ignored for logged in users)
`replyTo` | int | The message ID this message is a reply to (only allowed for messages from the same conversation and when the message type is not `system` or `command`)

* Response:
- Header:
Expand Down
3 changes: 2 additions & 1 deletion js/models/chatmessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
actorDisplayName: '',
timestamp: 0,
message: '',
messageParameters: []
messageParameters: [],
replyTo: 0
},

url: function() {
Expand Down
36 changes: 31 additions & 5 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,22 @@ public function addChangelogMessage(Room $chat, string $message): IComment {
* @param string $actorType
* @param string $actorId
* @param string $message
* @param IComment|null $replyTo
* @param \DateTime $creationDateTime
* @return IComment
*/
public function sendMessage(Room $chat, Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime): IComment {
public function sendMessage(Room $chat, Participant $participant, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, ?IComment $replyTo): IComment {
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId());
$comment->setMessage($message, self::MAX_CHAT_LENGTH);
$comment->setCreationDateTime($creationDateTime);
// A verb ('comment', 'like'...) must be provided to be able to save a
// comment
$comment->setVerb('comment');

if ($replyTo instanceof IComment) {
$comment->setParentId($replyTo->getId());
}

$this->dispatcher->dispatch(self::class . '::preSendMessage', new GenericEvent($chat, [
'comment' => $comment,
'room' => $chat,
Expand All @@ -161,13 +166,18 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT
// Update last_message
$chat->setLastMessage($comment);

$mentionedUsers = $this->notifier->notifyMentionedUsers($chat, $comment);
if (!empty($mentionedUsers)) {
$chat->markUsersAsMentioned($mentionedUsers, (int) $comment->getId());
$alreadyNotifiedUsers = [];
if ($replyTo instanceof IComment) {
$alreadyNotifiedUsers = $this->notifier->notifyReplyToAuthor($chat, $comment, $replyTo);
}

$alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $alreadyNotifiedUsers);
if (!empty($alreadyNotifiedUsers)) {
$chat->markUsersAsMentioned($alreadyNotifiedUsers, (int) $comment->getId());
}

// User was not mentioned, send a normal notification
$this->notifier->notifyOtherParticipant($chat, $comment, $mentionedUsers);
$this->notifier->notifyOtherParticipant($chat, $comment, $alreadyNotifiedUsers);

$this->dispatcher->dispatch(self::class . '::sendMessage', new GenericEvent($chat, [
'comment' => $comment,
Expand All @@ -180,6 +190,22 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT
return $comment;
}

/**
* @param Room $chat
* @param string $parentId
* @return IComment
* @throws NotFoundException
*/
public function getParentComment(Room $chat, string $parentId): IComment {
$comment = $this->commentsManager->get($parentId);

if ($comment->getObjectType() !== 'chat' || $comment->getObjectId() !== (string) $chat->getId()) {
throw new NotFoundException('Parent not found in the right context');
}

return $comment;
}

public function getLastReadMessageFromLegacy(Room $chat, IUser $user): int {
$marker = $this->commentsManager->getReadMark('chat', $chat->getId(), $user);
if ($marker === null) {
Expand Down
48 changes: 42 additions & 6 deletions lib/Chat/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ public function __construct(INotificationManager $notificationManager,
*
* @param Room $chat
* @param IComment $comment
* @param string[] $alreadyNotifiedUsers
* @return string[] Users that were mentioned
*/
public function notifyMentionedUsers(Room $chat, IComment $comment): array {
public function notifyMentionedUsers(Room $chat, IComment $comment, array $alreadyNotifiedUsers): array {
$mentionedUserIds = $this->getMentionedUserIds($comment);
if (empty($mentionedUserIds)) {
return [];
Expand All @@ -92,13 +93,48 @@ public function notifyMentionedUsers(Room $chat, IComment $comment): array {

$notification = $this->createNotification($chat, $comment, 'mention');
foreach ($mentionedUserIds as $mentionedUserId) {
if (in_array($mentionedUserId, $alreadyNotifiedUsers, true)) {
continue;
}

if ($this->shouldUserBeNotified($mentionedUserId, $comment)) {
$notification->setUser($mentionedUserId);
$this->notificationManager->notify($notification);
$alreadyNotifiedUsers[] = $mentionedUserId;
}
}

return $mentionedUserIds;
return $alreadyNotifiedUsers;
}

/**
* Notifies the author that wrote the comment which was replied to
*
* The comment must be a chat message comment. That is, its "objectId" must
* be the room ID.
*
* The author of the message is notified only if he is still able to participate in the room
*
* @param Room $chat
* @param IComment $comment
* @param IComment $replyTo
* @return string[] Users that were mentioned
*/
public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo): array {
if ($replyTo->getActorType() !== 'users') {
// No reply notification when the replyTo-author was not a user
return [];
}

if (!$this->shouldUserBeNotified($replyTo->getActorId(), $comment)) {
return [];
}

$notification = $this->createNotification($chat, $comment, 'reply');
$notification->setUser($replyTo->getActorId());
$this->notificationManager->notify($notification);

return [$replyTo->getActorId()];
}

/**
Expand All @@ -112,9 +148,9 @@ public function notifyMentionedUsers(Room $chat, IComment $comment): array {
*
* @param Room $chat
* @param IComment $comment
* @param string[] $mentionedUsers
* @param string[] $alreadyNotifiedUsers
*/
public function notifyOtherParticipant(Room $chat, IComment $comment, array $mentionedUsers): void {
public function notifyOtherParticipant(Room $chat, IComment $comment, array $alreadyNotifiedUsers): void {
$participants = $chat->getParticipantsByNotificationLevel(Participant::NOTIFY_ALWAYS);

$notification = $this->createNotification($chat, $comment, 'chat');
Expand All @@ -128,7 +164,7 @@ public function notifyOtherParticipant(Room $chat, IComment $comment, array $men
continue;
}

if (\in_array($participant->getUser(), $mentionedUsers, true)) {
if (\in_array($participant->getUser(), $alreadyNotifiedUsers, true)) {
continue;
}

Expand All @@ -154,7 +190,7 @@ public function notifyOtherParticipant(Room $chat, IComment $comment, array $men
continue;
}

if (\in_array($participant->getUser(), $mentionedUsers, true)) {
if (\in_array($participant->getUser(), $alreadyNotifiedUsers, true)) {
continue;
}

Expand Down
3 changes: 1 addition & 2 deletions lib/Chat/Parser/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@ public function parseMessage(Message $chatMessage): void {
throw new \OutOfBoundsException('Unknown subject');
}

$comment->setMessage($message, ChatManager::MAX_CHAT_LENGTH);
$chatMessage->setMessage($parsedMessage, $parsedParameters);
$chatMessage->setMessage($parsedMessage, $parsedParameters, $message);
}

/**
Expand Down
126 changes: 101 additions & 25 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OCA\Spreed\Chat\ChatManager;
use OCA\Spreed\Chat\MessageParser;
use OCA\Spreed\GuestManager;
use OCA\Spreed\Model\Message;
use OCA\Spreed\Room;
use OCA\Spreed\TalkSession;
use OCP\AppFramework\Http;
Expand All @@ -37,6 +38,7 @@
use OCP\Collaboration\Collaborators\ISearchResult;
use OCP\Comments\IComment;
use OCP\Comments\MessageTooLongException;
use OCP\Comments\NotFoundException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
Expand Down Expand Up @@ -118,11 +120,12 @@ public function __construct(string $appName,
*
* @param string $message the message to send
* @param string $actorDisplayName for guests
* @param int $replyTo Parent id which this message is a reply to
* @return DataResponse the status code is "201 Created" if successful, and
* "404 Not found" if the room or session for a guest user was not
* found".
*/
public function sendMessage(string $message, string $actorDisplayName = ''): DataResponse {
public function sendMessage(string $message, string $actorDisplayName = '', int $replyTo = 0): DataResponse {

if ($this->userId === null) {
$actorType = 'guests';
Expand All @@ -146,11 +149,27 @@ public function sendMessage(string $message, string $actorDisplayName = ''): Dat
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

$parent = $parentMessage = null;
if ($replyTo !== 0) {
try {
$parent = $this->chatManager->getParentComment($this->room, (string) $replyTo);
} catch (NotFoundException $e) {
// Someone is trying to reply cross-rooms or to a non-existing message
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

$parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l);
$this->messageParser->parseMessage($parentMessage);
if ($parentMessage->getMessageType() === 'system' || $parentMessage->getMessageType() === 'command') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that we are not adding any other special type of message anytime soon, but maybe explicitly limit replies only to pure messages for now by checking if the type is not comment instead of checking if it is system or command?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯\_(ツ)_/¯

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the question is if a follow up type would be more likely to be repliable or not. If it is something like stickers or so, it could be repliable. If it is reall bots instead of commands, maybe not. but who knows.

return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
}

$this->room->ensureOneToOneRoomIsFilled();
$creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC'));

try {
$comment = $this->chatManager->sendMessage($this->room, $this->participant, $actorType, $actorId, $message, $creationDateTime);
$comment = $this->chatManager->sendMessage($this->room, $this->participant, $actorType, $actorId, $message, $creationDateTime, $parent);
} catch (MessageTooLongException $e) {
return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE);
} catch (\Exception $e) {
Expand All @@ -164,17 +183,11 @@ public function sendMessage(string $message, string $actorDisplayName = ''): Dat
return new DataResponse([], Http::STATUS_CREATED);
}

return new DataResponse([
'id' => (int) $comment->getId(),
'token' => $this->room->getToken(),
'actorType' => $chatMessage->getActorType(),
'actorId' => $chatMessage->getActorId(),
'actorDisplayName' => $chatMessage->getActorDisplayName(),
'timestamp' => $comment->getCreationDateTime()->getTimestamp(),
'message' => $chatMessage->getMessage(),
'messageParameters' => $chatMessage->getMessageParameters(),
'systemMessage' => $chatMessage->getMessageType() === 'system' ? $comment->getMessage() : '',
], Http::STATUS_CREATED);
$data = $this->messageToData($chatMessage);
if ($parentMessage instanceof Message) {
$data['parent'] = $this->messageToData($parentMessage);
}
return new DataResponse($data, Http::STATUS_CREATED);
}

/**
Expand Down Expand Up @@ -241,28 +254,76 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last
return new DataResponse([], Http::STATUS_NOT_MODIFIED);
}

$messages = [];
$i = 0;
$messages = $commentIdToIndex = $parentIds = [];
foreach ($comments as $comment) {
$id = (int) $comment->getId();
$message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
$this->messageParser->parseMessage($message);

if (!$message->getVisibility()) {
$commentIdToIndex[$id] = null;
continue;
}

$messages[] = [
'id' => (int) $comment->getId(),
'token' => $this->room->getToken(),
'actorType' => $message->getActorType(),
'actorId' => $message->getActorId(),
'actorDisplayName' => $message->getActorDisplayName(),
'timestamp' => $comment->getCreationDateTime()->getTimestamp(),
'message' => $message->getMessage(),
'messageParameters' => $message->getMessageParameters(),
'systemMessage' => $message->getMessageType() === 'system' ? $comment->getMessage() : '',
];
if ($comment->getParentId() !== '0') {
$parentIds[$id] = $comment->getParentId();
}

$messages[] = $this->messageToData($message);
$commentIdToIndex[$id] = $i;
$i++;
}

/**
* Set the parent for reply-messages
*/
$loadedParents = [];
foreach ($parentIds as $commentId => $parentId) {
$commentKey = $commentIdToIndex[$commentId];

// Parent is already parsed in the message list
if (!empty($commentIdToIndex[$parentId])) {
$parentKey = $commentIdToIndex[$parentId];
$messages[$commentKey]['parent'] = $messages[$parentKey];

// We don't show nested parents…
unset($messages[$commentKey]['parent']['parent']);
continue;
}

// Parent was already loaded manually for another comment
if (!empty($loadedParents[$parentId])) {
$messages[$commentKey]['parent'] = $loadedParents[$parentId];
continue;
}

// Parent was not skipped due to visibility, so we need to manually grab it.
if (!isset($commentIdToIndex[$parentId])) {
try {
$comment = $this->chatManager->getParentComment($this->room, $parentId);
$message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
$this->messageParser->parseMessage($message);

if ($message->getVisibility()) {
$loadedParents[$parentId] = $this->messageToData($message);
$messages[$commentKey]['parent'] = $loadedParents[$parentId];
} else {
$loadedParents[$parentId] = [
'id' => $parentId,
'deleted' => true,
];
}
} catch (NotFoundException $e) {
}
}

// Message is not visible to the user
$messages[$commentKey]['parent'] = [
'id' => $parentId,
'deleted' => true,
];
}

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

Expand All @@ -277,6 +338,21 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last
return $response;
}

protected function messageToData(Message $message): array {
return [
'id' => (int) $message->getComment()->getId(),
'token' => $message->getRoom()->getToken(),
'actorType' => $message->getActorType(),
'actorId' => $message->getActorId(),
'actorDisplayName' => $message->getActorDisplayName(),
'timestamp' => $message->getComment()->getCreationDateTime()->getTimestamp(),
'message' => $message->getMessage(),
'messageParameters' => $message->getMessageParameters(),
'systemMessage' => $message->getMessageType() === 'system' ? $message->getMessageRaw() : '',
'messageType' => $message->getMessageType(),
];
}

/**
* @NoAdminRequired
* @RequireParticipant
Expand Down
Loading