Browse Source

Change read marker to work on the comment id isntead of datetime

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/1214/head
Joas Schilling 7 years ago
parent
commit
54692ebac3
No known key found for this signature in database GPG Key ID: 7076EA9751AACDDA
  1. 9
      appinfo/routes.php
  2. 18
      docs/chat.md
  3. 1
      docs/conversation.md
  4. 17
      lib/Chat/ChatManager.php
  5. 39
      lib/Chat/CommentsManager.php
  6. 13
      lib/Controller/ChatController.php
  7. 20
      lib/Controller/RoomController.php
  8. 8
      lib/Manager.php
  9. 116
      lib/Migration/Version7000Date20190724121136.php
  10. 95
      lib/Migration/Version7000Date20190724121137.php
  11. 59
      lib/Participant.php
  12. 4
      lib/Room.php
  13. 14
      tests/php/Chat/ChatManagerTest.php

9
appinfo/routes.php

@ -120,6 +120,15 @@ return [
'token' => '^[a-z0-9]{4,30}$',
],
],
[
'name' => 'Chat#setReadMarker',
'url' => '/api/{apiVersion}/chat/{token}/read',
'verb' => 'POST',
'requirements' => [
'apiVersion' => 'v1',
'token' => '^[a-z0-9]{4,30}$',
],
],
[
'name' => 'Chat#mentions',
'url' => '/api/{apiVersion}/chat/{token}/mentions',

18
docs/chat.md

@ -63,6 +63,24 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
- Data:
The full message array of the new message, as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)
## Mark chat as read
* Method: `POST`
* Endpoint: `/chat/{token}/read`
* Data:
field | type | Description
------|------|------------
`lastReadMessage` | int | The last read message ID
* Response:
- Header:
+ `200 OK`
+ `404 Not Found` When the room could not be found for the participant,
or the participant is a guest.
## Get mention autocomplete suggestions
* Method: `GET`

1
docs/conversation.md

@ -64,6 +64,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`notificationLevel` | int | The notification level for the user (one of `Participant::NOTIFY_*` (1-3))
`unreadMessages` | int | Number of unread chat messages in the conversation (only available with `chat-v2` capability)
`unreadMention` | bool | Flag if the user was mentioned since their last visit
`lastReadMessage` | int | ID of the last read message in a room
`lastMessage` | message | Last message in a conversation if available, otherwise empty
`objectType` | string | The type of object that the conversation is associated with; "share:password" if the conversation is used to request a password for a share, otherwise empty
`objectId` | string | Share token if "objectType" is "share:password", otherwise empty

17
lib/Chat/ChatManager.php

@ -163,7 +163,7 @@ class ChatManager {
$mentionedUsers = $this->notifier->notifyMentionedUsers($chat, $comment);
if (!empty($mentionedUsers)) {
$chat->markUsersAsMentioned($mentionedUsers, $creationDateTime);
$chat->markUsersAsMentioned($mentionedUsers, (int) $comment->getId());
}
// User was not mentioned, send a normal notification
@ -180,16 +180,17 @@ class ChatManager {
return $comment;
}
public function getUnreadMarker(Room $chat, IUser $user): \DateTime {
public function getLastReadMessageFromLegacy(Room $chat, IUser $user): int {
$marker = $this->commentsManager->getReadMark('chat', $chat->getId(), $user);
if ($marker === null) {
$marker = $this->timeFactory->getDateTime('2000-01-01');
return 0;
}
return $marker;
return $this->commentsManager->getLastCommentBeforeDate('chat', (string) $chat->getId(), $marker, 'comment');
}
public function getUnreadCount(Room $chat, \DateTime $unreadSince): int {
return $this->commentsManager->getNumberOfCommentsForObject('chat', $chat->getId(), $unreadSince, 'comment');
public function getUnreadCount(Room $chat, int $lastReadMessage): int {
return $this->commentsManager->getNumberOfCommentsForObjectSinceComment('chat', (string) $chat->getId(), $lastReadMessage, 'comment');
}
/**
@ -233,10 +234,6 @@ class ChatManager {
$comments = $this->commentsManager->getForObjectSince('chat', (string) $chat->getId(), $offset, 'asc', $limit);
if ($user instanceof IUser) {
$this->commentsManager->setReadMark('chat', (string) $chat->getId(), $this->timeFactory->getDateTime(), $user);
}
while (empty($comments) && $elapsedTime < $timeout) {
sleep(1);
$elapsedTime++;

39
lib/Chat/CommentsManager.php

@ -95,4 +95,43 @@ class CommentsManager extends Manager {
return $lastComments;
}
public function getNumberOfCommentsForObjectSinceComment(string $objectType, string $objectId, int $lastRead, string $verb = ''): int {
$query = $this->dbConn->getQueryBuilder();
$query->selectAlias($query->createFunction('COUNT(' . $query->getColumnName('id') . ')'), 'num_messages')
->from('comments')
->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
->andWhere($query->expr()->gt('id', $query->createNamedParameter($lastRead)));
if ($verb !== '') {
$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
}
$result = $query->execute();
$data = $result->fetch();
$result->closeCursor();
return (int) ($data['num_messages'] ?? 0);
}
public function getLastCommentBeforeDate(string $objectType, string $objectId, \DateTime $beforeDate, string $verb = ''): int {
$query = $this->dbConn->getQueryBuilder();
$query->select('id')
->from('comments')
->where($query->expr()->eq('object_type', $query->createNamedParameter($objectType)))
->andWhere($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
->andWhere($query->expr()->lt('creation_timestamp', $query->createNamedParameter($beforeDate, IQueryBuilder::PARAM_DATE)))
->orderBy('creation_timestamp', 'desc');
if ($verb !== '') {
$query->andWhere($query->expr()->eq('verb', $query->createNamedParameter($verb)));
}
$result = $query->execute();
$data = $result->fetch();
$result->closeCursor();
return (int) ($data['id'] ?? 0);
}
}

13
lib/Controller/ChatController.php

@ -267,11 +267,24 @@ class ChatController extends AEnvironmentAwareController {
$newLastKnown = end($comments);
if ($newLastKnown instanceof IComment) {
$response->addHeader('X-Chat-Last-Given', $newLastKnown->getId());
$this->participant->setLastReadMessage((int) $newLastKnown->getId());
}
return $response;
}
/**
* @NoAdminRequired
* @RequireParticipant
*
* @param int $lastReadMessage
* @return DataResponse
*/
public function setReadMarker(int $lastReadMessage): DataResponse {
$this->participant->setLastReadMessage($lastReadMessage);
return new DataResponse();
}
/**
* @PublicPage
* @RequireParticipant

20
lib/Controller/RoomController.php

@ -177,6 +177,7 @@ class RoomController extends AEnvironmentAwareController {
'hasPassword' => $room->hasPassword(),
'hasCall' => false,
'lastActivity' => 0,
'lastReadMessage' => 0,
'unreadMessages' => 0,
'unreadMention' => false,
'isFavorite' => false,
@ -227,12 +228,21 @@ class RoomController extends AEnvironmentAwareController {
$currentUser = $this->userManager->get($currentParticipant->getUser());
if ($currentUser instanceof IUser) {
$unreadSince = $this->chatManager->getUnreadMarker($room, $currentUser);
if ($currentParticipant instanceof Participant) {
$lastMention = $currentParticipant->getLastMention();
$roomData['unreadMention'] = $lastMention !== null && $unreadSince < $lastMention;
$lastReadMessage = $currentParticipant->getLastReadMessage();
if ($lastReadMessage === -1) {
/*
* Because the migration from the old comment_read_markers was
* not possible in a programmatic way with a reasonable O(1) or O(n)
* but only with O(user×chat), we do the conversion here.
*/
$lastReadMessage = $this->chatManager->getLastReadMessageFromLegacy($room, $currentUser);
$currentParticipant->setLastReadMessage($lastReadMessage);
}
$roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $unreadSince);
$roomData['unreadMessages'] = $this->chatManager->getUnreadCount($room, $lastReadMessage);
$lastMention = $currentParticipant->getLastMentionMessage();
$roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention;
$roomData['lastReadMessage'] = $lastReadMessage;
}
$numActiveGuests = 0;

8
lib/Manager.php

@ -154,11 +154,6 @@ class Manager {
* @return Participant
*/
public function createParticipantObject(Room $room, array $row): Participant {
$lastMention = null;
if (!empty($row['last_mention'])) {
$lastMention = $this->timeFactory->getDateTime($row['last_mention']);
}
$lastJoinedCall = null;
if (!empty($row['last_joined_call'])) {
$lastJoinedCall = $this->timeFactory->getDateTime($row['last_joined_call']);
@ -174,7 +169,8 @@ class Manager {
(int) $row['in_call'],
(int) $row['notification_level'],
(bool) $row['favorite'],
$lastMention,
(int) $row['last_read_message'],
(int) $row['last_mention_message'],
$lastJoinedCall
);
}

116
lib/Migration/Version7000Date20190724121136.php

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Spreed\Migration;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version7000Date20190724121136 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @throws SchemaException
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('talk_participants');
if (!$table->hasColumn('last_read_message')) {
$table->addColumn('last_read_message', Type::BIGINT, [
'default' => 0,
'notnull' => false,
]);
$table->addColumn('last_mention_message', Type::BIGINT, [
'default' => 0,
'notnull' => false,
]);
}
return $schema;
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @since 13.0.0
*/
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
$query = $this->connection->getQueryBuilder();
$query->select('m.user_id', 'm.object_id')
->selectAlias($query->createFunction('MAX(' . $query->getColumnName('c.id') . ')'), 'last_comment')
->from('comments_read_markers', 'm')
->leftJoin('m', 'comments', 'c', $query->expr()->andX(
$query->expr()->eq('c.object_id', 'm.object_id'),
$query->expr()->eq('c.object_type', 'm.object_type'),
$query->expr()->eq('c.creation_timestamp', 'm.marker_datetime')
))
->where($query->expr()->eq('m.object_type', $query->createNamedParameter('chat')))
->groupBy('m.user_id', 'm.object_id');
$update = $this->connection->getQueryBuilder();
$update->update('talk_participants')
->set('last_read_message', $update->createParameter('message_id'))
->where($update->expr()->eq('user_id', $update->createParameter('user_id')))
->andWhere($update->expr()->eq('room_id', $update->createParameter('room_id')));
$result = $query->execute();
while ($row = $result->fetch()) {
$update->setParameter('message_id', (int) $row['last_comment'], IQueryBuilder::PARAM_INT)
->setParameter('user_id', $row['user_id'])
->setParameter('room_id', (int) $row['object_id'], IQueryBuilder::PARAM_INT);
$update->execute();
}
$result->closeCursor();
/**
* The above query only works if the user read in the same exact second
* as the comment was posted (author only), we set the read marker to -1
* for all users and in case of -1 we calculate the marker on the next request.
*/
$default = $this->connection->getQueryBuilder();
$default->update('talk_participants')
->set('last_read_message', $default->createNamedParameter(-1))
->where($default->expr()->isNotNull('user_id'))
->andWhere($default->expr()->eq('last_read_message', $default->createNamedParameter(0)));
$default->execute();
}
}

95
lib/Migration/Version7000Date20190724121137.php

@ -0,0 +1,95 @@
<?php
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Spreed\Migration;
use Doctrine\DBAL\Schema\SchemaException;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version7000Date20190724121137 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @since 13.0.0
*/
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
$query = $this->connection->getQueryBuilder();
$query->select('p.user_id', 'p.room_id')
->selectAlias($query->createFunction('MAX(' . $query->getColumnName('c.id') . ')'), 'last_mention_message')
->from('talk_participants', 'p')
->leftJoin('p', 'comments', 'c', $query->expr()->andX(
$query->expr()->eq('c.object_id', 'p.room_id'),
$query->expr()->eq('c.object_type', $query->createNamedParameter('chat')),
$query->expr()->eq('c.creation_timestamp', 'p.last_mention')
))
->where($query->expr()->isNotNull('p.user_id'))
->groupBy('p.user_id', 'p.room_id');
$update = $this->connection->getQueryBuilder();
$update->update('talk_participants')
->set('last_mention_message', $update->createParameter('message_id'))
->where($update->expr()->eq('user_id', $update->createParameter('user_id')))
->andWhere($update->expr()->eq('room_id', $update->createParameter('room_id')));
$result = $query->execute();
while ($row = $result->fetch()) {
$update->setParameter('message_id', (int) $row['last_mention_message'], IQueryBuilder::PARAM_INT)
->setParameter('user_id', $row['user_id'])
->setParameter('room_id', (int) $row['room_id'], IQueryBuilder::PARAM_INT);
$update->execute();
}
$result->closeCursor();
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @throws SchemaException
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('talk_participants');
if ($table->hasColumn('last_mention')) {
$table->dropColumn('last_mention');
}
return $schema;
}
}

59
lib/Participant.php

@ -63,8 +63,10 @@ class Participant {
protected $notificationLevel;
/** @var bool */
private $isFavorite;
/** @var \DateTime|null */
private $lastMention;
/** @var int */
private $lastReadMessage;
/** @var int */
private $lastMentionMessage;
/** @var \DateTime|null */
private $lastJoinedCall;
@ -77,7 +79,8 @@ class Participant {
int $inCall,
int $notificationLevel,
bool $isFavorite,
\DateTime $lastMention = null,
int $lastReadMessage,
int $lastMentionMessage,
\DateTime $lastJoinedCall = null) {
$this->db = $db;
$this->room = $room;
@ -88,7 +91,8 @@ class Participant {
$this->inCall = $inCall;
$this->notificationLevel = $notificationLevel;
$this->isFavorite = $isFavorite;
$this->lastMention = $lastMention;
$this->lastReadMessage = $lastReadMessage;
$this->lastMentionMessage = $lastMentionMessage;
$this->lastJoinedCall = $lastJoinedCall;
}
@ -124,13 +128,6 @@ class Participant {
return $this->inCall;
}
/**
* @return \DateTime|null
*/
public function getLastMention(): ?\DateTime {
return $this->lastMention;
}
/**
* @return \DateTime|null
*/
@ -185,4 +182,44 @@ class Participant {
$this->notificationLevel = $notificationLevel;
return true;
}
public function getLastReadMessage(): int {
return $this->lastReadMessage;
}
public function setLastReadMessage(int $messageId): bool {
if (!$this->user) {
return false;
}
$query = $this->db->getQueryBuilder();
$query->update('talk_participants')
->set('last_read_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user)))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId())));
$query->execute();
$this->lastReadMessage = $messageId;
return true;
}
public function getLastMentionMessage(): int {
return $this->lastMentionMessage;
}
public function setLastMentionMessage(int $messageId): bool {
if (!$this->user) {
return false;
}
$query = $this->db->getQueryBuilder();
$query->update('talk_participants')
->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('user_id', $query->createNamedParameter($this->user)))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($this->room->getId())));
$query->execute();
$this->lastMentionMessage = $messageId;
return true;
}
}

4
lib/Room.php

@ -1112,10 +1112,10 @@ class Room {
return (int) ($row['num_participants'] ?? 0);
}
public function markUsersAsMentioned(array $userIds, \DateTime $time): void {
public function markUsersAsMentioned(array $userIds, int $messageId): void {
$query = $this->db->getQueryBuilder();
$query->update('talk_participants')
->set('last_mention', $query->createNamedParameter($time, 'datetime'))
->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('room_id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->in('user_id', $query->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
$query->execute();

14
tests/php/Chat/ChatManagerTest.php

@ -218,6 +218,20 @@ class ChatManagerTest extends TestCase {
$this->assertEquals($expected, $comments);
}
public function testGetUnreadCount() {
/** @var Room|MockObject $chat */
$chat = $this->createMock(Room::class);
$chat->expects($this->once())
->method('getId')
->willReturn(23);
$this->commentsManager->expects($this->once())
->method('getNumberOfCommentsForObjectSinceComment')
->with('chat', 23, 42, 'comment');
$this->chatManager->getUnreadCount($chat, 42);
}
public function testDeleteMessages() {
$chat = $this->createMock(Room::class);

Loading…
Cancel
Save