feat(pinned): Expose pin actor and pinned until for messages

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2025-10-31 21:56:59 +01:00
parent bf0f5f05e4
commit 3e32fbb0dd
No known key found for this signature in database
GPG key ID: F72FA5B49FFA96B0
7 changed files with 140 additions and 43 deletions

View file

@ -44,7 +44,10 @@ class MessageParser {
}
public function createMessage(Room $room, ?Participant $participant, IComment $comment, IL10N $l): Message {
return new Message($room, $participant, $comment, $l);
$message = new Message($room, $participant, $comment, $l);
$metaData = $this->addPinnedActorDisplayNameInfo($message, $comment->getMetaData() ?? []);
$message->setMetaData($metaData);
return $message;
}
public function createMessageFromProxyCache(Room $room, ?Participant $participant, ProxyCacheMessage $proxy, IL10N $l): Message {
@ -63,6 +66,15 @@ class MessageParser {
$proxy->getParsedMessageParameters()
);
try {
$metaData = json_decode($proxy->getMetaData(), true, flags: JSON_THROW_ON_ERROR);
if (is_array($metaData)) {
$metaData = $this->addPinnedActorDisplayNameInfo($message, $metaData);
$message->setMetaData($metaData);
}
} catch (\JsonException) {
}
return $message;
}
@ -121,6 +133,21 @@ class MessageParser {
}
}
protected function addPinnedActorDisplayNameInfo(Message $message, array $metaData): array {
if (isset($metaData[Message::METADATA_PINNED_BY_TYPE], $metaData[Message::METADATA_PINNED_BY_ID])) {
[$actorType, $actorId, $displayName] = $this->getActorInformation(
$message,
$metaData[Message::METADATA_PINNED_BY_TYPE],
$metaData[Message::METADATA_PINNED_BY_ID],
);
$metaData[Message::METADATA_PINNED_BY_TYPE] = $actorType;
$metaData[Message::METADATA_PINNED_BY_ID] = $actorId;
$metaData[Message::METADATA_PINNED_BY_NAME] = $displayName;
}
return $metaData;
}
protected function getActorInformation(Message $message, string $actorType, string $actorId, string $displayName = ''): array {
if ($actorType === Attendee::ACTOR_USERS) {
$tempDisplayName = $this->userManager->getDisplayName($actorId);

View file

@ -463,7 +463,21 @@ class CloudFederationProviderTalk implements ICloudFederationProvider, ISignedCl
// Note: `messageParameters` (array during parsing) vs `messageParameter` (string during sending)
$notification['messageData']['messageParameters'] = json_decode($notification['messageData']['messageParameter'], true, flags: JSON_THROW_ON_ERROR);
unset($notification['messageData']['messageParameter']);
if (isset($notification['messageData']['metaData'])) {
// Decode metaData to array and after converting back to string
$notification['messageData']['metaData'] = json_decode($notification['messageData']['metaData'], true, flags: JSON_THROW_ON_ERROR);
} else {
$notification['messageData']['metaData'] = [];
}
$converted = $this->userConverter->convertMessage($room, $notification['messageData']);
if (!empty($converted['metaData'])) {
$converted['metaData'] = json_encode($converted['metaData'], JSON_THROW_ON_ERROR);
} else {
unset($converted['metaData']);
}
$converted['messageParameter'] = json_encode($converted['messageParameters'], JSON_THROW_ON_ERROR);
unset($converted['messageParameters']);

View file

@ -139,6 +139,9 @@ class UserConverter {
$message['token'] = $room->getToken();
$message = $this->convertAttendee($room, $message, 'actorType', 'actorId', 'actorDisplayName');
$message = $this->convertAttendee($room, $message, 'lastEditActorType', 'lastEditActorId', 'lastEditActorDisplayName');
if (!empty($message['metaData']['pinnedActorType'])) {
$message['metaData'] = $this->convertAttendee($room, $message['metaData'], 'pinnedActorType', 'pinnedActorId', 'pinnedActorDisplayName');
}
$message = $this->convertMessageParameters($room, $message);
if (isset($message['parent'])) {

View file

@ -27,44 +27,29 @@ class Message {
public const METADATA_THREAD_ID = 'thread_id';
public const METADATA_PINNED_BY_TYPE = 'pinned_by_type';
public const METADATA_PINNED_BY_ID = 'pinned_by_id';
public const METADATA_PINNED_BY_NAME = 'pinned_by_name';
public const METADATA_PINNED_MESSAGE_ID = 'pinned_id';
public const METADATA_PINNED_UNTIL = 'pinned_until';
public const EXPOSED_METADATA_KEYS = [
self::METADATA_PINNED_BY_TYPE => 'pinnedActorType',
self::METADATA_PINNED_BY_ID => 'pinnedActorId',
self::METADATA_PINNED_BY_NAME => 'pinnedActorName',
self::METADATA_PINNED_UNTIL => 'pinnedUntil',
];
/** @var bool */
protected $visible = true;
/** @var string */
protected $type = '';
/** @var string */
protected $message = '';
/** @var string */
protected $rawMessage = '';
/** @var array */
protected $parameters = [];
/** @var string */
protected $actorType = '';
/** @var string */
protected $actorId = '';
/** @var string */
protected $actorDisplayName = '';
/** @var string */
protected $lastEditActorType = '';
/** @var string */
protected $lastEditActorId = '';
/** @var string */
protected $lastEditActorDisplayName = '';
/** @var int */
protected $lastEditTimestamp = 0;
protected bool $visible = true;
protected string $type = '';
protected string $message = '';
protected string $rawMessage = '';
protected array $parameters = [];
protected string $actorType = '';
protected string $actorId = '';
protected string $actorDisplayName = '';
protected string $lastEditActorType = '';
protected string $lastEditActorId = '';
protected string $lastEditActorDisplayName = '';
protected int $lastEditTimestamp = 0;
protected array $metaData = [];
public function __construct(
protected Room $room,
@ -154,6 +139,10 @@ class Message {
$this->lastEditTimestamp = $timestamp;
}
public function setMetaData(array $metaData): void {
$this->metaData = $metaData;
}
public function getActorType(): string {
return $this->actorType;
}
@ -242,6 +231,17 @@ class Message {
$data[self::METADATA_SILENT] = true;
}
$data['metaData'] = [];
foreach (self::EXPOSED_METADATA_KEYS as $exposedKey => $exposedAs) {
if (isset($this->metaData[$exposedKey])) {
$data['metaData'][$exposedAs] = $this->metaData[$exposedKey];
}
}
if (empty($data['metaData'])) {
unset($data['metaData']);
}
return $data;
}
}

View file

@ -123,6 +123,12 @@ namespace OCA\Talk;
* isThread?: bool,
* threadTitle?: string,
* threadReplies?: int,
* metaData?: array{
* pinnedActorType?: string,
* pinnedActorId?: string,
* pinnedActorDisplayName?: string,
* pinnedUntil?: int,
* },
* }
*
* @psalm-type TalkChatProxyMessage = TalkBaseMessage

View file

@ -2051,13 +2051,24 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
#[Then('/^user "([^"]*)" (unpins|pins|hides pinned) message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
public function userPinsMessage(string $user, string $action, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1'): void {
public function userPinActionMessage(string $user, string $action, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1'): void {
$this->userPinActionWithTimeMessage($user, $action, $message, 0, $identifier, $statusCode, $apiVersion);
}
#[Then('/^user "([^"]*)" (unpins|pins|hides pinned) message "([^"]*)" for (\d+) seconds in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')]
public function userPinActionWithTimeMessage(string $user, string $action, string $message, int $duration, string $identifier, int $statusCode, string $apiVersion = 'v1'): void {
$this->setCurrentUser($user);
$body = [];
if ($action === 'pins' && $duration !== 0) {
$body['pinUntil'] = time() + $duration;
}
$routeSuffix = $action === 'hides pinned' ? '/self' : '';
$this->sendRequest(
$action === 'pins' ? 'POST' : 'DELETE',
'/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/' . self::$textToMessageId[$message] . '/pin' . $routeSuffix
'/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/' . self::$textToMessageId[$message] . '/pin' . $routeSuffix,
$body
);
$this->assertStatusCode($this->response, $statusCode);
@ -2701,6 +2712,13 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$includeMessageType = in_array('messageType', $formData->getRow(0), true);
$includeThreadTitle = in_array('threadTitle', $formData->getRow(0), true);
$includeThreadReplies = in_array('threadReplies', $formData->getRow(0), true);
$includeMetaDataKeys = array_map(
static fn (string $field): string => substr($field, strlen('metaData.')),
array_filter(
$formData->getRow(0),
static fn (string $field): bool => str_starts_with($field, 'metaData.')
)
);
$expected = $formData->getHash();
$count = count($expected);
@ -2775,7 +2793,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
Assert::assertEquals($expected, array_map(function ($message, $expected) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf, $includeLastEdit, $includeMessageType, $includeThreadTitle, $includeThreadReplies) {
Assert::assertEquals($expected, array_map(function ($message, $expected) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf, $includeLastEdit, $includeMessageType, $includeThreadTitle, $includeThreadReplies, $includeMetaDataKeys) {
$data = [
'room' => self::$tokenToIdentifier[$message['token']],
'actorType' => $message['actorType'],
@ -2826,6 +2844,17 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$data['threadReplies'] = $message['threadReplies'] ?? null;
}
if (!empty($includeMetaDataKeys)) {
$metaData = $message['metaData'] ?? [];
foreach ($includeMetaDataKeys as $key) {
$data['metaData.' . $key] = $metaData[$key] ?? 'UNSET';
$expectedValue = $expected['metaData.' . $key];
if ($expectedValue === 'NUMERIC' && is_numeric($data['metaData.' . $key])) {
$data['metaData.' . $key] = $expectedValue;
}
}
}
return $data;
}, $messages, $expected));
}

View file

@ -10,6 +10,8 @@ Feature: chat-1/pinned-messages
And user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" sends message "Message 1" to room "room" with 201
When user "participant1" sends message "Message 2" to room "room" with 201
# Pinned messages are sorted by moment of pinning
When user "participant2" pins message "Message 2" in room "room" with 403
When user "participant1" pins message "Message 2" in room "room" with 200
Then user "participant2" is participant of the following rooms (v4)
@ -38,9 +40,11 @@ Feature: chat-1/pinned-messages
| room | users | participant1 | user_added | You added {user} | "IGNORE" |
| room | users | participant1 | conversation_created | You created the conversation | "IGNORE" |
Then user "participant1" sees the following shared pinned in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters |
| room | users | participant1 | participant1-displayname | Message 1 | [] |
| room | users | participant1 | participant1-displayname | Message 2 | [] |
| room | actorType | actorId | actorDisplayName | message | messageParameters | metaData.pinnedActorDisplayName | metaData.pinnedUntil |
| room | users | participant1 | participant1-displayname | Message 1 | [] | participant1-displayname | UNSET |
| room | users | participant1 | participant1-displayname | Message 2 | [] | participant1-displayname | UNSET |
# Unpinning resets lastPinnedId
When user "participant1" unpins message "Message 1" in room "room" with 200
Then user "participant1" sees the following system messages in room "room" with 200
| room | actorType | actorId | systemMessage | message | messageParameters |
@ -55,6 +59,8 @@ Feature: chat-1/pinned-messages
Then user "participant2" is participant of the following rooms (v4)
| id | type | lastPinnedId | hidePinnedId |
| room | 3 | Message 2 | EMPTY |
# Hide as user
When user "participant2" hides pinned message "Message 2" in room "room" with 200
Then user "participant2" is participant of the following rooms (v4)
| id | type | lastPinnedId | hidePinnedId |
@ -63,7 +69,19 @@ Feature: chat-1/pinned-messages
Then user "participant2" is participant of the following rooms (v4)
| id | type | lastPinnedId | hidePinnedId |
| room | 3 | EMPTY | Message 2 |
When user "participant1" pins message "Message 2" in room "room" with 200
# Pin temporarily
When user "participant1" pins message "Message 2" for 3 seconds in room "room" with 200
Then user "participant1" sees the following shared pinned in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters | metaData.pinnedActorDisplayName | metaData.pinnedUntil |
| room | users | participant1 | participant1-displayname | Message 2 | [] | participant1-displayname | NUMERIC |
Then user "participant2" is participant of the following rooms (v4)
| id | type | lastPinnedId | hidePinnedId |
| room | 3 | Message 2 | EMPTY |
When wait for 4 seconds
And run "OCA\Talk\BackgroundJob\UnpinMessage" background jobs
Then user "participant1" sees the following shared pinned in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters | metaData.pinnedActorDisplayName | metaData.pinnedUntil |
Then user "participant2" is participant of the following rooms (v4)
| id | type | lastPinnedId | hidePinnedId |
| room | 3 | EMPTY | EMPTY |