зеркало из https://github.com/nextcloud/spreed.git
Start with system chat messages
Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Родитель
a87de565af
Коммит
ee4ba11a53
|
@ -246,15 +246,23 @@ body:not(#body-public) #commentsTabView .comment .authorRow:not(.currentUser):no
|
|||
/* Make the mention the positioning context of its child contacts menu */
|
||||
position: relative;
|
||||
|
||||
font-weight: bold;
|
||||
background-color: nc-lighten($color-main-text, 90%);
|
||||
padding: 2px 5px;
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
#commentsTabView .comment .message .mention-user.current-user {
|
||||
background-color: $color-primary;
|
||||
color: $color-primary-text;
|
||||
#commentsTabView .comment:not(.systemMessage) .message .mention-user {
|
||||
font-weight: bold;
|
||||
|
||||
.current-user {
|
||||
background-color: $color-primary;
|
||||
color: $color-primary-text;
|
||||
}
|
||||
}
|
||||
|
||||
#commentsTabView .comment.systemMessage .message {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
body:not(#body-public) #commentsTabView .comment:not(.newCommentRow) .message .mention-user:not(.current-user) {
|
||||
|
|
|
@ -70,6 +70,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
|
|||
* `favorites` - Rooms can be marked as favorites which will pin them to the top of the room list.
|
||||
* `last-room-activity` - Rooms have the `lastActivity` attribute and should be sorted by that instead of the last ping of the user.
|
||||
* `no-ping` - The ping endpoint has been removed. Ping is updated with a call to fetch the signaling or chat messages instead.
|
||||
* `system-messages` - Chat messages have a "verb" and can be generated by the system
|
||||
|
||||
## Room management
|
||||
|
||||
|
@ -491,6 +492,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
|
|||
`actorId` | string | User id of the message author
|
||||
`actorDisplayName` | string | Display name of the message author
|
||||
`timestamp` | int | Timestamp in seconds and UTC time zone
|
||||
`verb` | string | `comment` for normal chat message or `system` as activity report
|
||||
`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))
|
||||
|
||||
|
|
|
@ -52,10 +52,12 @@
|
|||
'</div>';
|
||||
|
||||
var COMMENT_TEMPLATE =
|
||||
'<li class="comment" data-id="{{id}}">' +
|
||||
'<li class="comment{{#if isNotSystemMessage}}{{else}} systemMessage{{/if}}" data-id="{{id}}">' +
|
||||
' <div class="authorRow{{#if isUserAuthor}} currentUser{{/if}}{{#if isGuest}} guestUser{{/if}}">' +
|
||||
' {{#if isNotSystemMessage}}' +
|
||||
' <div class="avatar" data-user-id="{{actorId}}" data-displayname="{{actorDisplayName}}"> </div>' +
|
||||
' <div class="author">{{actorDisplayName}}</div>' +
|
||||
' {{/if}}' +
|
||||
' <div class="date has-tooltip{{#if relativeDate}} live-relative-timestamp{{/if}}" data-timestamp="{{timestamp}}" title="{{altDate}}">{{date}}</div>' +
|
||||
' </div>' +
|
||||
' <div class="message">{{{formattedMessage}}}</div>' +
|
||||
|
@ -355,6 +357,7 @@
|
|||
date: relativeDate ? OC.Util.relativeModifiedDate(timestamp) : OC.Util.formatDate(timestamp, 'LTS'),
|
||||
relativeDate: relativeDate,
|
||||
altDate: OC.Util.formatDate(timestamp),
|
||||
isNotSystemMessage: commentModel.get('verb') !== 'system',
|
||||
formattedMessage: formattedMessage
|
||||
});
|
||||
return data;
|
||||
|
@ -471,7 +474,8 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
return model1.get('actorId') === model2.get('actorId') &&
|
||||
return model1.get('verb') === model2.get('verb') &&
|
||||
model1.get('actorId') === model2.get('actorId') &&
|
||||
model1.get('actorType') === model2.get('actorType');
|
||||
},
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace OCA\Spreed\AppInfo;
|
|||
use OCA\Spreed\Activity\Hooks;
|
||||
use OCA\Spreed\Capabilities;
|
||||
use OCA\Spreed\Chat\ChatManager;
|
||||
use OCA\Spreed\Chat\SystemMessage\Listener;
|
||||
use OCA\Spreed\Config;
|
||||
use OCA\Spreed\GuestManager;
|
||||
use OCA\Spreed\HookListener;
|
||||
|
@ -70,6 +71,10 @@ class Application extends App {
|
|||
$this->registerCallNotificationHook($dispatcher);
|
||||
$this->registerChatHooks($dispatcher);
|
||||
$this->registerClientLinks($server);
|
||||
|
||||
/** @var Listener $systemMessageListener */
|
||||
$systemMessageListener = $this->getContainer()->query(Listener::class);
|
||||
$systemMessageListener->register();
|
||||
}
|
||||
|
||||
protected function registerNotifier(IServerContainer $server) {
|
||||
|
|
|
@ -51,6 +51,7 @@ class Capabilities implements IPublicCapability {
|
|||
'favorites',
|
||||
'last-room-activity',
|
||||
'no-ping',
|
||||
'system-messages',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -23,9 +23,11 @@
|
|||
|
||||
namespace OCA\Spreed\Chat;
|
||||
|
||||
use OCA\Spreed\Chat\SystemMessage\Parser;
|
||||
use OCA\Spreed\Room;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\Comments\NotFoundException;
|
||||
use OCP\IUser;
|
||||
|
||||
/**
|
||||
|
@ -56,6 +58,29 @@ class ChatManager {
|
|||
$this->notifier = $notifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new message to the given chat.
|
||||
*
|
||||
* @param Room $chat
|
||||
* @param string $actorType
|
||||
* @param string $actorId
|
||||
* @param string $message
|
||||
* @param \DateTime $creationDateTime
|
||||
* @return IComment
|
||||
*/
|
||||
public function addSystemMessage(Room $chat, $actorType, $actorId, $message, \DateTime $creationDateTime) {
|
||||
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId());
|
||||
$comment->setMessage($message);
|
||||
$comment->setCreationDateTime($creationDateTime);
|
||||
$comment->setVerb('system');
|
||||
try {
|
||||
$this->commentsManager->save($comment);
|
||||
} catch (NotFoundException $e) {
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a new message to the given chat.
|
||||
*
|
||||
|
@ -74,7 +99,10 @@ class ChatManager {
|
|||
// comment
|
||||
$comment->setVerb('comment');
|
||||
|
||||
$this->commentsManager->save($comment);
|
||||
try {
|
||||
$this->commentsManager->save($comment);
|
||||
} catch (NotFoundException $e) {
|
||||
}
|
||||
|
||||
// Update last_message
|
||||
$chat->setLastMessage($comment);
|
||||
|
|
|
@ -53,7 +53,7 @@ class RichMessageHelper {
|
|||
* not be resolved.
|
||||
*
|
||||
* @param IComment $comment
|
||||
* @return Array first element, the rich message; second element, the
|
||||
* @return array first element, the rich message; second element, the
|
||||
* parameters of the rich message (or an empty array if there are no
|
||||
* parameters).
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 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\Chat\SystemMessage;
|
||||
|
||||
|
||||
use OCA\Spreed\Chat\ChatManager;
|
||||
use OCA\Spreed\Participant;
|
||||
use OCA\Spreed\Room;
|
||||
use OCA\Spreed\TalkSession;
|
||||
use OCP\IUser;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||
|
||||
class Listener {
|
||||
|
||||
/** @var EventDispatcherInterface */
|
||||
protected $dispatcher;
|
||||
/** @var ChatManager */
|
||||
protected $chatManager;
|
||||
/** @var TalkSession */
|
||||
protected $session;
|
||||
/** @var string */
|
||||
protected $userId;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, ChatManager $chatManager, TalkSession $session, $userId) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->chatManager = $chatManager;
|
||||
$this->session = $session;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function register() {
|
||||
$this->dispatcher->addListener(Room::class . '::postSessionJoinCall', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
$this->sendSystemMessage($room, 'joined_call');
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postSessionLeaveCall', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
$this->sendSystemMessage($room, 'left_call');
|
||||
});
|
||||
|
||||
$this->dispatcher->addListener(Room::class . '::createRoom', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
$this->sendSystemMessage($room, 'created_conversation');
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postSetName', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
$this->sendSystemMessage($room, 'renamed_conversation', $event->getArguments());
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postSetPassword', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
if ($event->getArgument('password')) {
|
||||
$this->sendSystemMessage($room, 'set_password');
|
||||
} else {
|
||||
$this->sendSystemMessage($room, 'removed_password');
|
||||
}
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postChangeType', function(GenericEvent $event) {
|
||||
$arguments = $event->getArguments();
|
||||
|
||||
if ($arguments['newType'] !== Room::PUBLIC_CALL && $arguments['newType'] !== Room::PUBLIC_CALL) {
|
||||
// one2one => group: Only added a user
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
$this->sendSystemMessage($room, 'change_type', $event->getArguments());
|
||||
});
|
||||
|
||||
$this->dispatcher->addListener(Room::class . '::postAddUsers', function(GenericEvent $event) {
|
||||
$participants = $event->getArgument('users');
|
||||
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
foreach ($participants as $participant) {
|
||||
if ($this->userId !== $participant['userId']) {
|
||||
$this->sendSystemMessage($room, 'user_added', ['user' => $participant['userId']]);
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postRemoveUser', function(GenericEvent $event) {
|
||||
/** @var IUser $user */
|
||||
$user = $event->getArgument('user');
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
|
||||
$this->sendSystemMessage($room, 'user_removed', ['user' => $user->getUID()]);
|
||||
});
|
||||
$this->dispatcher->addListener(Room::class . '::postSetParticipantType', function(GenericEvent $event) {
|
||||
/** @var Room $room */
|
||||
$room = $event->getSubject();
|
||||
|
||||
if ($event->getArgument('newType') === Participant::MODERATOR) {
|
||||
$this->sendSystemMessage($room, 'moderator_promoted', ['user' => $event->getArgument('user')]);
|
||||
} else if ($event->getArgument('newType') === Participant::USER) {
|
||||
$this->sendSystemMessage($room, 'moderator_demoted', ['user' => $event->getArgument('user')]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function sendSystemMessage(Room $room, string $message, array $parameters = []) {
|
||||
|
||||
if ($this->userId === null) {
|
||||
$actorType = 'guests';
|
||||
$sessionId = $this->session->getSessionForRoom($room->getToken());
|
||||
$actorId = $sessionId ? sha1($sessionId) : '';
|
||||
} else {
|
||||
$actorType = 'users';
|
||||
$actorId = $this->userId;
|
||||
}
|
||||
|
||||
$this->chatManager->addSystemMessage(
|
||||
$room, $actorType, $actorId,
|
||||
json_encode(['message' => $message, 'parameters' => $parameters]),
|
||||
new \DateTime()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 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\Chat\SystemMessage;
|
||||
|
||||
|
||||
use OCA\Spreed\Room;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class Parser {
|
||||
|
||||
/** @var IUserManager */
|
||||
protected $userManager;
|
||||
/** @var IL10N */
|
||||
protected $l;
|
||||
|
||||
/** @var string[] */
|
||||
protected $displayNames = [];
|
||||
|
||||
public function __construct(IUserManager $userManager, IL10N $l) {
|
||||
$this->userManager = $userManager;
|
||||
$this->l = $l;
|
||||
}
|
||||
|
||||
public function parseMessage(IComment $comment, string $displayName): array {
|
||||
$data = json_decode($comment->getMessage(), true);
|
||||
$message = $data['message'];
|
||||
$parameters = $data['parameters'];
|
||||
|
||||
$parsedParameters = ['actor' => $this->getActor($comment, $displayName)];
|
||||
$parsedMessage = $comment->getMessage();
|
||||
|
||||
if ($message === 'joined_call') {
|
||||
$parsedMessage = $this->l->t('{actor} joined the call');
|
||||
} else if ($message === 'left_call') {
|
||||
$parsedMessage = $this->l->t('{actor} left the call');
|
||||
} else if ($message === 'created_conversation') {
|
||||
$parsedMessage = $this->l->t('{actor} created the conversation');
|
||||
} else if ($message === 'renamed_conversation') {
|
||||
$parsedMessage = $this->l->t('{actor} renamed the conversation from "%1$s" to "%2$s"', [$parameters['oldName'], $parameters['newName']]);
|
||||
} else if ($message === 'change_type') {
|
||||
if ($parameters['newType'] === Room::PUBLIC_CALL) {
|
||||
$parsedMessage = $this->l->t('{actor} allowed guests in the conversation');
|
||||
} else {
|
||||
$parsedMessage = $this->l->t('{actor} disallowed guests in the conversation');
|
||||
}
|
||||
} else if ($message === 'set_password') {
|
||||
$parsedMessage = $this->l->t('{actor} set a password for the conversation');
|
||||
} else if ($message === 'removed_password') {
|
||||
$parsedMessage = $this->l->t('{actor} removed the password for the conversation');
|
||||
} else if ($message === 'user_added') {
|
||||
$parsedParameters['user'] = $this->getUser($parameters['user']);
|
||||
$parsedMessage = $this->l->t('{actor} added {user} to the conversation');
|
||||
} else if ($message === 'user_removed') {
|
||||
$parsedParameters['user'] = $this->getUser($parameters['user']);
|
||||
$parsedMessage = $this->l->t('{actor} removed {user} from the conversation');
|
||||
} else if ($message === 'moderator_promoted') {
|
||||
$parsedParameters['user'] = $this->getUser($parameters['user']);
|
||||
$parsedMessage = $this->l->t('{actor} promoted {user} to moderator');
|
||||
} else if ($message === 'moderator_demoted') {
|
||||
$parsedParameters['user'] = $this->getUser($parameters['user']);
|
||||
$parsedMessage = $this->l->t('{actor} demoted {user} from moderator');
|
||||
}
|
||||
|
||||
|
||||
return [$parsedMessage, $parsedParameters];
|
||||
}
|
||||
|
||||
protected function getActor(IComment $comment, string $displayName): array {
|
||||
return [
|
||||
'type' => 'user',
|
||||
'id' => $comment->getActorId(),
|
||||
'name' => $displayName,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getUser(string $uid): array {
|
||||
if (!isset($this->displayNames[$uid])) {
|
||||
$this->displayNames[$uid] = $this->getDisplayName($uid);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'user',
|
||||
'id' => $uid,
|
||||
'name' => $this->displayNames[$uid],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getDisplayName(string $uid): string {
|
||||
$user = $this->userManager->get($uid);
|
||||
if ($user instanceof IUser) {
|
||||
return $user->getDisplayName();
|
||||
}
|
||||
return $uid;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ use OCA\Spreed\Chat\AutoComplete\SearchPlugin;
|
|||
use OCA\Spreed\Chat\AutoComplete\Sorter;
|
||||
use OCA\Spreed\Chat\ChatManager;
|
||||
use OCA\Spreed\Chat\RichMessageHelper;
|
||||
use OCA\Spreed\Chat\SystemMessage\Parser;
|
||||
use OCA\Spreed\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Spreed\Exceptions\RoomNotFoundException;
|
||||
use OCA\Spreed\GuestManager;
|
||||
|
@ -84,6 +85,9 @@ class ChatController extends OCSController {
|
|||
/** @var ISearchResult */
|
||||
private $searchResult;
|
||||
|
||||
/** @var Parser */
|
||||
private $parser;
|
||||
|
||||
/** @var EventDispatcherInterface */
|
||||
private $dispatcher;
|
||||
|
||||
|
@ -99,7 +103,7 @@ class ChatController extends OCSController {
|
|||
* @param RichMessageHelper $richMessageHelper
|
||||
* @param IManager $autoCompleteManager
|
||||
* @param SearchPlugin $searchPlugin
|
||||
* @param SearchResult $searchResult
|
||||
* @param ISearchResult $searchResult
|
||||
* @param EventDispatcherInterface $dispatcher
|
||||
*/
|
||||
public function __construct($appName,
|
||||
|
@ -113,7 +117,8 @@ class ChatController extends OCSController {
|
|||
RichMessageHelper $richMessageHelper,
|
||||
IManager $autoCompleteManager,
|
||||
SearchPlugin $searchPlugin,
|
||||
SearchResult $searchResult, // FIXME for 14 ISearchResult is injectable
|
||||
ISearchResult $searchResult,
|
||||
Parser $parser,
|
||||
EventDispatcherInterface $dispatcher) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
|
@ -127,6 +132,7 @@ class ChatController extends OCSController {
|
|||
$this->autoCompleteManager = $autoCompleteManager;
|
||||
$this->searchPlugin = $searchPlugin;
|
||||
$this->searchResult = $searchResult;
|
||||
$this->parser = $parser;
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
|
@ -299,7 +305,11 @@ class ChatController extends OCSController {
|
|||
$displayName = $guestNames[$comment->getActorId()];
|
||||
}
|
||||
|
||||
list($message, $messageParameters) = $this->richMessageHelper->getRichMessage($comment);
|
||||
if ($comment->getVerb() === 'system') {
|
||||
list($message, $messageParameters) = $this->parser->parseMessage($comment, $displayName);
|
||||
} else {
|
||||
list($message, $messageParameters) = $this->richMessageHelper->getRichMessage($comment);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $comment->getId(),
|
||||
|
@ -310,6 +320,7 @@ class ChatController extends OCSController {
|
|||
'timestamp' => $comment->getCreationDateTime()->getTimestamp(),
|
||||
'message' => $message,
|
||||
'messageParameters' => $messageParameters,
|
||||
'verb' => $comment->getVerb(),
|
||||
];
|
||||
}, $comments), Http::STATUS_OK);
|
||||
|
||||
|
|
|
@ -398,7 +398,11 @@ class Manager {
|
|||
$query->execute();
|
||||
$roomId = $query->getLastInsertId();
|
||||
|
||||
return $this->getRoomById($roomId);
|
||||
$room = $this->getRoomById($roomId);
|
||||
|
||||
$this->dispatcher->dispatch(Room::class . '::createRoom', new GenericEvent($room));
|
||||
|
||||
return $room;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -389,9 +389,6 @@ class Room {
|
|||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function resetActiveSince() {
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->update('talk_rooms')
|
||||
|
@ -399,7 +396,6 @@ class Room {
|
|||
->set('active_since', $query->createNamedParameter(null, 'datetime'))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
|
||||
$query->execute();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,6 +46,7 @@ class CapabilitiesTest extends TestCase {
|
|||
'favorites',
|
||||
'last-room-activity',
|
||||
'no-ping',
|
||||
'system-messages',
|
||||
],
|
||||
],
|
||||
], $capabilities->getCapabilities());
|
||||
|
|
Загрузка…
Ссылка в новой задаче