Merge branch 'attachment'

This commit is contained in:
Tim Düsterhus 2020-11-01 17:07:08 +01:00
commit b49ac97e10
Signed by: TimWolla
GPG Key ID: 8FF75566094168AF
18 changed files with 645 additions and 27 deletions

View File

@ -1,11 +1,11 @@
<?php <?php
/* /*
* Copyright (c) 2010-2018 Tim Düsterhus. * Copyright (c) 2010-2020 Tim Düsterhus.
* *
* Use of this software is governed by the Business Source License * Use of this software is governed by the Business Source License
* included in the LICENSE file. * included in the LICENSE file.
* *
* Change Date: 2024-10-20 * Change Date: 2024-11-01
* *
* On the date above, in accordance with the Business Source * On the date above, in accordance with the Business Source
* License, use of this software will be governed by version 2 * License, use of this software will be governed by version 2
@ -17,6 +17,7 @@ namespace chat\data\message;
use \chat\data\command\CommandCache; use \chat\data\command\CommandCache;
use \chat\data\room\RoomCache; use \chat\data\room\RoomCache;
use \wcf\data\object\type\ObjectTypeCache; use \wcf\data\object\type\ObjectTypeCache;
use \wcf\system\attachment\AttachmentHandler;
use \wcf\system\exception\PermissionDeniedException; use \wcf\system\exception\PermissionDeniedException;
use \wcf\system\exception\UserInputException; use \wcf\system\exception\UserInputException;
use \wcf\system\user\activity\point\UserActivityPointHandler; use \wcf\system\user\activity\point\UserActivityPointHandler;
@ -304,4 +305,66 @@ class MessageAction extends \wcf\data\AbstractDatabaseObjectAction {
$processor->validate($this->parameters['parameters'], $room); $processor->validate($this->parameters['parameters'], $room);
$processor->execute($this->parameters['parameters'], $room); $processor->execute($this->parameters['parameters'], $room);
} }
/**
* Validates parameters and permissions.
*/
public function validatePushAttachment() {
$this->readInteger('roomID');
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) throw new UserInputException('roomID');
if (!$room->canSee($user = null, $reason)) throw $reason;
$user = new \chat\data\user\User(WCF::getUser());
if (!$user->isInRoom($room)) throw new PermissionDeniedException();
$this->readString('tmpHash');
}
/**
* Pushes a new attachment into the given room.
*/
public function pushAttachment() {
$objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.attachment');
if (!$objectTypeID) throw new \LogicException('Missing object type');
$room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
if ($room === null) throw new UserInputException('roomID');
$attachmentHandler = new AttachmentHandler('be.bastelstu.chat.message', 0, $this->parameters['tmpHash'], $room->roomID);
$attachments = $attachmentHandler->getAttachmentList();
$attachmentIDs = [];
foreach ($attachments as $attachment) {
$attachmentIDs[] = $attachment->attachmentID;
}
$processor = new \wcf\system\html\input\HtmlInputProcessor();
$processor->process(implode(' ', array_map(function ($attachmentID) {
return '[attach='.$attachmentID.',none,true][/attach]';
}, $attachmentIDs)), 'be.bastelstu.chat.message', 0);
WCF::getDB()->beginTransaction();
/** @var Message $message */
$message = (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID
, 'userID' => WCF::getUser()->userID
, 'username' => WCF::getUser()->username
, 'time' => TIME_NOW
, 'objectTypeID' => $objectTypeID
, 'payload' => serialize([ 'attachmentIDs' => $attachmentIDs
, 'message' => $processor->getHtml()
])
]
]
)
)->executeAction()['returnValues'];
$attachmentHandler->updateObjectID($message->messageID);
$processor->setObjectID($message->messageID);
if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($processor)) {
(new MessageEditor($message))->update([
'hasEmbeddedObjects' => 1
]);
}
WCF::getDB()->commitTransaction();
}
} }

View File

@ -1,11 +1,11 @@
<?php <?php
/* /*
* Copyright (c) 2010-2018 Tim Düsterhus. * Copyright (c) 2010-2020 Tim Düsterhus.
* *
* Use of this software is governed by the Business Source License * Use of this software is governed by the Business Source License
* included in the LICENSE file. * included in the LICENSE file.
* *
* Change Date: 2024-10-20 * Change Date: 2024-11-01
* *
* On the date above, in accordance with the Business Source * On the date above, in accordance with the Business Source
* License, use of this software will be governed by version 2 * License, use of this software will be governed by version 2
@ -14,6 +14,9 @@
namespace chat\data\message; namespace chat\data\message;
use \wcf\system\attachment\AttachmentHandler;
use \wcf\system\WCF;
/** /**
* Represents a chat message editor. * Represents a chat message editor.
*/ */
@ -22,4 +25,20 @@ class MessageEditor extends \wcf\data\DatabaseObjectEditor {
* @inheritDoc * @inheritDoc
*/ */
protected static $baseClass = Message::class; protected static $baseClass = Message::class;
/**
* @inheritDoc
*/
public static function deleteAll(array $messageIDs = []) {
WCF::getDB()->beginTransaction();
$result = parent::deleteAll($messageIDs);
if (!empty($messageIDs)) {
AttachmentHandler::removeAttachments('be.bastelstu.chat.message', $messageIDs);
}
WCF::getDB()->commitTransaction();
return $result;
}
} }

View File

@ -25,6 +25,13 @@ use \wcf\system\WCF;
class RoomPage extends \wcf\page\AbstractPage { class RoomPage extends \wcf\page\AbstractPage {
use TConfiguredPage; use TConfiguredPage;
/**
* Almost dummy attachment handler (used in language variable)
*
* @var \wcf\system\attachment\AttachmentHandler
*/
public $attachmentHandler;
/** /**
* @inheritDoc * @inheritDoc
*/ */
@ -65,7 +72,7 @@ class RoomPage extends \wcf\page\AbstractPage {
*/ */
public function checkPermissions() { public function checkPermissions() {
parent::checkPermissions(); parent::checkPermissions();
$package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat'); $package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat');
if (stripos($package->packageVersion, 'Alpha') !== false) { if (stripos($package->packageVersion, 'Alpha') !== false) {
$sql = "SELECT COUNT(*) FROM wcf".WCF_N."_user"; $sql = "SELECT COUNT(*) FROM wcf".WCF_N."_user";
@ -91,6 +98,9 @@ class RoomPage extends \wcf\page\AbstractPage {
parent::readData(); 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 = \wcf\system\push\PushHandler::getInstance();
$pushHandler->joinChannel('be.bastelstu.chat'); $pushHandler->joinChannel('be.bastelstu.chat');
$pushHandler->joinChannel('be.bastelstu.chat.room-'.$this->room->roomID); $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 WCF::getTPL()->assign([ 'room' => $this->room
, 'config' => $this->getConfig() , 'config' => $this->getConfig()
, 'attachmentHandler' => $this->attachmentHandler
]); ]);
} }
} }

View File

@ -0,0 +1,105 @@
<?php
/*
* 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-10-31
*
* 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.
*/
namespace chat\system\attachment;
use \chat\data\message\Message;
use \chat\data\message\MessageList;
use \chat\data\room\RoomCache;
use \wcf\system\WCF;
/**
* Attachment object type implementation for messages.
*/
class MessageAttachmentObjectType extends \wcf\system\attachment\AbstractAttachmentObjectType {
/**
* @inheritDoc
*/
public function canDownload($objectID) {
if ($objectID) {
$message = new Message($objectID);
if ($message->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 getAllowedExtensions() {
return [ 'png'
, 'gif'
, 'jpg'
, 'jpeg'
];
}
/**
* @inheritDoc
*/
public function cacheObjects(array $objectIDs) {
$messageList = new MessageList();
$messageList->setObjectIDs($objectIDs);
$messageList->readObjects();
foreach ($messageList->getObjects() as $objectID => $object) {
$this->cachedObjects[$objectID] = $object;
}
}
}

View File

@ -0,0 +1,78 @@
<?php
/*
* 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-10-31
*
* 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.
*/
namespace chat\system\message\type;
/**
* AttachmentMessageType represents a message with an attached file.
*/
class AttachmentMessageType implements IMessageType, IDeletableMessageType {
use TCanSeeInSameRoom;
/**
* HtmlOutputProcessor to use.
* @var \wcf\system\html\output\HtmlOutputProcessor
*/
protected $processor = null;
public function __construct() {
$this->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;
$payload['formattedMessage'] = null;
$payload['plaintextMessage'] = null;
$parameters = [ 'message' => $message
, 'user' => $user
, 'payload' => $payload
];
\wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters);
if ($parameters['payload']['formattedMessage'] === null) {
$this->processor->setOutputType('text/html');
$this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID);
$parameters['payload']['formattedMessage'] = $this->processor->getHtml();
}
if ($parameters['payload']['plaintextMessage'] === null) {
$this->processor->setOutputType('text/plain');
$this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID);
$parameters['payload']['plaintextMessage'] = $this->processor->getHtml();
}
return $parameters['payload'];
}
}

View File

@ -57,6 +57,7 @@ $chatEmbedMaxWidth: 400px;
} }
#content { #content {
margin-left: 20px;
width: auto !important; width: auto !important;
} }
@ -378,15 +379,46 @@ $chatEmbedMaxWidth: 400px;
> div { > div {
display: flex; display: flex;
align-items: center;
> .flexibleTextarea { > div.chatAttachButton {
flex: 1 0 auto; flex-grow: 0;
max-width: 100%; flex-shrink: 0;
margin-right: 5px;
@include screen-xs {
> .disabled {
display: none;
}
}
.icon16 {
display: none;
}
@include screen-lg {
.icon16 {
display: inline-block;
}
.icon24 {
display: none;
}
}
} }
> #chatQuickSettings { > div.chatInputWrapper {
flex: 0 0 auto; flex-grow: 1;
display: flex;
align-items: center;
> .flexibleTextarea {
flex: 1 0 auto;
max-width: 100%;
}
> #chatQuickSettings {
flex: 0 0 auto;
}
} }
} }

View File

@ -4,7 +4,7 @@
* Use of this software is governed by the Business Source License * Use of this software is governed by the Business Source License
* included in the LICENSE file. * included in the LICENSE file.
* *
* Change Date: 2024-10-31 * Change Date: 2024-11-01
* *
* On the date above, in accordance with the Business Source * On the date above, in accordance with the Business Source
* License, use of this software will be governed by version 2 * License, use of this software will be governed by version 2
@ -27,6 +27,7 @@ define([ './Chat/console'
, './Chat/ProfileStore' , './Chat/ProfileStore'
, './Chat/Room' , './Chat/Room'
, './Chat/Template' , './Chat/Template'
, './Chat/Ui/Attachment/Upload'
, './Chat/Ui/AutoAway' , './Chat/Ui/AutoAway'
, './Chat/Ui/Chat' , './Chat/Ui/Chat'
, './Chat/Ui/ConnectionWarning' , './Chat/Ui/ConnectionWarning'
@ -43,7 +44,7 @@ define([ './Chat/console'
, './Chat/Ui/UserActionDropdownHandler' , './Chat/Ui/UserActionDropdownHandler'
, './Chat/Ui/UserList' , './Chat/Ui/UserList'
], function (console, Bottle, Push, Core, Language, RepeatingTimer, CoreUser, Autocompleter, ], 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, UiConnectionWarning, ErrorDialog, UiInput, UiInputAutocompleter, UiMessageStream, UiMessageActionDelete, UiMobile, UiNotification,
UiReadMarker, UiSettings, UiTopic, UiUserActionDropdownHandler, UiUserList) { UiReadMarker, UiSettings, UiTopic, UiUserActionDropdownHandler, UiUserList) {
"use strict"; "use strict";
@ -85,6 +86,7 @@ define([ './Chat/console'
this.service('UiTopic', UiTopic) this.service('UiTopic', UiTopic)
this.service('UiUserActionDropdownHandler', UiUserActionDropdownHandler) this.service('UiUserActionDropdownHandler', UiUserActionDropdownHandler)
this.service('UiUserList', UiUserList) this.service('UiUserList', UiUserList)
this.service('UiAttachmentUpload', UiAttachmentUpload)
// Register Models // Register Models
this.bottle.instanceFactory('Message', (container, m) => { this.bottle.instanceFactory('Message', (container, m) => {
@ -176,8 +178,11 @@ define([ './Chat/console'
this.ui = this.bottle.container.Ui this.ui = this.bottle.container.Ui
this.ui.bootstrap() this.ui.bootstrap()
this.bottle.container.UiInput.on('submit', this.onSubmit.bind(this)) this.ui.input.on('submit', this.onSubmit.bind(this))
this.bottle.container.UiInput.on('autocomplete', this.onAutocomplete.bind(this)) this.ui.input.on('autocomplete', this.onAutocomplete.bind(this))
this.ui.attachmentUpload.on('send', (event) => {
event.detail.promise = this.onSendAttachment(event)
})
await this.bottle.container.Room.join() await this.bottle.container.Room.join()
@ -328,6 +333,10 @@ define([ './Chat/console'
} }
} }
async onSendAttachment(event) {
return this.bottle.container.Messenger.pushAttachment(event.detail.tmpHash)
}
onAutocomplete(event) { onAutocomplete(event) {
const input = event.target const input = event.target
const value = input.getText(true) const value = input.getText(true)

View File

@ -1,10 +1,10 @@
/* /*
* Copyright (c) 2010-2018 Tim Düsterhus. * Copyright (c) 2010-2020 Tim Düsterhus.
* *
* Use of this software is governed by the Business Source License * Use of this software is governed by the Business Source License
* included in the LICENSE file. * included in the LICENSE file.
* *
* Change Date: 2024-10-20 * Change Date: 2024-11-01
* *
* On the date above, in accordance with the Business Source * On the date above, in accordance with the Business Source
* License, use of this software will be governed by version 2 * License, use of this software will be governed by version 2
@ -56,6 +56,14 @@ define([ './console'
return Ajax.api(this, payload) return Ajax.api(this, payload)
} }
async pushAttachment(tmpHash) {
const payload = { actionName: 'pushAttachment'
, parameters: { tmpHash }
}
return Ajax.api(this, payload)
}
_ajaxSetup() { _ajaxSetup() {
return { silent: true return { silent: true
, ignoreError: true , ignoreError: true

View File

@ -0,0 +1,207 @@
/*
* 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 = [ 'UiInput', 'Room' ];
class UiAttachmentUpload extends Upload {
constructor(input, 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.input = input
this.room = room
this.previewContainer = previewContainer
this.tmpHash = undefined
}
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())
this.input.on('input', (event) => {
if (event.target.input.value.length == 0) {
button.classList.remove('disabled')
}
else {
button.classList.add('disabled')
}
})
}
}
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(tmpHash, event) {
event.preventDefault()
const parameters = { promise: Promise.resolve()
, tmpHash
}
this.emit('send', parameters)
try {
await parameters.promise
this.closeDialog()
}
catch (error) {
// TODO: Error handling
console.error(error)
}
}
createButtonGroup(uploadId, objectId, tmpHash) {
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.innerText = Language.get('wcf.global.button.submit')
sendButton.addEventListener('click', (e) => this.send(tmpHash, e))
li.appendChild(sendButton)
buttonGroup.appendChild(li)
const target = this._fileElements[uploadId][0]
target.appendChild(buttonGroup)
DomChangeListener.trigger()
}
/**
* @see WoltLabSuite/Core/Upload#_getParameters
*/
_getParameters() {
this.tmpHash = [ ...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: this.tmpHash
}
}
/**
* @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, this.tmpHash)
}
}
}
UiAttachmentUpload.DEPENDENCIES = DEPENDENCIES
EventEmitter(UiAttachmentUpload.prototype)
return UiAttachmentUpload
})

View File

@ -14,12 +14,13 @@
define([ '../Ui' ], function (Ui) { define([ '../Ui' ], function (Ui) {
"use strict"; "use strict";
const DEPENDENCIES = [ 'UiAutoAway' const DEPENDENCIES = [ 'UiAttachmentUpload'
, 'UiAutoAway'
, 'UiConnectionWarning' , 'UiConnectionWarning'
, 'UiInput' , 'UiInput'
, 'UiInputAutocompleter' , 'UiInputAutocompleter'
, 'UiMessageStream'
, 'UiMessageActionDelete' , 'UiMessageActionDelete'
, 'UiMessageStream'
, 'UiMobile' , 'UiMobile'
, 'UiNotification' , 'UiNotification'
, 'UiReadMarker' , 'UiReadMarker'
@ -29,16 +30,17 @@ define([ '../Ui' ], function (Ui) {
, 'UiUserList' , 'UiUserList'
] ]
class Chat extends Ui { class Chat extends Ui {
constructor(autoAway, connectionWarning, input, autocompleter, messageStream, messageActionDelete, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) { constructor(attachmentUpload, autoAway, connectionWarning, input, autocompleter, messageActionDelete, messageStream, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) {
super() super()
this.actionDropdownHandler = userActionDropdownHandler this.actionDropdownHandler = userActionDropdownHandler
this.attachmentUpload = attachmentUpload
this.autoAway = autoAway this.autoAway = autoAway
this.autocompleter = autocompleter this.autocompleter = autocompleter
this.connectionWarning = connectionWarning this.connectionWarning = connectionWarning
this.input = input this.input = input
this.messageStream = messageStream
this.messageActionDelete = messageActionDelete this.messageActionDelete = messageActionDelete
this.messageStream = messageStream
this.mobile = mobile this.mobile = mobile
this.notification = notification this.notification = notification
this.readMarker = readMarker this.readMarker = readMarker
@ -49,12 +51,13 @@ define([ '../Ui' ], function (Ui) {
bootstrap() { bootstrap() {
this.actionDropdownHandler.bootstrap() this.actionDropdownHandler.bootstrap()
this.attachmentUpload.bootstrap()
this.autoAway.bootstrap() this.autoAway.bootstrap()
this.autocompleter.bootstrap() this.autocompleter.bootstrap()
this.connectionWarning.bootstrap() this.connectionWarning.bootstrap()
this.input.bootstrap() this.input.bootstrap()
this.messageStream.bootstrap()
this.messageActionDelete.bootstrap() this.messageActionDelete.bootstrap()
this.messageStream.bootstrap()
this.mobile.bootstrap() this.mobile.bootstrap()
this.notification.bootstrap() this.notification.bootstrap()
this.readMarker.bootstrap() this.readMarker.bootstrap()

View File

@ -63,7 +63,7 @@ define([ './ToggleButton'
*/ */
setupMobile() { setupMobile() {
this.shadowToggleButton = document.createElement('span') this.shadowToggleButton = document.createElement('span')
this.shadowToggleButton.classList.add('smiliesToggleMobileButton') this.shadowToggleButton.classList.add('smiliesToggleMobileButton', 'button', 'small')
this.shadowToggleButton.innerHTML = '<span class="icon icon24 fa-smile-o"></span>' this.shadowToggleButton.innerHTML = '<span class="icon icon24 fa-smile-o"></span>'
this.shadowToggleButton.addEventListener('mousedown', this.onClick.bind(this)) this.shadowToggleButton.addEventListener('mousedown', this.onClick.bind(this))

View File

@ -198,6 +198,7 @@
<item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Kann knebeln]]></item> <item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Kann knebeln]]></item>
<item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Achtung: Diese Berechtigung kann nicht über Raumspezifische Rechte entzogen werden.]]></item> <item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Achtung: Diese Berechtigung kann nicht über Raumspezifische Rechte entzogen werden.]]></item>
<item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Kann Teamnachrichten versenden]]></item> <item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Kann Teamnachrichten versenden]]></item>
<item name="wcf.acp.group.option.user.chat.canAttach"><![CDATA[Kann Dateianhänge hochladen]]></item>
<item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Kann Chaträume sehen]]></item> <item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Kann Chaträume sehen]]></item>
<item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Kann das Protokoll sehen]]></item> <item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Kann das Protokoll sehen]]></item>
<item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Kann den Benutzernamen färben]]></item> <item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Kann den Benutzernamen färben]]></item>

View File

@ -198,6 +198,7 @@
<item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Can mute]]></item> <item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Can mute]]></item>
<item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Note: If this permission is granted it cannot be revoked in the room specific permissions.]]></item> <item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Note: If this permission is granted it cannot be revoked in the room specific permissions.]]></item>
<item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Can use team internal messages]]></item> <item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Can use team internal messages]]></item>
<item name="wcf.acp.group.option.user.chat.canAttach"><![CDATA[Can upload attachments]]></item>
<item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Can see chat rooms]]></item> <item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Can see chat rooms]]></item>
<item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Can see chat log]]></item> <item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Can see chat log]]></item>
<item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Can choose to color their name]]></item> <item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Can choose to color their name]]></item>

View File

@ -132,6 +132,12 @@
<definitionname>be.bastelstu.chat.messageType</definitionname> <definitionname>be.bastelstu.chat.messageType</definitionname>
<classname>chat\system\message\type\WhisperMessageType</classname> <classname>chat\system\message\type\WhisperMessageType</classname>
</type> </type>
<type>
<name>be.bastelstu.chat.messageType.attachment</name>
<definitionname>be.bastelstu.chat.messageType</definitionname>
<classname>chat\system\message\type\AttachmentMessageType</classname>
</type>
<!-- /message types --> <!-- /message types -->
<!-- suspensions --> <!-- suspensions -->
@ -168,5 +174,13 @@
<points>1</points> <points>1</points>
</type> </type>
<!-- /activity points --> <!-- /activity points -->
<!-- attachments -->
<type>
<name>be.bastelstu.chat.message</name>
<definitionname>com.woltlab.wcf.attachment.objectType</definitionname>
<classname>chat\system\attachment\MessageAttachmentObjectType</classname>
</type>
<!-- attachments -->
</import> </import>
</data> </data>

View File

@ -0,0 +1,19 @@
<div id="chatAttachmentUploadDialog" class="jsStaticDialogContent" data-title="{lang}wcf.attachment.attachments{/lang}">
<div class="attachmentPreview"></div>
{* placeholder for the upload button *}
<div class="upload"></div>
<small>{lang}wcf.attachment.upload.limits{/lang}</small>
</div>
<script data-relocate="true">
require([ 'Language' ], function (Language) {
Language.addObject({
'wcf.attachment.upload.error.invalidExtension': '{lang}wcf.attachment.upload.error.invalidExtension{/lang}',
'wcf.attachment.upload.error.tooLarge': '{lang}wcf.attachment.upload.error.tooLarge{/lang}',
'wcf.attachment.upload.error.reachedLimit': '{lang}wcf.attachment.upload.error.reachedLimit{/lang}',
'wcf.attachment.upload.error.reachedRemainingLimit': '{lang}wcf.attachment.upload.error.reachedRemainingLimit{/lang}',
'wcf.attachment.upload.error.uploadFailed': '{lang}wcf.attachment.upload.error.uploadFailed{/lang}',
'wcf.attachment.upload.error.uploadPhpLimit': '{lang}wcf.attachment.upload.error.uploadPhpLimit{/lang}',
})
})
</script>

View File

@ -433,6 +433,37 @@
</div> </div>
</script> </script>
<script type="x-text/template" data-application="be.bastelstu.chat" data-template-name="be-bastelstu-chat-messageType-attachment" data-template-includes="DeleteButton">
<div class="chatMessageContainer">
<div class="chatMessageSide">
<div class="chatUserAvatar jsUserActionDropdown" data-user-id="{$author.userID}">
<a href="{$author.link}">{@$author.image32}</a>
</div>
<time><a href="{$message.link}">{$message.formattedTime}</a></time>
</div>
<div class="chatMessageContent">
<div class="chatMessageHeader">
<span class="username">
<a href="{$author.link}" class="jsUserActionDropdown" data-user-id="{$author.userID}">
{@$author.coloredUsername}
</a>
</span>
<small class="separatorLeft">
<time><a href="{$message.link}">{$message.formattedTime}</a></time>
</small>
</div>
<div class="chatMessage htmlContent">{@$message.payload.formattedMessage}</div>
</div>
<ul class="buttonGroup buttonList smallButtons">
{/literal}
{if $__wcf->session->getPermission('mod.chat.canDelete')}
{ldelim}include file=$t.DeleteButton}
{/if}
{literal}
</ul>
</div>
</script>
<script type="x-text/template" data-application="be.bastelstu.chat" data-template-name="be-bastelstu-chat-messageType-color"> <script type="x-text/template" data-application="be.bastelstu.chat" data-template-name="be-bastelstu-chat-messageType-color">
<div class="chatMessageContainer inline"> <div class="chatMessageContainer inline">
<div class="chatMessageSide"> <div class="chatMessageSide">

View File

@ -54,10 +54,20 @@
<div id="chatInputContainer"> <div id="chatInputContainer">
<div> <div>
<textarea maxlength="{CHAT_MAX_LENGTH}" class="long"></textarea> {if $__wcf->getSession()->getPermission('user.chat.canAttach')}
<span id="chatQuickSettings"> <div class="chatAttachButton">
<span class="icon icon24 fa-ellipsis-v"></span> <span id="chatAttachmentUploadButton" class="button small" title="{lang}wcf.attachment.attachments{/lang}">
</span> <span class="icon icon16 fa-paperclip"></span>
<span class="icon icon24 fa-paperclip"></span>
</span>
</div>
{/if}
<div class="chatInputWrapper">
<textarea maxlength="{CHAT_MAX_LENGTH}" class="long"></textarea>
<span id="chatQuickSettings">
<span class="icon icon24 fa-ellipsis-v"></span>
</span>
</div>
</div> </div>
<small class="innerError" style="display: none"></small> <small class="innerError" style="display: none"></small>
<span class="charCounter dimmed"></span> <span class="charCounter dimmed"></span>
@ -71,6 +81,7 @@
{include file='messageTypes' application='chat'} {include file='messageTypes' application='chat'}
{include file='userList' application='chat'} {include file='userList' application='chat'}
{include file='userListDropdownMenuItems' application='chat'} {include file='userListDropdownMenuItems' application='chat'}
{include file='__attachmentDialog' application='chat'}
{if !ENABLE_DEBUG_MODE}{js application='wcf' file='Bastelstu.be.Chat'}{/if} {if !ENABLE_DEBUG_MODE}{js application='wcf' file='Bastelstu.be.Chat'}{/if}
<script data-relocate="true"> <script data-relocate="true">

View File

@ -49,6 +49,12 @@
<admindefaultvalue>1</admindefaultvalue> <admindefaultvalue>1</admindefaultvalue>
<usersonly>1</usersonly> <usersonly>1</usersonly>
</option> </option>
<option name="user.chat.canAttach">
<categoryname>user.chat</categoryname>
<optiontype>boolean</optiontype>
<defaultvalue>1</defaultvalue>
<usersonly>1</usersonly>
</option>
<option name="user.chat.disallowedBBCodes"> <option name="user.chat.disallowedBBCodes">
<categoryname>user.chat</categoryname> <categoryname>user.chat</categoryname>
<optiontype>BBCodeSelect</optiontype> <optiontype>BBCodeSelect</optiontype>