diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index ce71303c07b..03cf8929f1d 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\DAV\CardDAV\PhotoCache; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; use OCA\Talk\Model\Attendee; @@ -43,6 +44,7 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Share\Exceptions\ShareNotFound; +use Sabre\VObject\Reader; class SystemMessage { @@ -56,6 +58,8 @@ class SystemMessage { protected $previewManager; /** @var RoomShareProvider */ protected $shareProvider; + /** @var PhotoCache */ + protected $photoCache; /** @var IRootFolder */ protected $rootFolder; /** @var IURLGenerator */ @@ -75,6 +79,7 @@ public function __construct(IUserManager $userManager, GuestManager $guestManager, IPreviewManager $previewManager, RoomShareProvider $shareProvider, + PhotoCache $photoCache, IRootFolder $rootFolder, IURLGenerator $url) { $this->userManager = $userManager; @@ -82,6 +87,7 @@ public function __construct(IUserManager $userManager, $this->guestManager = $guestManager; $this->previewManager = $previewManager; $this->shareProvider = $shareProvider; + $this->photoCache = $photoCache; $this->rootFolder = $rootFolder; $this->url = $url; } @@ -507,7 +513,7 @@ protected function getFileFromShare(Participant $participant, string $shareId): ]); } - return [ + $data = [ 'type' => 'file', 'id' => (string) $node->getId(), 'name' => $name, @@ -517,6 +523,23 @@ protected function getFileFromShare(Participant $participant, string $shareId): 'mimetype' => $node->getMimeType(), 'preview-available' => $this->previewManager->isAvailable($node) ? 'yes' : 'no', ]; + + if ($node->getMimeType() === 'text/vcard') { + $vCard = $node->getContent(); + + $vObject = Reader::read($vCard); + if (!empty($vObject->FN)) { + $data['contact-name'] = (string) $vObject->FN; + } + + $photo = $this->photoCache->getPhotoFromVObject($vObject); + if ($photo) { + $data['contact-photo-mimetype'] = $photo['Content-Type']; + $data['contact-photo'] = base64_encode($photo['body']); + } + } + + return $data; } protected function getActorFromComment(Room $room, IComment $comment): array { diff --git a/psalm.xml b/psalm.xml index b29e3e4ea91..ea6b9675917 100644 --- a/psalm.xml +++ b/psalm.xml @@ -24,9 +24,10 @@ - + + @@ -40,6 +41,7 @@ + diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 93065ed19ab..654b9ed6cf4 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -222,6 +222,7 @@ import { } from '@nextcloud/dialogs' import { generateUrl } from '@nextcloud/router' import Location from './MessagePart/Location' +import Contact from './MessagePart/Contact.vue' export default { name: 'Message', @@ -488,12 +489,14 @@ export default { const richParameters = {} Object.keys(this.messageParameters).forEach(function(p) { const type = this.messageParameters[p].type + console.debug(this.messageParameters[p].mimetype) + const mimetype = this.messageParameters[p].mimetype if (type === 'user' || type === 'call' || type === 'guest') { richParameters[p] = { component: Mention, props: this.messageParameters[p], } - } else if (type === 'file') { + } else if (type === 'file' && mimetype !== 'text/vcard') { richParameters[p] = { component: FilePreview, props: this.messageParameters[p], @@ -508,6 +511,11 @@ export default { component: Location, props: this.messageParameters[p], } + } else if (mimetype === 'text/vcard') { + richParameters[p] = { + component: Contact, + props: this.messageParameters[p], + } } else { richParameters[p] = { component: DefaultParameter, diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue new file mode 100644 index 00000000000..1b7a0e1b04d --- /dev/null +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Contact.vue @@ -0,0 +1,138 @@ + + + + + + + diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue index 162f1e3b7e6..436cf097092 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.vue @@ -65,7 +65,7 @@
- {{ name }} + {{ fileDetail }}
@@ -153,6 +153,7 @@ export default { type: String, default: 'no', }, + /** * Whether to render a small preview to embed in replies */ @@ -204,8 +205,8 @@ export default { } }, computed: { - shouldShowFileName() { - // display the file name below the preview if the preview + shouldShowFileDetail() { + // display the file detail below the preview if the preview // is not easily recognizable, when: return ( // the file is not an image @@ -218,8 +219,13 @@ export default { || this.isUploadEditor ) }, + + fileDetail() { + return this.name + }, + previewTooltip() { - if (this.shouldShowFileName) { + if (this.shouldShowFileDetail) { // no tooltip as the file name is already visible directly return null } @@ -229,6 +235,7 @@ export default { placement: 'left', } }, + // This is used to decide which outer element type to use // a or div filePreview() { @@ -253,9 +260,11 @@ export default { rel: 'noopener noreferrer', } }, + defaultIconUrl() { return imagePath('core', 'filetypes/file') }, + previewImageClass() { let classes = '' if (this.smallPreview) { @@ -269,6 +278,7 @@ export default { } return classes }, + previewType() { if (this.hasTemporaryImageUrl) { return PREVIEW_TYPE.TEMPORARY @@ -284,6 +294,7 @@ export default { return PREVIEW_TYPE.PREVIEW }, + previewUrl() { const userId = this.$store.getters.getUserId() @@ -326,6 +337,7 @@ export default { }) } }, + isViewerAvailable() { if (!OCA.Viewer) { return false @@ -340,6 +352,7 @@ export default { return false }, + isPlayable() { // don't show play button for direct renders if (this.failed || !this.isViewerAvailable || this.previewType !== PREVIEW_TYPE.PREVIEW) { @@ -349,6 +362,7 @@ export default { // videos only display a preview, so always show a button if playable return this.mimetype === 'image/gif' || this.mimetype.startsWith('video/') }, + internalAbsolutePath() { if (this.path.startsWith('/')) { return this.path @@ -356,9 +370,11 @@ export default { return '/' + this.path }, + isTemporaryUpload() { return this.id.startsWith('temp') && this.index && this.uploadId }, + uploadProgress() { if (this.isTemporaryUpload) { if (this.$store.getters.uploadProgress(this.uploadId, this.index)) { @@ -368,6 +384,7 @@ export default { // likely never reached return 0 }, + hasTemporaryImageUrl() { return this.mimetype.startsWith('image/') && this.localUrl }, @@ -380,6 +397,7 @@ export default { return t('spreed', 'Remove {fileName}', { fileName: this.name }) }, }, + mounted() { const img = new Image() img.onerror = () => { @@ -391,6 +409,7 @@ export default { } img.src = this.previewUrl }, + methods: { handleClick(event) { if (this.isUploadEditor) { diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php index 5f140fb2324..c7ea3b78f41 100644 --- a/tests/php/Chat/Parser/SystemMessageTest.php +++ b/tests/php/Chat/Parser/SystemMessageTest.php @@ -21,6 +21,7 @@ namespace OCA\Talk\Tests\php\Chat\Parser; +use OCA\DAV\CardDAV\PhotoCache; use OCA\Talk\Chat\Parser\SystemMessage; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; @@ -63,6 +64,8 @@ class SystemMessageTest extends TestCase { protected $previewManager; /** @var RoomShareProvider|MockObject */ protected $shareProvider; + /** @var PhotoCache|MockObject */ + protected $photoCache; /** @var IRootFolder|MockObject */ protected $rootFolder; /** @var IURLGenerator|MockObject */ @@ -78,6 +81,7 @@ public function setUp(): void { $this->guestManager = $this->createMock(GuestManager::class); $this->previewManager = $this->createMock(IPreviewManager::class); $this->shareProvider = $this->createMock(RoomShareProvider::class); + $this->photoCache = $this->createMock(PhotoCache::class); $this->rootFolder = $this->createMock(IRootFolder::class); $this->url = $this->createMock(IURLGenerator::class); $this->l = $this->createMock(IL10N::class); @@ -107,6 +111,7 @@ protected function getParser(array $methods = []): SystemMessage { $this->guestManager, $this->previewManager, $this->shareProvider, + $this->photoCache, $this->rootFolder, $this->url, ]) @@ -121,6 +126,7 @@ protected function getParser(array $methods = []): SystemMessage { $this->guestManager, $this->previewManager, $this->shareProvider, + $this->photoCache, $this->rootFolder, $this->url ); @@ -540,7 +546,7 @@ public function testGetFileFromShareForGuest() { $node->expects($this->once()) ->method('getName') ->willReturn('name'); - $node->expects($this->once()) + $node->expects($this->atLeastOnce()) ->method('getMimeType') ->willReturn('text/plain'); $node->expects($this->once()) @@ -601,7 +607,7 @@ public function testGetFileFromShareForOwner() { $node->expects($this->once()) ->method('getPath') ->willReturn('/owner/files/path/to/file/name'); - $node->expects($this->once()) + $node->expects($this->atLeastOnce()) ->method('getMimeType') ->willReturn('httpd/unix-directory'); $node->expects($this->once()) @@ -666,7 +672,7 @@ public function testGetFileFromShareForRecipient() { $node->expects($this->once()) ->method('getName') ->willReturn('name'); - $node->expects($this->once()) + $node->expects($this->atLeastOnce()) ->method('getMimeType') ->willReturn('application/octet-stream'); $node->expects($this->once())