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
/*
* 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
* 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
* 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\room\RoomCache;
use \wcf\data\object\type\ObjectTypeCache;
use \wcf\system\attachment\AttachmentHandler;
use \wcf\system\exception\PermissionDeniedException;
use \wcf\system\exception\UserInputException;
use \wcf\system\user\activity\point\UserActivityPointHandler;
@ -304,4 +305,66 @@ class MessageAction extends \wcf\data\AbstractDatabaseObjectAction {
$processor->validate($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
/*
* 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
* 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
* License, use of this software will be governed by version 2
@ -14,6 +14,9 @@
namespace chat\data\message;
use \wcf\system\attachment\AttachmentHandler;
use \wcf\system\WCF;
/**
* Represents a chat message editor.
*/
@ -22,4 +25,20 @@ class MessageEditor extends \wcf\data\DatabaseObjectEditor {
* @inheritDoc
*/
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 {
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
]);
}
}

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 {
margin-left: 20px;
width: auto !important;
}
@ -378,15 +379,46 @@ $chatEmbedMaxWidth: 400px;
> div {
display: flex;
align-items: center;
> .flexibleTextarea {
flex: 1 0 auto;
max-width: 100%;
> div.chatAttachButton {
flex-grow: 0;
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 {
flex: 0 0 auto;
> div.chatInputWrapper {
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
* 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
* License, use of this software will be governed by version 2
@ -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) => {
@ -176,8 +178,11 @@ define([ './Chat/console'
this.ui = this.bottle.container.Ui
this.ui.bootstrap()
this.bottle.container.UiInput.on('submit', this.onSubmit.bind(this))
this.bottle.container.UiInput.on('autocomplete', this.onAutocomplete.bind(this))
this.ui.input.on('submit', this.onSubmit.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()
@ -328,6 +333,10 @@ define([ './Chat/console'
}
}
async onSendAttachment(event) {
return this.bottle.container.Messenger.pushAttachment(event.detail.tmpHash)
}
onAutocomplete(event) {
const input = event.target
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
* 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
* License, use of this software will be governed by version 2
@ -56,6 +56,14 @@ define([ './console'
return Ajax.api(this, payload)
}
async pushAttachment(tmpHash) {
const payload = { actionName: 'pushAttachment'
, parameters: { tmpHash }
}
return Ajax.api(this, payload)
}
_ajaxSetup() {
return { silent: 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) {
"use strict";
const DEPENDENCIES = [ 'UiAutoAway'
const DEPENDENCIES = [ 'UiAttachmentUpload'
, 'UiAutoAway'
, 'UiConnectionWarning'
, 'UiInput'
, 'UiInputAutocompleter'
, 'UiMessageStream'
, 'UiMessageActionDelete'
, 'UiMessageStream'
, 'UiMobile'
, 'UiNotification'
, 'UiReadMarker'
@ -29,16 +30,17 @@ 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, messageActionDelete, messageStream, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) {
super()
this.actionDropdownHandler = userActionDropdownHandler
this.attachmentUpload = attachmentUpload
this.autoAway = autoAway
this.autocompleter = autocompleter
this.connectionWarning = connectionWarning
this.input = input
this.messageStream = messageStream
this.messageActionDelete = messageActionDelete
this.messageStream = messageStream
this.mobile = mobile
this.notification = notification
this.readMarker = readMarker
@ -49,12 +51,13 @@ define([ '../Ui' ], function (Ui) {
bootstrap() {
this.actionDropdownHandler.bootstrap()
this.attachmentUpload.bootstrap()
this.autoAway.bootstrap()
this.autocompleter.bootstrap()
this.connectionWarning.bootstrap()
this.input.bootstrap()
this.messageStream.bootstrap()
this.messageActionDelete.bootstrap()
this.messageStream.bootstrap()
this.mobile.bootstrap()
this.notification.bootstrap()
this.readMarker.bootstrap()

View File

@ -63,7 +63,7 @@ define([ './ToggleButton'
*/
setupMobile() {
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.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.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.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.canSeeLog"><![CDATA[Kann das Protokoll sehen]]></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.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.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.canSeeLog"><![CDATA[Can see chat log]]></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>
<classname>chat\system\message\type\WhisperMessageType</classname>
</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 -->
<!-- suspensions -->
@ -168,5 +174,13 @@
<points>1</points>
</type>
<!-- /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>
</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>
</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">
<div class="chatMessageContainer inline">
<div class="chatMessageSide">

View File

@ -54,10 +54,20 @@
<div id="chatInputContainer">
<div>
<textarea maxlength="{CHAT_MAX_LENGTH}" class="long"></textarea>
<span id="chatQuickSettings">
<span class="icon icon24 fa-ellipsis-v"></span>
</span>
{if $__wcf->getSession()->getPermission('user.chat.canAttach')}
<div class="chatAttachButton">
<span id="chatAttachmentUploadButton" class="button small" title="{lang}wcf.attachment.attachments{/lang}">
<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>
<small class="innerError" style="display: none"></small>
<span class="charCounter dimmed"></span>
@ -71,6 +81,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}
<script data-relocate="true">

View File

@ -49,6 +49,12 @@
<admindefaultvalue>1</admindefaultvalue>
<usersonly>1</usersonly>
</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">
<categoryname>user.chat</categoryname>
<optiontype>BBCodeSelect</optiontype>