From 762719179b0bc55daaf294ffdce5f773774ba0c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sat, 31 Oct 2020 18:07:45 +0100 Subject: [PATCH 01/14] Add attachment object types --- .../MessageAttachmentObjectType.class.php | 94 +++++++++++++++++++ .../type/AttachmentMessageType.class.php | 67 +++++++++++++ objectType.xml | 14 +++ 3 files changed, 175 insertions(+) create mode 100644 files/lib/system/attachment/MessageAttachmentObjectType.class.php create mode 100644 files/lib/system/message/type/AttachmentMessageType.class.php diff --git a/files/lib/system/attachment/MessageAttachmentObjectType.class.php b/files/lib/system/attachment/MessageAttachmentObjectType.class.php new file mode 100644 index 0000000..b5a9f16 --- /dev/null +++ b/files/lib/system/attachment/MessageAttachmentObjectType.class.php @@ -0,0 +1,94 @@ +getMessageType()->objectType !== 'be.bastelstu.chat.messageType.attachment') { + throw new \LogicException('Unreachable'); + } + $room = $message->getRoom(); + + return $room->canSee(); + } + + return false; + } + + /** + * @inheritDoc + */ + public function canUpload($objectID, $parentObjectID = 0) { + if ($objectID) { + return false; + } + + if (!WCF::getSession()->getPermission('user.chat.canAttach')) { + return false; + } + + $room = null; + if ($parentObjectID) { + $room = RoomCache::getInstance()->getRoom($parentObjectID); + } + + if ($room !== null) { + return $room->canSee(); + } + + return false; + } + + /** + * @inheritDoc + */ + public function canDelete($objectID) { + return false; + } + + /** + * @inheritDoc + */ + public function getMaxCount() { + return 1; + } + + /** + * @inheritDoc + */ + public function cacheObjects(array $objectIDs) { + $messageList = new MessageList(); + $messageList->setObjectIDs($objectIDs); + $messageList->readObjects(); + + foreach ($messageList->getObjects() as $objectID => $object) { + $this->cachedObjects[$objectID] = $object; + } + } +} diff --git a/files/lib/system/message/type/AttachmentMessageType.class.php b/files/lib/system/message/type/AttachmentMessageType.class.php new file mode 100644 index 0000000..eb7176d --- /dev/null +++ b/files/lib/system/message/type/AttachmentMessageType.class.php @@ -0,0 +1,67 @@ +processor = new \wcf\system\html\output\HtmlOutputProcessor(); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Plain'; + } + + /** + * @inheritDoc + */ + public function canDelete(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + return $user->getPermission('mod.chat.canDelete'); + } + + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + // TODO + + return $parameters['payload']; + } +} diff --git a/objectType.xml b/objectType.xml index d0699cd..d58f59a 100644 --- a/objectType.xml +++ b/objectType.xml @@ -132,6 +132,12 @@ be.bastelstu.chat.messageType chat\system\message\type\WhisperMessageType + + + be.bastelstu.chat.messageType.attachment + be.bastelstu.chat.messageType + chat\system\message\type\AttachmentMessageType + @@ -168,5 +174,13 @@ 1 + + + + be.bastelstu.chat.message + com.woltlab.wcf.attachment.objectType + chat\system\attachment\MessageAttachmentObjectType + + From d07d18a0cb084e3afb3864a10ef5b43e5855b736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sat, 31 Oct 2020 19:38:03 +0100 Subject: [PATCH 02/14] Hardcode acceptable attachment extensions to common images --- .../attachment/MessageAttachmentObjectType.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/files/lib/system/attachment/MessageAttachmentObjectType.class.php b/files/lib/system/attachment/MessageAttachmentObjectType.class.php index b5a9f16..bd5aadb 100644 --- a/files/lib/system/attachment/MessageAttachmentObjectType.class.php +++ b/files/lib/system/attachment/MessageAttachmentObjectType.class.php @@ -79,6 +79,17 @@ class MessageAttachmentObjectType extends \wcf\system\attachment\AbstractAttachm return 1; } + /** + * @inheritDoc + */ + public function getAllowedExtensions() { + return [ 'png' + , 'gif' + , 'jpg' + , 'jpeg' + ]; + } + /** * @inheritDoc */ From d5195c05622322959c34d44a25c816d38ef89b53 Mon Sep 17 00:00:00 2001 From: Maximilian Mader Date: Sun, 1 Nov 2020 00:42:28 +0100 Subject: [PATCH 03/14] Implement attachment upload UI skeleton --- files/lib/page/RoomPage.class.php | 13 +- files_wcf/js/Bastelstu.be/Chat.js | 4 +- .../Bastelstu.be/Chat/Ui/Attachment/Upload.js | 195 ++++++++++++++++++ files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js | 7 +- templates/__attachmentDialog.tpl | 19 ++ templates/quickSettings.tpl | 3 + templates/room.tpl | 1 + 7 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Attachment/Upload.js create mode 100644 templates/__attachmentDialog.tpl diff --git a/files/lib/page/RoomPage.class.php b/files/lib/page/RoomPage.class.php index ed01eb2..81e194e 100644 --- a/files/lib/page/RoomPage.class.php +++ b/files/lib/page/RoomPage.class.php @@ -25,6 +25,13 @@ use \wcf\system\WCF; class RoomPage extends \wcf\page\AbstractPage { use TConfiguredPage; + /** + * Almost dummy attachment handler (used in language variable) + * + * @var \wcf\system\attachment\AttachmentHandler + */ + public $attachmentHandler; + /** * @inheritDoc */ @@ -65,7 +72,7 @@ class RoomPage extends \wcf\page\AbstractPage { */ public function checkPermissions() { parent::checkPermissions(); - + $package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat'); if (stripos($package->packageVersion, 'Alpha') !== false) { $sql = "SELECT COUNT(*) FROM wcf".WCF_N."_user"; @@ -91,6 +98,9 @@ class RoomPage extends \wcf\page\AbstractPage { parent::readData(); + // This attachment handler gets only used for the language variable `wcf.attachment.upload.limits`! + $this->attachmentHandler = new \wcf\system\attachment\AttachmentHandler('be.bastelstu.chat.message', 0, 'DEADC0DE00000000DEADC0DE00000000DEADC0DE', $this->room->roomID); + $pushHandler = \wcf\system\push\PushHandler::getInstance(); $pushHandler->joinChannel('be.bastelstu.chat'); $pushHandler->joinChannel('be.bastelstu.chat.room-'.$this->room->roomID); @@ -104,6 +114,7 @@ class RoomPage extends \wcf\page\AbstractPage { WCF::getTPL()->assign([ 'room' => $this->room , 'config' => $this->getConfig() + , 'attachmentHandler' => $this->attachmentHandler ]); } } diff --git a/files_wcf/js/Bastelstu.be/Chat.js b/files_wcf/js/Bastelstu.be/Chat.js index df82e61..86bc092 100644 --- a/files_wcf/js/Bastelstu.be/Chat.js +++ b/files_wcf/js/Bastelstu.be/Chat.js @@ -27,6 +27,7 @@ define([ './Chat/console' , './Chat/ProfileStore' , './Chat/Room' , './Chat/Template' + , './Chat/Ui/Attachment/Upload' , './Chat/Ui/AutoAway' , './Chat/Ui/Chat' , './Chat/Ui/ConnectionWarning' @@ -43,7 +44,7 @@ define([ './Chat/console' , './Chat/Ui/UserActionDropdownHandler' , './Chat/Ui/UserList' ], function (console, Bottle, Push, Core, Language, RepeatingTimer, CoreUser, Autocompleter, - CommandHandler, Throttle, Message, Messenger, ParseError, ProfileStore, Room, Template, UiAutoAway, Ui, + CommandHandler, Throttle, Message, Messenger, ParseError, ProfileStore, Room, Template, UiAttachmentUpload, UiAutoAway, Ui, UiConnectionWarning, ErrorDialog, UiInput, UiInputAutocompleter, UiMessageStream, UiMessageActionDelete, UiMobile, UiNotification, UiReadMarker, UiSettings, UiTopic, UiUserActionDropdownHandler, UiUserList) { "use strict"; @@ -85,6 +86,7 @@ define([ './Chat/console' this.service('UiTopic', UiTopic) this.service('UiUserActionDropdownHandler', UiUserActionDropdownHandler) this.service('UiUserList', UiUserList) + this.service('UiAttachmentUpload', UiAttachmentUpload) // Register Models this.bottle.instanceFactory('Message', (container, m) => { diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Attachment/Upload.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Attachment/Upload.js new file mode 100644 index 0000000..a69bc67 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Attachment/Upload.js @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2010-2020 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2024-11-01 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Language' + , 'WoltLabSuite/Core/Upload' + , 'WoltLabSuite/Core/Dom/Change/Listener' + , 'WoltLabSuite/Core/Dom/Util' + , 'WoltLabSuite/Core/Ui/Dialog' + , '../../DataStructure/EventEmitter' + ], function(Language, Upload, DomChangeListener, DomUtil, Dialog, EventEmitter) { + "use strict"; + + const DIALOG_BUTTON_ID = 'chatAttachmentUploadButton' + const DIALOG_CONTAINER_ID = 'chatAttachmentUploadDialog' + + const DEPENDENCIES = [ 'Room' ]; + class UiAttachmentUpload extends Upload { + constructor(room) { + const buttonContainer = document.querySelector(`#${DIALOG_CONTAINER_ID} > .upload`) + const buttonContainerId = DomUtil.identify(buttonContainer) + + const previewContainer = document.querySelector(`#${DIALOG_CONTAINER_ID} > .attachmentPreview`) + const previewContainerId = DomUtil.identify(previewContainer) + + super(buttonContainerId, previewContainerId, { + className: 'wcf\\data\\attachment\\AttachmentAction', + acceptableFiles: [ '.png', '.gif', '.jpg', '.jpeg' ] + }) + + this.room = room + this.previewContainer = previewContainer + } + + bootstrap() { + this.uploadDescription = document.querySelector(`#${DIALOG_CONTAINER_ID} > small`) + + const button = document.getElementById(DIALOG_BUTTON_ID) + const container = document.getElementById(DIALOG_CONTAINER_ID) + + elHide(container) + container.classList.remove('jsStaticDialogContent') + container.dataset.isStaticDialog = 'true' + + if (button) { + button.addEventListener('click', (event) => { + event.preventDefault() + + Dialog.openStatic(container.id, null, { + title: elData(container, 'title'), + onShow: () => this.showDialog() + }) + }) + + const deleteAction = new WCF.Action.Delete('wcf\\data\\attachment\\AttachmentAction', `#${this.previewContainer.id} > p`) + deleteAction.setCallback(() => this.closeDialog()) + } + } + + closeDialog() { + if (Dialog.getDialog(DIALOG_CONTAINER_ID)) { + Dialog.close(DIALOG_CONTAINER_ID) + } + } + + showDialog() { + if (this._button.parentNode) { + this._removeButton() + } + + this._target.innerHTML = '' + this._createButton() + elShow(this.uploadDescription) + } + + async send(event) { + event.preventDefault() + const parameters = { promise: Promise.resolve() } + this.emit('send', parameters) + + try { + await parameters.promise + this.closeDialog() + } + catch (error) { + // TODO: Error handling + console.error(error) + } + } + + createButtonGroup(uploadId, objectID) { + const buttonGroup = document.createElement('ul') + buttonGroup.classList.add('buttonGroup') + + let li = document.createElement('li') + const cancelButton = document.createElement('span') + cancelButton.classList.add('button', 'jsDeleteButton') + cancelButton.dataset.objectId = objectID + cancelButton.dataset.eventName = 'attachment' + cancelButton.innerText = Language.get('wcf.global.button.cancel') + li.appendChild(cancelButton) + buttonGroup.appendChild(li) + + li = document.createElement('li') + const sendButton = document.createElement('span') + sendButton.classList.add('button') + sendButton.dataset.objectId = objectID + sendButton.innerText = Language.get('wcf.global.button.submit') + sendButton.addEventListener('click', (e) => this.send(e)) + li.appendChild(sendButton) + buttonGroup.appendChild(li) + + const target = this._fileElements[uploadId][0] + target.appendChild(buttonGroup) + + DomChangeListener.trigger() + } + + /** + * @see WoltLabSuite/Core/Upload#_getParameters + */ + _getParameters() { + const hash = [ ...crypto.getRandomValues(new Uint8Array(20)) ] + .map(m => ('0' + m.toString(16)).slice(-2)) + .join('') + + return { objectType: "be.bastelstu.chat.message" + , parentObjectID: this.room.roomID + , tmpHash: hash + } + } + + /** + * @see WoltLabSuite/Core/Upload#_success + */ + _success(uploadId, data, responseText, xhr, requestOptions) { + if (data.returnValues?.errors?.[0]) { + const error = data.returnValues.errors[0] + + elInnerError(this._button, Language.get(`wcf.attachment.upload.error.${error.errorType}`, { + filename: error.filename + })) + } + else { + elInnerError(this._button, '') + } + + if (data.returnValues?.attachments?.[uploadId]) { + this._removeButton() + elHide(this.uploadDescription) + + const attachment = data.returnValues.attachments[uploadId] + const url = attachment.thumbnailURL || attachment.tinyURL || attachment.url + + if (!url) { + throw new Error('Missing image URL') + } + + const target = this._fileElements[uploadId][0] + const progress = target.querySelector(':scope > progress') + + const img = document.createElement('img') + img.setAttribute('src', url) + img.setAttribute('alt', '') + + if (url === attachment.thumbnailURL) { + img.classList.add('attachmentThumbnail') + } + else if (url === attachment.tinyURL) { + img.classList.add('attachmentTinyThumbnail') + } + + img.dataset.width = attachment.width + img.dataset.height = attachment.height + + DomUtil.replaceElement(progress, img) + + this.createButtonGroup(uploadId, attachment.attachmentID) + } + } + } + UiAttachmentUpload.DEPENDENCIES = DEPENDENCIES + EventEmitter(UiAttachmentUpload.prototype) + + return UiAttachmentUpload +}) diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js index 5b73f91..f37d86b 100644 --- a/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js @@ -14,7 +14,8 @@ define([ '../Ui' ], function (Ui) { "use strict"; - const DEPENDENCIES = [ 'UiAutoAway' + const DEPENDENCIES = [ 'UiAttachmentUpload' + , 'UiAutoAway' , 'UiConnectionWarning' , 'UiInput' , 'UiInputAutocompleter' @@ -29,7 +30,7 @@ define([ '../Ui' ], function (Ui) { , 'UiUserList' ] class Chat extends Ui { - constructor(autoAway, connectionWarning, input, autocompleter, messageStream, messageActionDelete, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) { + constructor(attachmentUpload, autoAway, connectionWarning, input, autocompleter, messageStream, messageActionDelete, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) { super() this.actionDropdownHandler = userActionDropdownHandler @@ -45,6 +46,7 @@ define([ '../Ui' ], function (Ui) { this.settings = settings this.topic = topic this.userList = userList + this.attachmentUpload = attachmentUpload } bootstrap() { @@ -61,6 +63,7 @@ define([ '../Ui' ], function (Ui) { this.settings.bootstrap() this.topic.bootstrap() this.userList.bootstrap() + this.attachmentUpload.bootstrap() } } Chat.DEPENDENCIES = DEPENDENCIES diff --git a/templates/__attachmentDialog.tpl b/templates/__attachmentDialog.tpl new file mode 100644 index 0000000..c49deb4 --- /dev/null +++ b/templates/__attachmentDialog.tpl @@ -0,0 +1,19 @@ +
+
+ + {* placeholder for the upload button *} +
+ {lang}wcf.attachment.upload.limits{/lang} +
+ diff --git a/templates/quickSettings.tpl b/templates/quickSettings.tpl index 83da260..8d18513 100644 --- a/templates/quickSettings.tpl +++ b/templates/quickSettings.tpl @@ -4,6 +4,9 @@
  • {lang}chat.room.button.fullscreen{/lang}
  • {lang}chat.room.button.notifications{/lang}
  • {lang}chat.room.button.autoscroll{/lang}
  • + {if $__wcf->getSession()->getPermission('user.chat.canAttach')} +
  • {lang}wcf.attachment.attachments{/lang} + {/if} {event name='buttons'} diff --git a/templates/room.tpl b/templates/room.tpl index d4930c9..6c1e044 100644 --- a/templates/room.tpl +++ b/templates/room.tpl @@ -71,6 +71,7 @@ {include file='messageTypes' application='chat'} {include file='userList' application='chat'} {include file='userListDropdownMenuItems' application='chat'} +{include file='__attachmentDialog' application='chat'} {if !ENABLE_DEBUG_MODE}{js application='wcf' file='Bastelstu.be.Chat'}{/if} + +