From d5195c05622322959c34d44a25c816d38ef89b53 Mon Sep 17 00:00:00 2001 From: Maximilian Mader Date: Sun, 1 Nov 2020 00:42:28 +0100 Subject: [PATCH] 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}