diff --git a/clipboardAction.xml b/clipboardAction.xml index 17e53aec..87d7192f 100644 --- a/clipboardAction.xml +++ b/clipboardAction.xml @@ -1,6 +1,6 @@ - + wcf\system\clipboard\action\ConversationClipboardAction @@ -51,5 +51,5 @@ wcf\page\ConversationListPage - + diff --git a/fileDelete.xml b/fileDelete.xml index 7255da18..0577d1b5 100644 --- a/fileDelete.xml +++ b/fileDelete.xml @@ -22,5 +22,22 @@ lib/system/event/listener/UserGroupAddCanBeAddedAsParticipantListener.class.php lib/system/user/online/location/ConversationLocation.class.php style/conversation.less + lib/system/clipboard/action/ConversationClipboardAction.class.php + js/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.js + js/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.js + js/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.js + js/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.js + js/WoltLabSuite/Core/Api/Conversations/LeaveConversation.js + js/WoltLabSuite/Core/Conversation/Component/EditorHandler.js + js/WoltLabSuite/Core/Conversation/Component/Leave.js + js/WoltLabSuite/Core/Conversation/Component/Label/Editor.js + js/WoltLabSuite/Core/Conversation/Component/Label/Manager.js + js/WoltLabSuite/Core/Conversation/Component/Subject/Editor.js + js/WoltLabSuite/Core/Conversation/Clipboard.js + js/WoltLabSuite/Core/Conversation/Ui/Participant/Add.js + js/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.js + js/WCF.Conversation.js + js/WCF.Conversation.min.js + js/WCF.Conversation.tiny.min.js diff --git a/files/js/WCF.Conversation.js b/files/js/WCF.Conversation.js deleted file mode 100644 index 4dd0a89e..00000000 --- a/files/js/WCF.Conversation.js +++ /dev/null @@ -1,982 +0,0 @@ -/** - * Namespace for conversations. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - */ -WCF.Conversation = { }; - -/** - * Core editor handler for conversations. - * - * @deprecated 6.2 use `WoltLabSuite/Core/Conversation/EditorHandler` instead - */ -WCF.Conversation.EditorHandler = Class.extend({ - /** - * Initializes the core editor handler for conversations. - */ - init: function (availableLabels) { - require(["WoltLabSuite/Core/Conversation/Component/EditorHandler"], ({ setup }) => { - setup(availableLabels); - }); - }, - - /** - * Returns a permission's value for given conversation id. - * - * @param integer conversationID - * @param string permission - * @return boolean - */ - getPermission: function (conversationID, permission) { - switch (permission) { - case "canAddParticipants": - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getConversationEditor( - conversationID, - ).canAddParticipants; - case "canCloseConversation": - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getConversationEditor( - conversationID, - ).canCloseConversation; - default: - return false; - } - }, - - /** - * Returns an attribute's value for given conversation id. - * - * @param integer conversationID - * @param string key - * @return mixed - */ - getValue: function (conversationID, key) { - switch (key) { - case "labelIDs": - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getConversationEditor( - conversationID, - ).labelIDs; - break; - - case "isClosed": - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getConversationEditor( - conversationID, - ).isClosed; - } - }, - - /** - * Counts available labels. - * - * @return integer - */ - countAvailableLabels: function () { - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getAvailableLabels().length; - }, - - /** - * Returns a list with the data of the available labels. - * - * @return array - */ - getAvailableLabels: function () { - return require("WoltLabSuite/Core/Conversation/Component/EditorHandler").getAvailableLabels(); - }, - - /** - * Updates conversation data. - * - * @param integer conversationID - * @param object data - */ - update: function (conversationID, key, data) { - require(["WoltLabSuite/Core/Conversation/Component/EditorHandler"], ({ getConversationEditor }) => { - switch (key) { - case "close": - getConversationEditor(conversationID).isClosed = true; - break; - case "labelIDs": - getConversationEditor(conversationID).labelIDs = data; - break; - case "open": - getConversationEditor(conversationID).isClosed = false; - break; - } - }); - }, -}); - -/** - * Conversation editor handler for conversation page. - * - * @see WCF.Conversation.EditorHandler - * @param array availableLabels - * - * @deprecated 6.2 use `WoltLabSuite/Core/Conversation/EditorHandler` instead - */ -WCF.Conversation.EditorHandlerConversation = WCF.Conversation.EditorHandler.extend({ - /** - * @see WCF.Conversation.EditorHandler.init() - * - * @param array availableLabels - */ - init: function(availableLabels) { - this._super(availableLabels); - }, -}); - -/** - * Provides extended actions for conversation clipboard actions. - * - * @deprecated 6.2 use `WoltLabSuite/Core/Conversation/Clipboard` instead - */ -WCF.Conversation.Clipboard = Class.extend({ - /** - * editor handler - * @var WCF.Conversation.EditorHandler - */ - _editorHandler: null, - - /** - * Initializes a new WCF.Conversation.Clipboard object. - * - * @param {WCF.Conversation.EditorHandler} editorHandler - */ - init: function(editorHandler) { - this._editorHandler = editorHandler; - - WCF.System.Event.addListener('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.conversation.conversation', (function (data) { - if (data.responseData === null) { - this._execute(data.data.actionName, data.data.parameters); - } - else { - this._evaluateResponse(data.data.actionName, data.responseData); - } - }).bind(this)); - }, - - /** - * Handles clipboard actions. - * - * @param {string} actionName - * @param {Object} parameters - */ - _execute: function(actionName, parameters) { - if (actionName === 'com.woltlab.wcf.conversation.conversation.assignLabel') { - new WCF.Conversation.Label.Editor(this._editorHandler, null, parameters.objectIDs); - } - }, - - /** - * Evaluates AJAX responses. - * - * @param {Object} data - * @param {string} actionName - */ - _evaluateResponse: function(actionName, data) { - switch (actionName) { - case 'com.woltlab.wcf.conversation.conversation.leave': - case 'com.woltlab.wcf.conversation.conversation.leavePermanently': - case 'com.woltlab.wcf.conversation.conversation.markAsRead': - case 'com.woltlab.wcf.conversation.conversation.restore': - window.location.reload(); - break; - - case 'com.woltlab.wcf.conversation.conversation.close': - case 'com.woltlab.wcf.conversation.conversation.open': - //noinspection JSUnresolvedVariable - for (var conversationId in data.returnValues.conversationData) { - //noinspection JSUnresolvedVariable - if (data.returnValues.conversationData.hasOwnProperty(conversationId)) { - //noinspection JSUnresolvedVariable - var $data = data.returnValues.conversationData[conversationId]; - - this._editorHandler.update(conversationId, ($data.isClosed ? 'close' : 'open'), $data); - } - } - break; - } - } -}); - -/** - * Inline editor implementation for conversations. - * - * @see WCF.Inline.Editor - */ -WCF.Conversation.InlineEditor = WCF.InlineEditor.extend({ - /** - * editor handler object - * @var WCF.Conversation.EditorHandler - */ - _editorHandler: null, - - /** - * execution environment - * @var string - */ - _environment: 'conversation', - - /** - * @see WCF.InlineEditor._setOptions() - */ - _setOptions: function() { - this._options = [ - // edit title - { label: WCF.Language.get('wcf.conversation.edit.subject'), optionName: 'editSubject' }, - - // isClosed - { label: WCF.Language.get('wcf.conversation.edit.close'), optionName: 'close' }, - { label: WCF.Language.get('wcf.conversation.edit.open'), optionName: 'open' }, - - // assign labels - { label: WCF.Language.get('wcf.conversation.edit.assignLabel'), optionName: 'assignLabel' }, - - // divider - { optionName: 'divider' }, - - // add participants - { label: WCF.Language.get('wcf.conversation.edit.addParticipants'), optionName: 'addParticipants' }, - - // leave conversation - { label: WCF.Language.get('wcf.conversation.edit.leave'), optionName: 'leave' }, - - // edit draft - { label: WCF.Language.get('wcf.global.button.edit'), optionName: 'edit', isQuickOption: true } - ]; - }, - - /** - * Sets editor handler object. - * - * @param WCF.Conversation.EditorHandler editorHandler - * @param string environment - */ - setEditorHandler: function(editorHandler, environment) { - this._editorHandler = editorHandler; - this._environment = (environment == 'list') ? 'list' : 'conversation'; - }, - - /** - * @see WCF.InlineEditor._getTriggerElement() - */ - _getTriggerElement: function(element) { - return element.find('.jsConversationInlineEditor'); - }, - - /** - * @see WCF.InlineEditor._validate() - */ - _validate: function(elementID, optionName) { - var $conversationID = $('#' + elementID).data('conversationID'); - - switch (optionName) { - case 'addParticipants': - return (this._editorHandler.getPermission($conversationID, 'canAddParticipants')); - break; - - case 'assignLabel': - return (this._editorHandler.countAvailableLabels()) ? true : false; - break; - - case 'editSubject': - return (!!this._editorHandler.getPermission($conversationID, 'canCloseConversation')); - break; - - case 'close': - case 'open': - if (!this._editorHandler.getPermission($conversationID, 'canCloseConversation')) { - return false; - } - - if (optionName === 'close') return !(this._editorHandler.getValue($conversationID, 'isClosed')); - else return (this._editorHandler.getValue($conversationID, 'isClosed')); - break; - - case 'leave': - return true; - break; - - case 'edit': - return ($('#' + elementID).data('isDraft') ? true : false); - break; - } - - return false; - }, - - /** - * @see WCF.InlineEditor._execute() - */ - _execute: function(elementID, optionName) { - // abort if option is invalid or not accessible - if (!this._validate(elementID, optionName)) { - return false; - } - - switch (optionName) { - case 'addParticipants': - require(['WoltLabSuite/Core/Conversation/Ui/Participant/Add'], function(UiParticipantAdd) { - new UiParticipantAdd(elData(elById(elementID), 'conversation-id')); - }); - break; - - case 'assignLabel': - new WCF.Conversation.Label.Editor(this._editorHandler, elementID); - break; - - case 'editSubject': - require(['WoltLabSuite/Core/Conversation/Ui/Subject/Editor'], function (UiSubjectEditor) { - UiSubjectEditor.beginEdit(elData(elById(elementID), 'conversation-id')); - }); - break; - - case 'close': - case 'open': - this._updateConversation(elementID, optionName, { isClosed: (optionName === 'close' ? 1 : 0) }); - break; - - case 'leave': - require(["WoltLabSuite/Core/Conversation/Component/Leave"], ({ openDialog }) => { - openDialog(elData(elById(elementID), "conversation-id"), this._environment); - }); - break; - - case 'edit': - window.location = this._getTriggerElement($('#' + elementID)).prop('href'); - break; - } - }, - - /** - * Updates conversation properties. - * - * @param string elementID - * @param string optionName - * @param object data - */ - _updateConversation: function(elementID, optionName, data) { - var $conversationID = this._elements[elementID].data('conversationID'); - - switch (optionName) { - case 'close': - case 'editSubject': - case 'open': - this._updateData.push({ - elementID: elementID, - optionName: optionName, - data: data - }); - - this._proxy.setOption('data', { - actionName: optionName, - className: 'wcf\\data\\conversation\\ConversationAction', - objectIDs: [ $conversationID ] - }); - this._proxy.sendRequest(); - break; - } - }, - - /** - * @see WCF.InlineEditor._updateState() - */ - _updateState: function() { - for (var $i = 0, $length = this._updateData.length; $i < $length; $i++) { - var $data = this._updateData[$i]; - var $conversationID = this._elements[$data.elementID].data('conversationID'); - - switch ($data.optionName) { - case 'close': - case 'editSubject': - case 'open': - this._editorHandler.update($conversationID, $data.optionName, $data.data); - break; - } - } - } -}); - -/** - * Provides a dialog for leaving or restoring conversation. - * - * @param array conversationIDs - * - * @deprecated 6.2 use `WoltLabSuite/Core/Conversation/Component/Leave` instead - */ -WCF.Conversation.Leave = Class.extend({ - /** - * list of conversation ids - * @var array - */ - _conversationIDs: [ ], - - /** - * dialog overlay - * @var jQuery - */ - _dialog: null, - - /** - * environment name - * @var string - */ - _environment: '', - - /** - * action proxy - * @var WCF.Action.Proxy - */ - _proxy: null, - - /** - * Initializes the leave/restore dialog for conversations. - * - * @param array conversationIDs - * @param string environment - */ - init: function(conversationIDs, environment) { - this._conversationIDs = conversationIDs; - this._dialog = null; - this._environment = environment; - - this._proxy = new WCF.Action.Proxy({ - success: $.proxy(this._success, this) - }); - - this._loadDialog(); - }, - - /** - * Loads the dialog overlay. - */ - _loadDialog: function() { - this._proxy.setOption('data', { - actionName: 'getLeaveForm', - className: 'wcf\\data\\conversation\\ConversationAction', - objectIDs: this._conversationIDs - }); - this._proxy.sendRequest(); - }, - - /** - * Handles successful AJAX requests. - * - * @param object data - * @param string textStatus - * @param jQuery jqXHR - */ - _success: function(data, textStatus, jqXHR) { - switch (data.returnValues.actionName) { - case 'getLeaveForm': - this._showDialog(data); - break; - - case 'hideConversation': - if (this._environment === 'conversation') { - window.location = data.returnValues.redirectURL; - } - else { - window.location.reload(); - } - break; - } - }, - - /** - * Displays the leave/restore conversation dialog overlay. - * - * @param object data - */ - _showDialog: function(data) { - if (this._dialog === null) { - this._dialog = $('#leaveDialog'); - if (!this._dialog.length) { - this._dialog = $('
').hide().appendTo(document.body); - } - } - - // render dialog - this._dialog.html(data.returnValues.template); - this._dialog.wcfDialog({ - title: WCF.Language.get('wcf.conversation.leave.title') - }); - - this._dialog.wcfDialog('render'); - - // bind event listener - this._dialog.find('#hideConversation').click($.proxy(this._click, this)); - }, - - /** - * Handles conversation state changes. - */ - _click: function() { - var $input = this._dialog.find('input[type=radio]:checked'); - if ($input.length === 1) { - this._proxy.setOption('data', { - actionName: 'hideConversation', - className: 'wcf\\data\\conversation\\ConversationAction', - objectIDs: this._conversationIDs, - parameters: { - hideConversation: $input.val() - } - }); - this._proxy.sendRequest(); - } - } -}); - -/** - * Namespace for label-related classes. - */ -WCF.Conversation.Label = { }; - -/** - * Providers an editor for conversation labels. - * - * @param WCF.Conversation.EditorHandler editorHandler - * @param string elementID - * @param array conversationIDs - * - * @deprecated 6.2 Use `WoltLabSuite/Core/Conversation/Component/Label/Editor` instead - */ -WCF.Conversation.Label.Editor = Class.extend({ - /** - * list of conversation id - * @var array - */ - _conversationIDs: 0, - - /** - * dialog object - * @var jQuery - */ - _dialog: null, - - /** - * editor handler object - * @var WCF.Conversation.EditorHandler - */ - _editorHandler: null, - - /** - * system notification object - * @var WCF.System.Notification - */ - _notification: null, - - /** - * action proxy object - * @var WCF.Action.Proxy - */ - _proxy: null, - - /** - * Initializes the label editor for given conversation. - * - * @param WCF.Conversation.EditorHandler editorHandler - * @param string elementID - * @param array conversationIDs - */ - init: function(editorHandler, elementID, conversationIDs) { - if (elementID) { - this._conversationIDs = [ $('#' + elementID).data('conversationID') ]; - } - else { - this._conversationIDs = conversationIDs; - } - - this._dialog = null; - this._editorHandler = editorHandler; - - this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success.edit')); - this._proxy = new WCF.Action.Proxy({ - success: $.proxy(this._success, this) - }); - - this._loadDialog(); - }, - - /** - * Loads label assignment dialog. - */ - _loadDialog: function() { - this._proxy.setOption('data', { - actionName: 'getLabelForm', - className: 'wcf\\data\\conversation\\label\\ConversationLabelAction', - parameters: { - conversationIDs: this._conversationIDs - } - }); - this._proxy.sendRequest(); - }, - - /** - * Handles successful AJAX requests. - * - * @param object data - * @param string textStatus - * @param jQuery jqXHR - */ - _success: function(data, textStatus, jqXHR) { - switch (data.returnValues.actionName) { - case 'assignLabel': - this._assignLabels(data); - break; - - case 'getLabelForm': - this._renderDialog(data); - break; - } - }, - - /** - * Renders the label assignment form overlay. - * - * @param object data - */ - _renderDialog: function(data) { - if (this._dialog === null) { - this._dialog = $('#conversationLabelForm'); - if (!this._dialog.length) { - this._dialog = $('
').hide().appendTo(document.body); - } - } - - this._dialog.html(data.returnValues.template); - this._dialog.wcfDialog({ - title: WCF.Language.get('wcf.conversation.label.assignLabels') - }); - this._dialog.wcfDialog('render'); - - $('#assignLabels').click($.proxy(this._save, this)); - }, - - /** - * Saves label assignments for current conversation id. - */ - _save: function() { - var $labelIDs = [ ]; - this._dialog.find('input').each(function(index, checkbox) { - var $checkbox = $(checkbox); - if ($checkbox.is(':checked')) { - $labelIDs.push($checkbox.data('labelID')); - } - }); - - this._proxy.setOption('data', { - actionName: 'assignLabel', - className: 'wcf\\data\\conversation\\label\\ConversationLabelAction', - parameters: { - conversationIDs: this._conversationIDs, - labelIDs: $labelIDs - } - }); - this._proxy.sendRequest(); - }, - - /** - * Updates conversation labels. - * - * @param object data - */ - _assignLabels: function(data) { - // update conversation - for (var $i = 0, $length = this._conversationIDs.length; $i < $length; $i++) { - this._editorHandler.update(this._conversationIDs[$i], 'labelIDs', data.returnValues.labelIDs); - } - - // close dialog and show a 'success' notice - this._dialog.wcfDialog('close'); - this._notification.show(); - } -}); - -/** - * Label manager for conversations. - * - * @param string link - * - * @deprecated 6.2 use `WoltLabSuite/Core/Conversation/Component/Label/Manager` instead - */ -WCF.Conversation.Label.Manager = Class.extend({ - /** - * deleted label id - * @var integer - */ - _deletedLabelID: 0, - - /** - * dialog object - * @var jQuery - */ - _dialog: null, - - /** - * list of labels - * @var jQuery - */ - _labels: null, - - /** - * parsed label link - * @var string - */ - _link: '', - - /** - * system notification object - * @var WCF.System.Notification - */ - _notification: '', - - /** - * action proxy object - * @var WCF.Action.Proxy - */ - _proxy: null, - - /** - * maximum number of labels the user may create - * @var integer - */ - _maxLabels: 0, - - /** - * number of labels the user created - * @var integer - */ - _labelCount: 0, - - /** - * Initializes the label manager for conversations. - * - * @param string link - */ - init: function(link) { - this._deletedLabelID = 0; - this._maxLabels = 0; - this._labelCount = 0; - this._link = link; - - this._labels = WCF.Dropdown.getDropdownMenu('conversationLabelFilter').children('.scrollableDropdownMenu'); - $('#manageLabel').click($.proxy(this._click, this)); - - this._notification = new WCF.System.Notification(WCF.Language.get('wcf.conversation.label.management.addLabel.success')); - this._proxy = new WCF.Action.Proxy({ - success: $.proxy(this._success, this) - }); - }, - - /** - * Handles clicks on the 'manage labels' button. - */ - _click: function() { - this._proxy.setOption('data', { - actionName: 'getLabelManagement', - className: 'wcf\\data\\conversation\\ConversationAction' - }); - this._proxy.sendRequest(); - }, - - /** - * Handles successful AJAX requests. - * - * @param object data - * @param string textStatus - * @param jQuery jqXHR - */ - _success: function(data, textStatus, jqXHR) { - if (this._dialog === null) { - this._dialog = $('
').hide().appendTo(document.body); - } - - if (data.returnValues && data.returnValues.actionName) { - switch (data.returnValues.actionName) { - case 'add': - this._insertLabel(data); - break; - - case 'getLabelManagement': - this._maxLabels = parseInt(data.returnValues.maxLabels); - this._labelCount = parseInt(data.returnValues.labelCount); - - // render dialog - this._dialog.empty().html(data.returnValues.template); - this._dialog.wcfDialog({ - title: WCF.Language.get('wcf.conversation.label.management') - }); - this._dialog.wcfDialog('render'); - - // bind action listeners - this._bindListener(); - break; - } - } - else { - // check if delete label id is present within URL (causing an IllegalLinkException if reloading) - if (this._deletedLabelID) { - var $regex = new RegExp('(\\?|&)labelID=' + this._deletedLabelID); - window.location = window.location.toString().replace($regex, ''); - } - else { - // reload page - window.location.reload(); - } - } - }, - - /** - * Inserts a previously created label. - * - * @param object data - */ - _insertLabel: function(data) { - var $listItem = $('
  • ' + data.returnValues.label + '
  • '); - $listItem.find('a > span').data('labelID', data.returnValues.labelID).data('cssClassName', data.returnValues.cssClassName); - - this._labels.append($listItem); - - this._notification.show(); - - this._labelCount++; - - if (this._labelCount >= this._maxLabels) { - $('#conversationLabelManagementForm').hide(); - } - }, - - /** - * Binds event listener for label management. - */ - _bindListener: function() { - $('#labelName').on('keyup keydown keypress', $.proxy(this._updateLabels, this)); - if ($.browser.mozilla && $.browser.touch) { - $('#labelName').on('input', $.proxy(this._updateLabels, this)); - } - - if (this._labelCount >= this._maxLabels) { - $('#conversationLabelManagementForm').hide(); - this._dialog.wcfDialog('render'); - } - - $('#addLabel').disable().click($.proxy(this._addLabel, this)); - $('#editLabel').disable(); - - this._dialog.find('.conversationLabelList a.label').click($.proxy(this._edit, this)); - }, - - /** - * Prepares a label for editing. - * - * @param object event - */ - _edit: function(event) { - if (this._labelCount >= this._maxLabels) { - $('#conversationLabelManagementForm').show(); - this._dialog.wcfDialog('render'); - } - - var $label = $(event.currentTarget); - - // replace legends - var $legend = WCF.Language.get('wcf.conversation.label.management.editLabel').replace(/#labelName#/, WCF.String.escapeHTML($label.text())); - $('#conversationLabelManagementForm').data('labelID', $label.data('labelID')).children('.sectionTitle').html($legend); - - // update text input - $('#labelName').val($label.text()).trigger('keyup'); - - // select css class name - var $cssClassName = $label.data('cssClassName'); - $('#labelManagementList input').each(function(index, input) { - var $input = $(input); - - if ($input.val() == $cssClassName) { - $input.attr('checked', 'checked'); - } - }); - - // toggle buttons - $('#addLabel').hide(); - $('#editLabel').show().click($.proxy(this._editLabel, this)); - $('#deleteLabel').show().click($.proxy(this._deleteLabel, this)); - }, - - /** - * Edits a label. - */ - _editLabel: function() { - this._proxy.setOption('data', { - actionName: 'update', - className: 'wcf\\data\\conversation\\label\\ConversationLabelAction', - objectIDs: [ $('#conversationLabelManagementForm').data('labelID') ], - parameters: { - data: { - cssClassName: $('#labelManagementList input:checked').val(), - label: $('#labelName').val() - } - } - }); - this._proxy.sendRequest(); - }, - - /** - * Deletes a label. - */ - _deleteLabel: function() { - var $title = WCF.Language.get('wcf.conversation.label.management.deleteLabel.confirmMessage').replace(/#labelName#/, $('#labelName').val()); - WCF.System.Confirmation.show($title, $.proxy(function(action) { - if (action === 'confirm') { - this._proxy.setOption('data', { - actionName: 'delete', - className: 'wcf\\data\\conversation\\label\\ConversationLabelAction', - objectIDs: [ $('#conversationLabelManagementForm').data('labelID') ] - }); - this._proxy.sendRequest(); - - this._deletedLabelID = $('#conversationLabelManagementForm').data('labelID'); - } - }, this), undefined, undefined, true); - }, - - /** - * Updates label text within label management. - */ - _updateLabels: function() { - var $value = $.trim($('#labelName').val()); - if ($value) { - $('#addLabel, #editLabel').enable(); - } - else { - $('#addLabel, #editLabel').disable(); - $value = WCF.Language.get('wcf.conversation.label.placeholder'); - } - - $('#labelManagementList').find('span.label').text($value); - }, - - /** - * Sends an AJAX request to add a new label. - */ - _addLabel: function() { - var $labelName = $('#labelName').val(); - var $cssClassName = $('#labelManagementList input:checked').val(); - - this._proxy.setOption('data', { - actionName: 'add', - className: 'wcf\\data\\conversation\\label\\ConversationLabelAction', - parameters: { - data: { - cssClassName: $cssClassName, - labelName: $labelName - } - } - }); - this._proxy.sendRequest(); - - // close dialog - this._dialog.wcfDialog('close'); - } -}); diff --git a/files/js/WCF.Conversation.min.js b/files/js/WCF.Conversation.min.js deleted file mode 100755 index 7c3254f0..00000000 --- a/files/js/WCF.Conversation.min.js +++ /dev/null @@ -1 +0,0 @@ -WCF.Conversation={},WCF.Conversation.EditorHandler=Class.extend({_attributes:{},_conversations:{},_permissions:{},init:function(availableLabels){this._conversations={};var self=this;$(".conversation").each((function(index,conversation){var $labelIDs,$conversation=$(conversation),$conversationID=$conversation.data("conversationID");self._conversations[$conversationID]||(self._conversations[$conversationID]=$conversation,$labelIDs=eval($conversation.data("labelIDs")),self._attributes[$conversationID]={isClosed:!!$conversation.data("isClosed"),labelIDs:$labelIDs},self._permissions[$conversationID]={canAddParticipants:!!$conversation.data("canAddParticipants"),canCloseConversation:!!$conversation.data("canCloseConversation")})}))},getPermission:function(e,a){return void 0!==this._permissions[e][a]&&!!this._permissions[e][a]},getValue:function(e,a){switch(a){case"labelIDs":return void 0===this._attributes[e].labelIDs&&(this._attributes[e].labelIDs=[]),this._attributes[e].labelIDs;case"isClosed":return!!this._attributes[e].isClosed}},countAvailableLabels:function(){return this.getAvailableLabels().length},getAvailableLabels:function(){var e=[];return WCF.Dropdown.getDropdownMenu("conversationLabelFilter").children(".scrollableDropdownMenu").children("li").each((function(a,t){var n,s=$(t);if(s.hasClass("dropdownDivider"))return!1;n=s.find("span"),e.push({cssClassName:n.data("cssClassName"),labelID:n.data("labelID"),label:n.text()})})),e},update:function(e,a,t){var n,s,i,o,l,r;if(this._conversations[e]){switch(n=this._conversations[e],a){case"close":$(`
  • \n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
  • `).prependTo(n.find(".statusIcons")),this._attributes[e].isClosed=1;break;case"labelIDs":if(s={},WCF.Dropdown.getDropdownMenu("conversationLabelFilter").find("li > a > span").each((function(e,a){var t=$(a);s[t.data("labelID")]={cssClassName:t.data("cssClassName"),label:t.text(),url:t.parent().attr("href")}})),i=n.find(".columnSubject > .labelList"),t.length)for(i.length||(i=$('
      ').prependTo(n.find(".columnSubject"))),i.empty(),o=0,l=t.length;o'+WCF.String.escapeHTML(r.label)+"").appendTo(i);else i.length&&i.remove();break;case"open":n.find(".statusIcons li").each((function(e,a){var t=$(a);if(t.children("span.jsIconLock").length)return t.remove(),!1})),this._attributes[e].isClosed=0}WCF.Clipboard.reload()}else console.debug("[WCF.Conversation.EditorHandler] Unknown conversation id '"+e+"'")}}),WCF.Conversation.EditorHandlerConversation=WCF.Conversation.EditorHandler.extend({_availableLabels:null,init:function(e){this._availableLabels=e||[],this._super()},getAvailableLabels:function(){return this._availableLabels},update:function(e,a,t){var n,s,i,o;if(this._conversations[e])switch(n=$(".contentHeaderTitle > .contentHeaderMetaData"),a){case"close":$(`
    • \n\t\t\t\t\t\n\t\t\t\t\t${WCF.Language.get("wcf.global.state.closed")}\n\t\t\t\t
    • `).appendTo(n),this._attributes[e].isClosed=1;break;case"labelIDs":s=n.find(".labelList"),t.length?(i=this.getAvailableLabels(),s.length||(s=(s=$('
    • \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
        \n\t\t\t\t\t\t
      • ').prependTo(n)).children("ul")),o="",t.forEach((function(e){i.forEach((function(a){a.labelID==e&&(o+='
      • '+a.label+"
      • ")}))})),s[0].innerHTML=o):s.parent().remove();break;case"open":n.find(".jsIconLock").parent().remove(),this._attributes[e].isClosed=0}else console.debug("[WCF.Conversation.EditorHandler] Unknown conversation id '"+e+"'")}}),WCF.Conversation.Clipboard=Class.extend({_editorHandler:null,init:function(e){this._editorHandler=e,WCF.System.Event.addListener("com.woltlab.wcf.clipboard","com.woltlab.wcf.conversation.conversation",function(e){null===e.responseData?this._execute(e.data.actionName,e.data.parameters):this._evaluateResponse(e.data.actionName,e.responseData)}.bind(this))},_execute:function(e,a){"com.woltlab.wcf.conversation.conversation.assignLabel"===e&&new WCF.Conversation.Label.Editor(this._editorHandler,null,a.objectIDs)},_evaluateResponse:function(e,a){var t,n;switch(e){case"com.woltlab.wcf.conversation.conversation.leave":case"com.woltlab.wcf.conversation.conversation.leavePermanently":case"com.woltlab.wcf.conversation.conversation.markAsRead":case"com.woltlab.wcf.conversation.conversation.restore":window.location.reload();break;case"com.woltlab.wcf.conversation.conversation.close":case"com.woltlab.wcf.conversation.conversation.open":for(t in a.returnValues.conversationData)a.returnValues.conversationData.hasOwnProperty(t)&&(n=a.returnValues.conversationData[t],this._editorHandler.update(t,n.isClosed?"close":"open",n))}}}),WCF.Conversation.InlineEditor=WCF.InlineEditor.extend({_editorHandler:null,_environment:"conversation",_setOptions:function(){this._options=[{label:WCF.Language.get("wcf.conversation.edit.subject"),optionName:"editSubject"},{label:WCF.Language.get("wcf.conversation.edit.close"),optionName:"close"},{label:WCF.Language.get("wcf.conversation.edit.open"),optionName:"open"},{label:WCF.Language.get("wcf.conversation.edit.assignLabel"),optionName:"assignLabel"},{optionName:"divider"},{label:WCF.Language.get("wcf.conversation.edit.addParticipants"),optionName:"addParticipants"},{label:WCF.Language.get("wcf.conversation.edit.leave"),optionName:"leave"},{label:WCF.Language.get("wcf.global.button.edit"),optionName:"edit",isQuickOption:!0}]},setEditorHandler:function(e,a){this._editorHandler=e,this._environment="list"==a?"list":"conversation"},_getTriggerElement:function(e){return e.find(".jsConversationInlineEditor")},_validate:function(e,a){var t=$("#"+e).data("conversationID");switch(a){case"addParticipants":return this._editorHandler.getPermission(t,"canAddParticipants");case"assignLabel":return!!this._editorHandler.countAvailableLabels();case"editSubject":return!!this._editorHandler.getPermission(t,"canCloseConversation");case"close":case"open":return!!this._editorHandler.getPermission(t,"canCloseConversation")&&("close"===a?!this._editorHandler.getValue(t,"isClosed"):this._editorHandler.getValue(t,"isClosed"));case"leave":return!0;case"edit":return!!$("#"+e).data("isDraft")}return!1},_execute:function(e,a){if(!this._validate(e,a))return!1;switch(a){case"addParticipants":require(["WoltLabSuite/Core/Conversation/Ui/Participant/Add"],(function(a){new a(elData(elById(e),"conversation-id"))}));break;case"assignLabel":new WCF.Conversation.Label.Editor(this._editorHandler,e);break;case"editSubject":require(["WoltLabSuite/Core/Conversation/Ui/Subject/Editor"],(function(a){a.beginEdit(elData(elById(e),"conversation-id"))}));break;case"close":case"open":this._updateConversation(e,a,{isClosed:"close"===a?1:0});break;case"leave":new WCF.Conversation.Leave([$("#"+e).data("conversationID")],this._environment);break;case"edit":window.location=this._getTriggerElement($("#"+e)).prop("href")}},_updateConversation:function(e,a,t){var n=this._elements[e].data("conversationID");switch(a){case"close":case"editSubject":case"open":this._updateData.push({elementID:e,optionName:a,data:t}),this._proxy.setOption("data",{actionName:a,className:"wcf\\data\\conversation\\ConversationAction",objectIDs:[n]}),this._proxy.sendRequest()}},_updateState:function(){var e,a,t,n;for(e=0,a=this._updateData.length;e').hide().appendTo(document.body))),this._dialog.html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.leave.title")}),this._dialog.wcfDialog("render"),this._dialog.find("#hideConversation").click($.proxy(this._click,this))},_click:function(){var e=this._dialog.find("input[type=radio]:checked");1===e.length&&(this._proxy.setOption("data",{actionName:"hideConversation",className:"wcf\\data\\conversation\\ConversationAction",objectIDs:this._conversationIDs,parameters:{hideConversation:e.val()}}),this._proxy.sendRequest())}}),WCF.Conversation.Label={},WCF.Conversation.Label.Editor=Class.extend({_conversationIDs:0,_dialog:null,_editorHandler:null,_notification:null,_proxy:null,init:function(e,a,t){this._conversationIDs=a?[$("#"+a).data("conversationID")]:t,this._dialog=null,this._editorHandler=e,this._notification=new WCF.System.Notification(WCF.Language.get("wcf.global.success.edit")),this._proxy=new WCF.Action.Proxy({success:$.proxy(this._success,this)}),this._loadDialog()},_loadDialog:function(){this._proxy.setOption("data",{actionName:"getLabelForm",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{conversationIDs:this._conversationIDs}}),this._proxy.sendRequest()},_success:function(e,a,t){switch(e.returnValues.actionName){case"assignLabel":this._assignLabels(e);break;case"getLabelForm":this._renderDialog(e)}},_renderDialog:function(e){null===this._dialog&&(this._dialog=$("#conversationLabelForm"),this._dialog.length||(this._dialog=$('
        ').hide().appendTo(document.body))),this._dialog.html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.label.assignLabels")}),this._dialog.wcfDialog("render"),$("#assignLabels").click($.proxy(this._save,this))},_save:function(){var e=[];this._dialog.find("input").each((function(a,t){var n=$(t);n.is(":checked")&&e.push(n.data("labelID"))})),this._proxy.setOption("data",{actionName:"assignLabel",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{conversationIDs:this._conversationIDs,labelIDs:e}}),this._proxy.sendRequest()},_assignLabels:function(e){for(var a=0,t=this._conversationIDs.length;a').hide().appendTo(document.body)),e.returnValues&&e.returnValues.actionName)switch(e.returnValues.actionName){case"add":this._insertLabel(e);break;case"getLabelManagement":this._maxLabels=parseInt(e.returnValues.maxLabels),this._labelCount=parseInt(e.returnValues.labelCount),this._dialog.empty().html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.label.management")}),this._dialog.wcfDialog("render"),this._bindListener()}else if(this._deletedLabelID){var n=new RegExp("(\\?|&)labelID="+this._deletedLabelID);window.location=window.location.toString().replace(n,"")}else window.location.reload()},_insertLabel:function(e){var a=$('
      • '+e.returnValues.label+"
      • ");a.find("a > span").data("labelID",e.returnValues.labelID).data("cssClassName",e.returnValues.cssClassName),this._labels.append(a),this._notification.show(),this._labelCount++,this._labelCount>=this._maxLabels&&$("#conversationLabelManagementForm").hide()},_bindListener:function(){$("#labelName").on("keyup keydown keypress",$.proxy(this._updateLabels,this)),$.browser.mozilla&&$.browser.touch&&$("#labelName").on("input",$.proxy(this._updateLabels,this)),this._labelCount>=this._maxLabels&&($("#conversationLabelManagementForm").hide(),this._dialog.wcfDialog("render")),$("#addLabel").disable().click($.proxy(this._addLabel,this)),$("#editLabel").disable(),this._dialog.find(".conversationLabelList a.label").click($.proxy(this._edit,this))},_edit:function(e){var a,t,n;this._labelCount>=this._maxLabels&&($("#conversationLabelManagementForm").show(),this._dialog.wcfDialog("render")),a=$(e.currentTarget),t=WCF.Language.get("wcf.conversation.label.management.editLabel").replace(/#labelName#/,WCF.String.escapeHTML(a.text())),$("#conversationLabelManagementForm").data("labelID",a.data("labelID")).children(".sectionTitle").html(t),$("#labelName").val(a.text()).trigger("keyup"),n=a.data("cssClassName"),$("#labelManagementList input").each((function(e,a){var t=$(a);t.val()==n&&t.attr("checked","checked")})),$("#addLabel").hide(),$("#editLabel").show().click($.proxy(this._editLabel,this)),$("#deleteLabel").show().click($.proxy(this._deleteLabel,this))},_editLabel:function(){this._proxy.setOption("data",{actionName:"update",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",objectIDs:[$("#conversationLabelManagementForm").data("labelID")],parameters:{data:{cssClassName:$("#labelManagementList input:checked").val(),label:$("#labelName").val()}}}),this._proxy.sendRequest()},_deleteLabel:function(){var e=WCF.Language.get("wcf.conversation.label.management.deleteLabel.confirmMessage").replace(/#labelName#/,$("#labelName").val());WCF.System.Confirmation.show(e,$.proxy((function(e){"confirm"===e&&(this._proxy.setOption("data",{actionName:"delete",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",objectIDs:[$("#conversationLabelManagementForm").data("labelID")]}),this._proxy.sendRequest(),this._deletedLabelID=$("#conversationLabelManagementForm").data("labelID"))}),this),void 0,void 0,!0)},_updateLabels:function(){var e=$.trim($("#labelName").val());e?$("#addLabel, #editLabel").enable():($("#addLabel, #editLabel").disable(),e=WCF.Language.get("wcf.conversation.label.placeholder")),$("#labelManagementList").find("span.label").text(e)},_addLabel:function(){var e=$("#labelName").val(),a=$("#labelManagementList input:checked").val();this._proxy.setOption("data",{actionName:"add",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{data:{cssClassName:a,labelName:e}}}),this._proxy.sendRequest(),this._dialog.wcfDialog("close")}}),WCF.Conversation.Message={},WCF.Conversation.Message.InlineEditor=WCF.Message.InlineEditor.extend({init:function(e,a){this._super(e,!0,a)},_getClassName:function(){return"wcf\\data\\conversation\\message\\ConversationMessageAction"}}); \ No newline at end of file diff --git a/files/js/WCF.Conversation.tiny.min.js b/files/js/WCF.Conversation.tiny.min.js deleted file mode 100644 index 7c3254f0..00000000 --- a/files/js/WCF.Conversation.tiny.min.js +++ /dev/null @@ -1 +0,0 @@ -WCF.Conversation={},WCF.Conversation.EditorHandler=Class.extend({_attributes:{},_conversations:{},_permissions:{},init:function(availableLabels){this._conversations={};var self=this;$(".conversation").each((function(index,conversation){var $labelIDs,$conversation=$(conversation),$conversationID=$conversation.data("conversationID");self._conversations[$conversationID]||(self._conversations[$conversationID]=$conversation,$labelIDs=eval($conversation.data("labelIDs")),self._attributes[$conversationID]={isClosed:!!$conversation.data("isClosed"),labelIDs:$labelIDs},self._permissions[$conversationID]={canAddParticipants:!!$conversation.data("canAddParticipants"),canCloseConversation:!!$conversation.data("canCloseConversation")})}))},getPermission:function(e,a){return void 0!==this._permissions[e][a]&&!!this._permissions[e][a]},getValue:function(e,a){switch(a){case"labelIDs":return void 0===this._attributes[e].labelIDs&&(this._attributes[e].labelIDs=[]),this._attributes[e].labelIDs;case"isClosed":return!!this._attributes[e].isClosed}},countAvailableLabels:function(){return this.getAvailableLabels().length},getAvailableLabels:function(){var e=[];return WCF.Dropdown.getDropdownMenu("conversationLabelFilter").children(".scrollableDropdownMenu").children("li").each((function(a,t){var n,s=$(t);if(s.hasClass("dropdownDivider"))return!1;n=s.find("span"),e.push({cssClassName:n.data("cssClassName"),labelID:n.data("labelID"),label:n.text()})})),e},update:function(e,a,t){var n,s,i,o,l,r;if(this._conversations[e]){switch(n=this._conversations[e],a){case"close":$(`
      • \n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
      • `).prependTo(n.find(".statusIcons")),this._attributes[e].isClosed=1;break;case"labelIDs":if(s={},WCF.Dropdown.getDropdownMenu("conversationLabelFilter").find("li > a > span").each((function(e,a){var t=$(a);s[t.data("labelID")]={cssClassName:t.data("cssClassName"),label:t.text(),url:t.parent().attr("href")}})),i=n.find(".columnSubject > .labelList"),t.length)for(i.length||(i=$('
          ').prependTo(n.find(".columnSubject"))),i.empty(),o=0,l=t.length;o'+WCF.String.escapeHTML(r.label)+"").appendTo(i);else i.length&&i.remove();break;case"open":n.find(".statusIcons li").each((function(e,a){var t=$(a);if(t.children("span.jsIconLock").length)return t.remove(),!1})),this._attributes[e].isClosed=0}WCF.Clipboard.reload()}else console.debug("[WCF.Conversation.EditorHandler] Unknown conversation id '"+e+"'")}}),WCF.Conversation.EditorHandlerConversation=WCF.Conversation.EditorHandler.extend({_availableLabels:null,init:function(e){this._availableLabels=e||[],this._super()},getAvailableLabels:function(){return this._availableLabels},update:function(e,a,t){var n,s,i,o;if(this._conversations[e])switch(n=$(".contentHeaderTitle > .contentHeaderMetaData"),a){case"close":$(`
        • \n\t\t\t\t\t\n\t\t\t\t\t${WCF.Language.get("wcf.global.state.closed")}\n\t\t\t\t
        • `).appendTo(n),this._attributes[e].isClosed=1;break;case"labelIDs":s=n.find(".labelList"),t.length?(i=this.getAvailableLabels(),s.length||(s=(s=$('
        • \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
            \n\t\t\t\t\t\t
          • ').prependTo(n)).children("ul")),o="",t.forEach((function(e){i.forEach((function(a){a.labelID==e&&(o+='
          • '+a.label+"
          • ")}))})),s[0].innerHTML=o):s.parent().remove();break;case"open":n.find(".jsIconLock").parent().remove(),this._attributes[e].isClosed=0}else console.debug("[WCF.Conversation.EditorHandler] Unknown conversation id '"+e+"'")}}),WCF.Conversation.Clipboard=Class.extend({_editorHandler:null,init:function(e){this._editorHandler=e,WCF.System.Event.addListener("com.woltlab.wcf.clipboard","com.woltlab.wcf.conversation.conversation",function(e){null===e.responseData?this._execute(e.data.actionName,e.data.parameters):this._evaluateResponse(e.data.actionName,e.responseData)}.bind(this))},_execute:function(e,a){"com.woltlab.wcf.conversation.conversation.assignLabel"===e&&new WCF.Conversation.Label.Editor(this._editorHandler,null,a.objectIDs)},_evaluateResponse:function(e,a){var t,n;switch(e){case"com.woltlab.wcf.conversation.conversation.leave":case"com.woltlab.wcf.conversation.conversation.leavePermanently":case"com.woltlab.wcf.conversation.conversation.markAsRead":case"com.woltlab.wcf.conversation.conversation.restore":window.location.reload();break;case"com.woltlab.wcf.conversation.conversation.close":case"com.woltlab.wcf.conversation.conversation.open":for(t in a.returnValues.conversationData)a.returnValues.conversationData.hasOwnProperty(t)&&(n=a.returnValues.conversationData[t],this._editorHandler.update(t,n.isClosed?"close":"open",n))}}}),WCF.Conversation.InlineEditor=WCF.InlineEditor.extend({_editorHandler:null,_environment:"conversation",_setOptions:function(){this._options=[{label:WCF.Language.get("wcf.conversation.edit.subject"),optionName:"editSubject"},{label:WCF.Language.get("wcf.conversation.edit.close"),optionName:"close"},{label:WCF.Language.get("wcf.conversation.edit.open"),optionName:"open"},{label:WCF.Language.get("wcf.conversation.edit.assignLabel"),optionName:"assignLabel"},{optionName:"divider"},{label:WCF.Language.get("wcf.conversation.edit.addParticipants"),optionName:"addParticipants"},{label:WCF.Language.get("wcf.conversation.edit.leave"),optionName:"leave"},{label:WCF.Language.get("wcf.global.button.edit"),optionName:"edit",isQuickOption:!0}]},setEditorHandler:function(e,a){this._editorHandler=e,this._environment="list"==a?"list":"conversation"},_getTriggerElement:function(e){return e.find(".jsConversationInlineEditor")},_validate:function(e,a){var t=$("#"+e).data("conversationID");switch(a){case"addParticipants":return this._editorHandler.getPermission(t,"canAddParticipants");case"assignLabel":return!!this._editorHandler.countAvailableLabels();case"editSubject":return!!this._editorHandler.getPermission(t,"canCloseConversation");case"close":case"open":return!!this._editorHandler.getPermission(t,"canCloseConversation")&&("close"===a?!this._editorHandler.getValue(t,"isClosed"):this._editorHandler.getValue(t,"isClosed"));case"leave":return!0;case"edit":return!!$("#"+e).data("isDraft")}return!1},_execute:function(e,a){if(!this._validate(e,a))return!1;switch(a){case"addParticipants":require(["WoltLabSuite/Core/Conversation/Ui/Participant/Add"],(function(a){new a(elData(elById(e),"conversation-id"))}));break;case"assignLabel":new WCF.Conversation.Label.Editor(this._editorHandler,e);break;case"editSubject":require(["WoltLabSuite/Core/Conversation/Ui/Subject/Editor"],(function(a){a.beginEdit(elData(elById(e),"conversation-id"))}));break;case"close":case"open":this._updateConversation(e,a,{isClosed:"close"===a?1:0});break;case"leave":new WCF.Conversation.Leave([$("#"+e).data("conversationID")],this._environment);break;case"edit":window.location=this._getTriggerElement($("#"+e)).prop("href")}},_updateConversation:function(e,a,t){var n=this._elements[e].data("conversationID");switch(a){case"close":case"editSubject":case"open":this._updateData.push({elementID:e,optionName:a,data:t}),this._proxy.setOption("data",{actionName:a,className:"wcf\\data\\conversation\\ConversationAction",objectIDs:[n]}),this._proxy.sendRequest()}},_updateState:function(){var e,a,t,n;for(e=0,a=this._updateData.length;e').hide().appendTo(document.body))),this._dialog.html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.leave.title")}),this._dialog.wcfDialog("render"),this._dialog.find("#hideConversation").click($.proxy(this._click,this))},_click:function(){var e=this._dialog.find("input[type=radio]:checked");1===e.length&&(this._proxy.setOption("data",{actionName:"hideConversation",className:"wcf\\data\\conversation\\ConversationAction",objectIDs:this._conversationIDs,parameters:{hideConversation:e.val()}}),this._proxy.sendRequest())}}),WCF.Conversation.Label={},WCF.Conversation.Label.Editor=Class.extend({_conversationIDs:0,_dialog:null,_editorHandler:null,_notification:null,_proxy:null,init:function(e,a,t){this._conversationIDs=a?[$("#"+a).data("conversationID")]:t,this._dialog=null,this._editorHandler=e,this._notification=new WCF.System.Notification(WCF.Language.get("wcf.global.success.edit")),this._proxy=new WCF.Action.Proxy({success:$.proxy(this._success,this)}),this._loadDialog()},_loadDialog:function(){this._proxy.setOption("data",{actionName:"getLabelForm",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{conversationIDs:this._conversationIDs}}),this._proxy.sendRequest()},_success:function(e,a,t){switch(e.returnValues.actionName){case"assignLabel":this._assignLabels(e);break;case"getLabelForm":this._renderDialog(e)}},_renderDialog:function(e){null===this._dialog&&(this._dialog=$("#conversationLabelForm"),this._dialog.length||(this._dialog=$('
            ').hide().appendTo(document.body))),this._dialog.html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.label.assignLabels")}),this._dialog.wcfDialog("render"),$("#assignLabels").click($.proxy(this._save,this))},_save:function(){var e=[];this._dialog.find("input").each((function(a,t){var n=$(t);n.is(":checked")&&e.push(n.data("labelID"))})),this._proxy.setOption("data",{actionName:"assignLabel",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{conversationIDs:this._conversationIDs,labelIDs:e}}),this._proxy.sendRequest()},_assignLabels:function(e){for(var a=0,t=this._conversationIDs.length;a').hide().appendTo(document.body)),e.returnValues&&e.returnValues.actionName)switch(e.returnValues.actionName){case"add":this._insertLabel(e);break;case"getLabelManagement":this._maxLabels=parseInt(e.returnValues.maxLabels),this._labelCount=parseInt(e.returnValues.labelCount),this._dialog.empty().html(e.returnValues.template),this._dialog.wcfDialog({title:WCF.Language.get("wcf.conversation.label.management")}),this._dialog.wcfDialog("render"),this._bindListener()}else if(this._deletedLabelID){var n=new RegExp("(\\?|&)labelID="+this._deletedLabelID);window.location=window.location.toString().replace(n,"")}else window.location.reload()},_insertLabel:function(e){var a=$('
          • '+e.returnValues.label+"
          • ");a.find("a > span").data("labelID",e.returnValues.labelID).data("cssClassName",e.returnValues.cssClassName),this._labels.append(a),this._notification.show(),this._labelCount++,this._labelCount>=this._maxLabels&&$("#conversationLabelManagementForm").hide()},_bindListener:function(){$("#labelName").on("keyup keydown keypress",$.proxy(this._updateLabels,this)),$.browser.mozilla&&$.browser.touch&&$("#labelName").on("input",$.proxy(this._updateLabels,this)),this._labelCount>=this._maxLabels&&($("#conversationLabelManagementForm").hide(),this._dialog.wcfDialog("render")),$("#addLabel").disable().click($.proxy(this._addLabel,this)),$("#editLabel").disable(),this._dialog.find(".conversationLabelList a.label").click($.proxy(this._edit,this))},_edit:function(e){var a,t,n;this._labelCount>=this._maxLabels&&($("#conversationLabelManagementForm").show(),this._dialog.wcfDialog("render")),a=$(e.currentTarget),t=WCF.Language.get("wcf.conversation.label.management.editLabel").replace(/#labelName#/,WCF.String.escapeHTML(a.text())),$("#conversationLabelManagementForm").data("labelID",a.data("labelID")).children(".sectionTitle").html(t),$("#labelName").val(a.text()).trigger("keyup"),n=a.data("cssClassName"),$("#labelManagementList input").each((function(e,a){var t=$(a);t.val()==n&&t.attr("checked","checked")})),$("#addLabel").hide(),$("#editLabel").show().click($.proxy(this._editLabel,this)),$("#deleteLabel").show().click($.proxy(this._deleteLabel,this))},_editLabel:function(){this._proxy.setOption("data",{actionName:"update",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",objectIDs:[$("#conversationLabelManagementForm").data("labelID")],parameters:{data:{cssClassName:$("#labelManagementList input:checked").val(),label:$("#labelName").val()}}}),this._proxy.sendRequest()},_deleteLabel:function(){var e=WCF.Language.get("wcf.conversation.label.management.deleteLabel.confirmMessage").replace(/#labelName#/,$("#labelName").val());WCF.System.Confirmation.show(e,$.proxy((function(e){"confirm"===e&&(this._proxy.setOption("data",{actionName:"delete",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",objectIDs:[$("#conversationLabelManagementForm").data("labelID")]}),this._proxy.sendRequest(),this._deletedLabelID=$("#conversationLabelManagementForm").data("labelID"))}),this),void 0,void 0,!0)},_updateLabels:function(){var e=$.trim($("#labelName").val());e?$("#addLabel, #editLabel").enable():($("#addLabel, #editLabel").disable(),e=WCF.Language.get("wcf.conversation.label.placeholder")),$("#labelManagementList").find("span.label").text(e)},_addLabel:function(){var e=$("#labelName").val(),a=$("#labelManagementList input:checked").val();this._proxy.setOption("data",{actionName:"add",className:"wcf\\data\\conversation\\label\\ConversationLabelAction",parameters:{data:{cssClassName:a,labelName:e}}}),this._proxy.sendRequest(),this._dialog.wcfDialog("close")}}),WCF.Conversation.Message={},WCF.Conversation.Message.InlineEditor=WCF.Message.InlineEditor.extend({init:function(e,a){this._super(e,!0,a)},_getClassName:function(){return"wcf\\data\\conversation\\message\\ConversationMessageAction"}}); \ No newline at end of file diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.js b/files/js/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.js deleted file mode 100644 index cd2b86fd..00000000 --- a/files/js/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Assign the labels for the given conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.assignConversationLabels = assignConversationLabels; - async function assignConversationLabels(conversationIDs, labelIDs) { - try { - await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/assign-labels`) - .post({ - conversationIDs: conversationIDs, - labelIDs: labelIDs, - }) - .fetchAsJson(); - } - catch (e) { - return (0, Result_1.apiResultFromError)(e); - } - return (0, Result_1.apiResultFromValue)([]); - } -}); diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.js b/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.js deleted file mode 100644 index 5fe51b6b..00000000 --- a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Gets the form data to assign labels to conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getConversationLabels = getConversationLabels; - async function getConversationLabels(conversationIDs) { - const url = new URL(`${window.WSC_RPC_API_URL}core/conversations/labels`); - for (const conversationID of conversationIDs) { - url.searchParams.append("conversationIDs[]", conversationID.toString()); - } - let response; - try { - response = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson()); - } - catch (e) { - return (0, Result_1.apiResultFromError)(e); - } - return (0, Result_1.apiResultFromValue)(response); - } -}); diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.js b/files/js/WoltLabSuite/Core/Api/Conversations/GetParticipantList.js similarity index 72% rename from files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.js rename to files/js/WoltLabSuite/Core/Api/Conversations/GetParticipantList.js index 997affed..34fa146b 100644 --- a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.js +++ b/files/js/WoltLabSuite/Core/Api/Conversations/GetParticipantList.js @@ -1,5 +1,5 @@ /** - * Gets the html code for the rendering the conversation leave dialog. + * Gets the html code for the rendering of the participant list of a conversation. * * @author Olaf Braun * @copyright 2001-2025 WoltLab GmbH @@ -9,11 +9,11 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.getConversationLeaveDialog = getConversationLeaveDialog; - async function getConversationLeaveDialog(conversationId) { + exports.getParticipantList = getParticipantList; + async function getParticipantList(conversationId) { let response; try { - response = (await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/leave-dialog`) + response = (await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/participants`) .get() .fetchAsJson()); } diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/LeaveConversation.js b/files/js/WoltLabSuite/Core/Api/Conversations/LeaveConversation.js deleted file mode 100644 index 8aafab4f..00000000 --- a/files/js/WoltLabSuite/Core/Api/Conversations/LeaveConversation.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Leaves a conversation. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.leaveConversation = leaveConversation; - async function leaveConversation(conversationId, hideConversation) { - let response; - try { - response = (await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/leave`) - .post({ - hideConversation, - }) - .fetchAsJson()); - } - catch (e) { - return (0, Result_1.apiResultFromError)(e); - } - return (0, Result_1.apiResultFromValue)(response.redirectUrl); - } -}); diff --git a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.js b/files/js/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.js similarity index 65% rename from files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.js rename to files/js/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.js index 52322661..af853049 100644 --- a/files/js/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.js +++ b/files/js/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.js @@ -1,5 +1,5 @@ /** - * Gets the data for the conversation label manager. + * Remove a participant from a conversation. * * @author Olaf Braun * @copyright 2001-2025 WoltLab GmbH @@ -9,17 +9,17 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.getConversationLabelManager = getConversationLabelManager; - async function getConversationLabelManager() { + exports.removeParticipant = removeParticipant; + async function removeParticipant(conversationId, participantId) { let response; try { - response = (await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/label-manager`) - .get() + response = (await (0, Backend_1.prepareRequest)(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/participants/${participantId}`) + .delete() .fetchAsJson()); } catch (e) { return (0, Result_1.apiResultFromError)(e); } - return (0, Result_1.apiResultFromValue)(response); + return (0, Result_1.apiResultFromValue)(response.template); } }); diff --git a/files/js/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.js b/files/js/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.js new file mode 100644 index 00000000..5d08027d --- /dev/null +++ b/files/js/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.js @@ -0,0 +1,31 @@ +/** + * Reacts to participants being removed from a conversation. + * + * @author Olaf Braun, Matthias Schmidt + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + */ +define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "../../Api/Conversations/RemoveParticipant", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Component/Confirmation", "WoltLabSuite/Core/Language"], function (require, exports, Selector_1, RemoveParticipant_1, PromiseMutex_1, Confirmation_1, Language_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = setup; + function setup() { + (0, Selector_1.wheneverSeen)(".conversationRemoveParticipant", (element) => { + const participantId = parseInt(element.dataset.participantId || "0", 10); + const conversationId = parseInt(element.dataset.conversationId || "0", 10); + const confirmMessage = element.dataset.confirmMessage; + element.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { + const confirmed = await (0, Confirmation_1.confirmationFactory)() + .custom((0, Language_1.getPhrase)("wcf.global.confirmation.title")) + .message(confirmMessage); + if (confirmed) { + const response = await (0, RemoveParticipant_1.removeParticipant)(conversationId, participantId); + if (!response.ok) { + return; + } + document.querySelector(".conversationParticipantList").outerHTML = response.value; + } + })); + }); + } +}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Bootstrap.js b/files/js/WoltLabSuite/Core/Conversation/Bootstrap.js index b65b2465..3f7fbc12 100644 --- a/files/js/WoltLabSuite/Core/Conversation/Bootstrap.js +++ b/files/js/WoltLabSuite/Core/Conversation/Bootstrap.js @@ -55,6 +55,11 @@ define(["require", "exports", "WoltLabSuite/Core/LazyLoader", "../Api/Conversati }); }); }); + (0, LazyLoader_1.whenFirstSeen)(".conversationRemoveParticipant", () => { + void new Promise((resolve_2, reject_2) => { require(["../Component/Conversation/RemoveParticipant"], resolve_2, reject_2); }).then(__importStar).then(({ setup }) => { + setup(); + }); + }); } function setup() { setupPopover(); diff --git a/files/js/WoltLabSuite/Core/Conversation/Clipboard.js b/files/js/WoltLabSuite/Core/Conversation/Clipboard.js deleted file mode 100644 index 933e8f9f..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Clipboard.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Clipboard for conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Event/Handler", "./Component/Label/Editor", "./Component/EditorHandler"], function (require, exports, Handler_1, Editor_1, EditorHandler_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.setup = setup; - function setup() { - (0, Handler_1.add)("com.woltlab.wcf.clipboard", "com.woltlab.wcf.conversation.conversation", (data) => { - if (data.responseData === null) { - execute(data.data.actionName, data.data.parameters); - } - else { - evaluateResponse(data.data.actionName, data.responseData); - } - }); - } - function execute(actionName, parameters) { - if (actionName === "com.woltlab.wcf.conversation.conversation.assignLabel") { - void (0, Editor_1.openDialog)(parameters.objectIDs); - } - } - function evaluateResponse(actionName, data) { - switch (actionName) { - case "com.woltlab.wcf.conversation.conversation.leave": - case "com.woltlab.wcf.conversation.conversation.leavePermanently": - case "com.woltlab.wcf.conversation.conversation.markAsRead": - case "com.woltlab.wcf.conversation.conversation.restore": - window.location.reload(); - break; - case "com.woltlab.wcf.conversation.conversation.close": - case "com.woltlab.wcf.conversation.conversation.open": - Object.entries(data.returnValues.conversationData).forEach(([conversationId, conversationData]) => { - (0, EditorHandler_1.getConversationEditor)(parseInt(conversationId)).isClosed = conversationData.isClosed; - }); - break; - } - } -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/EditorHandler.js b/files/js/WoltLabSuite/Core/Conversation/Component/EditorHandler.js deleted file mode 100644 index 4283ed1a..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Component/EditorHandler.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Handles editing for conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil", "WoltLabSuite/Core/Controller/Clipboard"], function (require, exports, Selector_1, Language_1, StringUtil_1, Clipboard_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getAvailableLabels = getAvailableLabels; - exports.addAvailableLabel = addAvailableLabel; - exports.getLabel = getLabel; - exports.getConversationEditor = getConversationEditor; - exports.setup = setup; - const _availableLabels = new Map(); - const conversations = new Map(); - class ConversationEditor { - #conversation; - #isClosed; - #canAddParticipants; - #canCloseConversation; - #labelIDs; - #isConversationListItem; - constructor(conversation) { - this.#conversation = conversation; - this.#isConversationListItem = conversation.classList.contains("jsClipboardObject"); - this.#isClosed = conversation.dataset.isClosed === "1"; - this.#canAddParticipants = conversation.dataset.canAddParticipants === "1"; - this.#canCloseConversation = conversation.dataset.canCloseConversation === "1"; - this.#labelIDs = JSON.parse(conversation.dataset.labelIds); - } - get isClosed() { - return this.#isClosed; - } - set isClosed(isClosed) { - this.#isClosed = isClosed; - this.#conversation.dataset.isClosed = isClosed ? "1" : "0"; - if (isClosed) { - this.#getContainer(".statusIcons")?.insertAdjacentHTML("beforeend", `
          • - - - -
          • `); - } - else { - this.#getContainer(".statusIcons").querySelector("li .jsIconLock")?.parentElement?.remove(); - } - (0, Clipboard_1.reload)(); - } - get canAddParticipants() { - return this.#canAddParticipants; - } - get canCloseConversation() { - return this.#canCloseConversation; - } - get labelIDs() { - return this.#labelIDs; - } - set labelIDs(labelIDs) { - this.#labelIDs = labelIDs; - this.#conversation.dataset.labelIDs = JSON.stringify(labelIDs); - const labels = labelIDs.map((labelID) => { - return getLabel(labelID); - }); - let labelList = this.#getContainer(".columnSubject > .labelList"); - if (labelIDs.length == 0) { - labelList?.remove(); - } - else { - if (!labelList) { - labelList = document.createElement("ul"); - labelList.classList.add("labelList"); - this.#getContainer(".columnSubject")?.insertAdjacentElement("afterbegin", labelList); - } - // remove old labels - labelList.innerHTML = ""; - for (const label of labels) { - if (this.#isConversationListItem) { - labelList.insertAdjacentHTML("beforeend", `
          • ${(0, StringUtil_1.escapeHTML)(label.label)}
          • `); - } - else { - labelList.insertAdjacentHTML("beforeend", `
          • ${(0, StringUtil_1.escapeHTML)(label.label)}
          • `); - } - } - } - (0, Clipboard_1.reload)(); - } - #getContainer(selector) { - if (this.#isConversationListItem) { - return this.#conversation.querySelector(selector); - } - else { - return document.querySelector(".contentHeaderTitle > .contentHeaderMetaData"); - } - } - } - function getAvailableLabels() { - return Array.from(_availableLabels.values()); - } - function addAvailableLabel(label) { - _availableLabels.set(label.labelID, label); - } - function getLabel(labelID) { - return _availableLabels.get(labelID); - } - function getConversationEditor(conversationID) { - return conversations.get(conversationID); - } - function setup(availableLabels) { - availableLabels.forEach((label) => { - _availableLabels.set(label.labelID, label); - }); - (0, Selector_1.wheneverFirstSeen)(".conversation", (conversation) => { - conversations.set(parseInt(conversation.dataset.conversationId), new ConversationEditor(conversation)); - }); - } -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/Label/Editor.js b/files/js/WoltLabSuite/Core/Conversation/Component/Label/Editor.js deleted file mode 100644 index b8889a30..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Component/Label/Editor.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Editor to assign labels to conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Dialog", "../../../Api/Conversations/GetConversationLabels", "WoltLabSuite/Core/Form/Builder/Manager", "../../../Api/Conversations/AssignConversationLabels", "WoltLabSuite/Core/Controller/Clipboard", "WoltLabSuite/Core/Component/Snackbar", "../EditorHandler"], function (require, exports, tslib_1, Dialog_1, GetConversationLabels_1, FormBuilderManager, AssignConversationLabels_1, Clipboard_1, Snackbar_1, EditorHandler_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.openDialog = openDialog; - FormBuilderManager = tslib_1.__importStar(FormBuilderManager); - async function openDialog(conversationIDs) { - const response = await (0, GetConversationLabels_1.getConversationLabels)(conversationIDs); - if (!response.ok) { - throw new Error("Failed to load form to assign labels to conversations."); - } - const dialog = (0, Dialog_1.dialogFactory)().fromHtml(response.value.template).asPrompt(); - dialog.addEventListener("afterClose", () => { - if (FormBuilderManager.hasForm(response.value.formId)) { - FormBuilderManager.unregisterForm(response.value.formId); - } - }); - dialog.addEventListener("primary", () => { - void FormBuilderManager.getData(response.value.formId).then(async (data) => { - const labelIDs = []; - for (const labelID of data["conversationLabel_labelIDs"]) { - labelIDs.push(parseInt(labelID)); - } - await (0, AssignConversationLabels_1.assignConversationLabels)(conversationIDs, labelIDs); - assignLabels(conversationIDs, labelIDs); - (0, Clipboard_1.reload)(); - }); - }); - dialog.show(response.value.title); - } - function assignLabels(conversationIDs, labelIDs) { - conversationIDs.forEach((conversationID) => { - (0, EditorHandler_1.getConversationEditor)(conversationID).labelIDs = labelIDs; - }); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/Label/Manager.js b/files/js/WoltLabSuite/Core/Conversation/Component/Label/Manager.js deleted file mode 100644 index fcdeddb6..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Component/Label/Manager.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Managed the labels of a conversation. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "tslib", "../../../Api/Conversations/GetConversationLabelManager", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Ui/Dropdown/Simple", "../EditorHandler"], function (require, exports, tslib_1, GetConversationLabelManager_1, PromiseMutex_1, Dialog_1, Language_1, Snackbar_1, Simple_1, EditorHandler_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.LabelManager = void 0; - Simple_1 = tslib_1.__importDefault(Simple_1); - class LabelManager { - #conversationListLink; - #formLink; - #dialog; - #maxLabels = 0; - #labelCount = 0; - constructor(formLink, conversationListLink) { - this.#formLink = formLink; - this.#conversationListLink = conversationListLink; - document.getElementById("manageLabel")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - const response = await (0, GetConversationLabelManager_1.getConversationLabelManager)(); - if (!response.ok) { - throw new Error("Could not fetch conversation label manager"); - } - this.#openDialog(response.value); - })); - } - #openDialog(data) { - this.#maxLabels = data.maxLabels; - this.#labelCount = data.labelCount; - this.#dialog = (0, Dialog_1.dialogFactory)().fromHtml(data.template).withoutControls(); - this.#updateAddButtonState(); - this.#dialog.show((0, Language_1.getPhrase)("wcf.conversation.label.management")); - this.#dialog?.content.querySelectorAll(".conversationLabelList .badge").forEach((badge) => { - const labelId = parseInt(badge.dataset.labelId); - badge.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await this.#openForm(labelId); - })); - }); - this.#dialog?.content.querySelector(".addLabel")?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await this.#openForm(); - })); - } - async #openForm(labelId) { - const url = new URL(this.#formLink); - if (labelId) { - url.searchParams.set("labelID", labelId.toString()); - } - const response = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(url.toString()); - if (response.ok) { - if (response.result.deleteLabel) { - // check if delete label id is present within URL (causing an IllegalLinkException if reloading) - const regex = new RegExp("(\\?|&)labelID=" + response.result.labelID); - window.location.href = window.location.toString().replace(regex, ""); - return; - } - if (labelId) { - window.location.reload(); - return; - } - this.#labelCount++; - const button = document.createElement("button"); - button.type = "button"; - button.classList.add("badge", "label", response.result.cssClassName || ""); - button.dataset.labelId = response.result.labelID.toString(); - button.dataset.cssClassName = response.result.cssClassName; - button.textContent = response.result.label; - button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => this.#openForm(response.result.labelID))); - const li = document.createElement("li"); - li.append(button); - this.#dialog?.content.querySelector(".conversationLabelList")?.append(li); - this.#insertLabel(response.result); - this.#updateAddButtonState(); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - } - #updateAddButtonState() { - this.#dialog.content.querySelector(".addLabel").disabled = this.#labelCount >= this.#maxLabels; - } - #insertLabel(data) { - const listItem = document.createElement("li"); - const anchor = document.createElement("a"); - const url = new URL(this.#conversationListLink); - url.searchParams.set("labelID", data.labelID.toString()); - anchor.href = url.toString(); - const span = document.createElement("span"); - span.className = `badge label${data.cssClassName ? " " + data.cssClassName : ""}`; - span.textContent = data.label; - span.dataset.labelID = data.labelID.toString(); - span.dataset.cssClassName = data.cssClassName; - anchor.appendChild(span); - listItem.appendChild(anchor); - Simple_1.default.getDropdownMenu("conversationLabelFilter") - ?.querySelector(".scrollableDropdownMenu") - ?.append(listItem); - (0, EditorHandler_1.addAvailableLabel)({ - labelID: data.labelID, - label: data.label, - cssClassName: data.cssClassName, - url: url.toString(), - }); - } - } - exports.LabelManager = LabelManager; -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Component/Leave.js b/files/js/WoltLabSuite/Core/Conversation/Component/Leave.js deleted file mode 100644 index 91e564fb..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Component/Leave.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Handles the leave conversation action. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ -define(["require", "exports", "tslib", "../../Api/Conversations/GetConversationLeaveDialog", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Language", "../../Api/Conversations/LeaveConversation", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, GetConversationLeaveDialog_1, Dialog_1, Language_1, LeaveConversation_1, Util_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.openDialog = openDialog; - Util_1 = tslib_1.__importDefault(Util_1); - async function openDialog(conversationID, environment) { - const result = await (0, GetConversationLeaveDialog_1.getConversationLeaveDialog)(conversationID); - if (result.ok) { - const dialog = (0, Dialog_1.dialogFactory)().fromHtml(result.value).asConfirmation(); - dialog.addEventListener("validate", (event) => { - const checked = getSelectedValue(dialog) !== undefined; - event.detail.push(Promise.resolve(checked)); - Util_1.default.innerError(dialog.querySelector("dl"), checked ? undefined : (0, Language_1.getPhrase)("wcf.global.form.error.empty")); - }); - dialog.addEventListener("primary", () => { - const hideConversation = getSelectedValue(dialog); - void (0, LeaveConversation_1.leaveConversation)(conversationID, hideConversation).then((leaveResult) => { - if (!leaveResult.ok) { - return; - } - if (environment === "conversation") { - window.location.href = leaveResult.value; - } - else { - window.location.reload(); - } - }); - }); - dialog.show((0, Language_1.getPhrase)("wcf.conversation.leave.title")); - } - } - function getSelectedValue(dialog) { - const selected = dialog.querySelector('input[name="hideConversation"]:checked'); - return selected ? parseInt(selected.value) : undefined; - } -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.js b/files/js/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.js deleted file mode 100644 index aef87fbb..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Reacts to participants being removed from a conversation. - * - * @author Matthias Schmidt - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/Object/Action/Handler"], function (require, exports, tslib_1, Handler_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.setup = setup; - Handler_1 = tslib_1.__importDefault(Handler_1); - function removeParticipant(data) { - data.objectElement.querySelector(".userLink").classList.add("conversationLeft"); - data.objectElement.querySelector(".jsObjectAction[data-object-action='removeParticipant']").remove(); - } - function setup() { - new Handler_1.default("removeParticipant", [], removeParticipant); - } -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Ui/Participant/Add.js b/files/js/WoltLabSuite/Core/Conversation/Ui/Participant/Add.js deleted file mode 100644 index c81dfe9d..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Ui/Participant/Add.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Adds participants to an existing conversation. - * - * @author Alexander Ebert - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Ui/Dialog", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Ui/ItemList/User", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Ajax, Util_1, Dialog_1, Snackbar_1, UiItemListUser, Language) { - "use strict"; - Ajax = tslib_1.__importStar(Ajax); - Util_1 = tslib_1.__importDefault(Util_1); - Dialog_1 = tslib_1.__importDefault(Dialog_1); - UiItemListUser = tslib_1.__importStar(UiItemListUser); - Language = tslib_1.__importStar(Language); - class UiParticipantAdd { - conversationId; - constructor(conversationId) { - this.conversationId = conversationId; - Ajax.api(this, { - actionName: "getAddParticipantsForm", - }); - } - _ajaxSetup() { - return { - data: { - className: "wcf\\data\\conversation\\ConversationAction", - objectIDs: [this.conversationId], - }, - }; - } - _ajaxSuccess(data) { - switch (data.actionName) { - case "addParticipants": - this.handleResponse(data); - break; - case "getAddParticipantsForm": - this.render(data); - break; - } - } - /** - * Shows the success message and closes the dialog overlay. - */ - handleResponse(data) { - if ("errorMessage" in data.returnValues) { - Util_1.default.innerError(document.getElementById("participantsInput").closest(".inputItemList"), data.returnValues.errorMessage); - return; - } - if ("count" in data.returnValues) { - (0, Snackbar_1.showSuccessSnackbar)(data.returnValues.successMessage).addEventListener("snackbar:close", () => { - window.location.reload(); - }); - } - Dialog_1.default.close(this); - } - /** - * Renders the dialog to add participants. - * @protected - */ - render(data) { - Dialog_1.default.open(this, data.returnValues.template); - const buttonSubmit = document.getElementById("addParticipants"); - buttonSubmit.disabled = true; - UiItemListUser.init("participantsInput", { - callbackChange: (elementId, values) => { - buttonSubmit.disabled = values.length === 0; - }, - excludedSearchValues: data.returnValues.excludedSearchValues, - maxItems: data.returnValues.maxItems, - includeUserGroups: data.returnValues.canAddGroupParticipants && data.returnValues.restrictUserGroupIDs.length > 0, - restrictUserGroupIDs: data.returnValues.restrictUserGroupIDs, - csvPerType: true, - }); - buttonSubmit.addEventListener("click", () => { - this.submit(); - }); - } - /** - * Sends a request to add participants. - */ - submit() { - const participants = []; - const participantsGroupIDs = []; - UiItemListUser.getValues("participantsInput").forEach((value) => { - if (value.type === "group") { - participantsGroupIDs.push(value.objectId); - } - else { - participants.push(value.value); - } - }); - const parameters = { - participants: participants, - participantsGroupIDs: participantsGroupIDs, - visibility: null, - }; - const visibility = Dialog_1.default.getDialog(this).content.querySelector('input[name="messageVisibility"]:checked, input[name="messageVisibility"][type="hidden"]'); - if (visibility) { - parameters.visibility = visibility.value; - } - Ajax.api(this, { - actionName: "addParticipants", - parameters: parameters, - }); - } - _dialogSetup() { - return { - id: "conversationAddParticipants", - options: { - title: Language.get("wcf.conversation.edit.addParticipants"), - }, - source: null, - }; - } - } - return UiParticipantAdd; -}); diff --git a/files/js/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.js b/files/js/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.js deleted file mode 100644 index 63d2bd8b..00000000 --- a/files/js/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.js +++ /dev/null @@ -1,106 +0,0 @@ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/Dialog", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Ajax", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Snackbar"], function (require, exports, tslib_1, Dialog_1, Util_1, Ajax, Language, Snackbar_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.beginEdit = beginEdit; - Dialog_1 = tslib_1.__importDefault(Dialog_1); - Util_1 = tslib_1.__importDefault(Util_1); - Ajax = tslib_1.__importStar(Ajax); - Language = tslib_1.__importStar(Language); - class UiSubjectEditor { - objectId; - subject; - constructor(objectId) { - this.objectId = objectId; - } - /** - * Shows the subject editor dialog. - */ - show() { - Dialog_1.default.open(this); - } - /** - * Validates and saves the new subject. - */ - saveEdit(event) { - event.preventDefault(); - const value = this.subject.value.trim(); - if (value === "") { - Util_1.default.innerError(this.subject, Language.get("wcf.global.form.error.empty")); - } - else { - Util_1.default.innerError(this.subject, ""); - Ajax.api(this, { - parameters: { - subject: value, - }, - objectIDs: [this.objectId], - }); - } - } - /** - * Returns the current conversation subject. - */ - getCurrentValue() { - return Array.from(document.querySelectorAll(`.jsConversationSubject[data-conversation-id="${this.objectId}"], .conversationLink[data-object-id="${this.objectId}"]`)) - .map((subject) => subject.textContent) - .slice(-1)[0]; - } - _ajaxSuccess(data) { - Dialog_1.default.close(this); - document - .querySelectorAll(`.jsConversationSubject[data-conversation-id="${this.objectId}"], .conversationLink[data-object-id="${this.objectId}"]`) - .forEach((subject) => { - subject.textContent = data.returnValues.subject; - }); - (0, Snackbar_1.showDefaultSuccessSnackbar)(); - } - _dialogSetup() { - return { - id: "dialogConversationSubjectEditor", - options: { - onSetup: (content) => { - this.subject = document.getElementById("jsConversationSubject"); - this.subject.addEventListener("keyup", (ev) => { - if (ev.key === "Enter") { - this.saveEdit(ev); - } - }); - content.querySelector(".jsButtonSave").addEventListener("click", (ev) => { - this.saveEdit(ev); - }); - }, - onShow: () => { - this.subject.value = this.getCurrentValue(); - }, - title: Language.get("wcf.conversation.edit.subject"), - }, - source: ` -
            -
            - -
            -
            - -
            -
            -
            - -
            - `, - }; - } - _ajaxSetup() { - return { - data: { - actionName: "editSubject", - className: "wcf\\data\\conversation\\ConversationAction", - }, - }; - } - } - let editor; - function beginEdit(objectId) { - editor = new UiSubjectEditor(objectId); - editor.show(); - } -}); diff --git a/files/lib/action/AddParticipantConversationDialogAction.class.php b/files/lib/action/AddParticipantConversationDialogAction.class.php new file mode 100644 index 00000000..a59f10a3 --- /dev/null +++ b/files/lib/action/AddParticipantConversationDialogAction.class.php @@ -0,0 +1,158 @@ + + * @since 6.2 + */ +final class AddParticipantConversationDialogAction implements RequestHandlerInterface +{ + use TConversationForm; + + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + try { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int, + } + EOT + ); + } catch (MappingError) { + throw new IllegalLinkException(); + } + + $conversation = new Conversation($parameters['id']); + + if (!Conversation::isParticipant([$conversation->conversationID]) || !$conversation->canAddParticipants()) { + throw new PermissionDeniedException(); + } + + $form = $this->getForm($conversation); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData(); + + $messageVisibility = $data['data']['messageVisibility'] ?? 'new'; + $participants = $data['participants'] ?? []; + if (isset($data['participantGroups'])) { + $groupIDs = $data['participantGroups']; + $participants = \array_unique( + \array_merge( + $participants, + $this->getUserByGroups($groupIDs) + ) + ); + } + + $participants = $this->filterOutParticipantsAlreadyAdded($participants, $conversation); + + (new AddParticipantConversation($conversation, $participants, $messageVisibility))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + /** + * @param int[] $participants + * + * @return int[] + */ + private function filterOutParticipantsAlreadyAdded(array $participants, Conversation $conversation): array + { + $alreadyParticipantIDs = $conversation->getParticipantIDs(true); + + return \array_filter($participants, static function (int $userID) use ($alreadyParticipantIDs): bool { + return !\in_array($userID, $alreadyParticipantIDs, true); + }); + } + + private function getForm(Conversation $conversation): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->get('wcf.conversation.edit.addParticipants') + ); + + $groupParticipants = \array_filter( + UserGroupCacheBuilder::getInstance()->getData([], 'groups'), + // @phpstan-ignore property.notFound + static fn (UserGroup $group) => $group->canBeAddedAsConversationParticipant + ); + + $form->appendChildren([ + UserFormField::create('participants') + ->label('wcf.conversation.participants') + ->description('wcf.conversation.participants.description') + ->maximumMultiples(WCF::getSession()->getPermission('user.conversation.maxParticipants')) + ->multiple() + ->maximumMultiples(WCF::getSession()->getPermission('user.conversation.maxParticipants') - $conversation->participants) + ->addValidator($this->getParticipantsValidator()) + ->addValidator($this->getMaximumParticipantsValidator(invisibleParticipantGroupsFieldId: null)), + BooleanFormField::create('addGroupParticipants') + ->label('wcf.conversation.addGroupParticipants') + ->available(\count($groupParticipants) > 0), + MultipleSelectionFormField::create('participantGroups') + ->label('wcf.conversation.participantGroups') + ->available(WCF::getSession()->getPermission('user.conversation.canAddGroupParticipants')) + ->filterable() + ->options($groupParticipants) + ->addDependency( + NonEmptyFormFieldDependency::create('addGroupParticipantsDependency') + ->fieldId('addGroupParticipants') + ), + RadioButtonFormField::create('messageVisibility') + ->label('wcf.conversation.visibility') + ->available(!$conversation->isDraft && $conversation->canAddParticipantsUnrestricted()) + ->required() + ->options([ + 'all' => 'wcf.conversation.visibility.all', + 'new' => 'wcf.conversation.visibility.new', + ]) + ->value('all'), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/files/lib/action/AssignConversationLabelDialogAction.class.php b/files/lib/action/AssignConversationLabelDialogAction.class.php new file mode 100644 index 00000000..8adc8e2f --- /dev/null +++ b/files/lib/action/AssignConversationLabelDialogAction.class.php @@ -0,0 +1,124 @@ + + * @since 6.2 + */ +final class AssignConversationLabelDialogAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id?: positive-int, + objectIDs?: positive-int[] + } + EOT + ); + + if (!isset($parameters['id']) && !isset($parameters['objectIDs'])) { + throw new IllegalLinkException(); + } + + $conversationIDs = $parameters['objectIDs'] ?? [$parameters['id']]; + + if ($conversationIDs === []) { + throw new IllegalLinkException(); + } + + if (!Conversation::isParticipant($conversationIDs)) { + throw new PermissionDeniedException(); + } + + $labelList = ConversationLabel::getLabelsByUser(); + if ($labelList->count() === 0) { + throw new IllegalLinkException(); + } + + $form = $this->getForm($conversationIDs, $labelList); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + $labelIDs = $form->getData()['labelIDs'] ?? []; + + (new AssignConversationLabel($labelList, $conversationIDs, $labelIDs))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + /** + * @param int[] $conversationIDs + */ + private function getForm(array $conversationIDs, ConversationLabelList $labelList): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->get('wcf.conversation.label.assignLabels') + ); + + $form->appendChildren([ + MultipleSelectionFormField::create('labelIDs') + ->options( + \array_map(static fn (ConversationLabel $label) => $label->render(), $labelList->getObjects()) + ) + ->value($this->getSelectedLabelIDs($conversationIDs)), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } + + /** + * @param int[] $conversationIDs + * + * @return int[] + */ + private function getSelectedLabelIDs(array $conversationIDs): array + { + if (\count($conversationIDs) !== 1) { + return []; + } + + $sql = "SELECT labelID + FROM wcf1_conversation_label_to_object + WHERE conversationID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([\reset($conversationIDs)]); + + return $statement->fetchAll(\PDO::FETCH_COLUMN) ?: []; + } +} diff --git a/files/lib/action/EditSubjectConversationDialogAction.class.php b/files/lib/action/EditSubjectConversationDialogAction.class.php new file mode 100644 index 00000000..89fc33be --- /dev/null +++ b/files/lib/action/EditSubjectConversationDialogAction.class.php @@ -0,0 +1,90 @@ + + * @since 6.2 + */ +final class EditSubjectConversationDialogAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + try { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int, + } + EOT + ); + } catch (MappingError) { + throw new IllegalLinkException(); + } + + $conversation = new Conversation($parameters['id']); + + if ($conversation->userID !== WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + + $form = $this->getForm($conversation); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + $data = $form->getData()['data']; + + (new SetConversationSubject($conversation, $data['subject']))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function getForm(Conversation $conversation): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->get('wcf.conversation.edit.subject') + ); + + $form->appendChildren([ + TextFormField::create('subject') + ->label('wcf.global.subject') + ->maximumLength(255) + ->required(), + ]); + + $form->markRequiredFields(false); + $form->updatedObject($conversation); + $form->build(); + + return $form; + } +} diff --git a/files/lib/bootstrap/com.woltlab.wcf.conversation.php b/files/lib/bootstrap/com.woltlab.wcf.conversation.php index b0267675..c8b5ffac 100644 --- a/files/lib/bootstrap/com.woltlab.wcf.conversation.php +++ b/files/lib/bootstrap/com.woltlab.wcf.conversation.php @@ -41,10 +41,13 @@ static function (\wcf\event\user\profile\UserProfileHeaderInteractionOptionColle static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationPopover()); $event->register(new \wcf\system\endpoint\controller\core\conversations\LeaveConversation()); - $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationLeaveDialog()); - $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationLabels()); - $event->register(new \wcf\system\endpoint\controller\core\conversations\AssignConversationLabels()); - $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationLabelManager()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\LeavePermanentlyConversation()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\RestoreConversation()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\OpenConversation()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\CloseConversation()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationHeaderTitle()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\RemoveConversationParticipant()); + $event->register(new \wcf\system\endpoint\controller\core\conversations\GetConversationParticipantList()); } ); }; diff --git a/files/lib/data/conversation/ConversationAction.class.php b/files/lib/data/conversation/ConversationAction.class.php index 3d6c69a1..33b3b086 100644 --- a/files/lib/data/conversation/ConversationAction.class.php +++ b/files/lib/data/conversation/ConversationAction.class.php @@ -5,21 +5,15 @@ use wcf\data\AbstractDatabaseObjectAction; use wcf\data\conversation\message\ConversationMessageAction; use wcf\data\conversation\message\ConversationMessageList; -use wcf\data\IClipboardAction; use wcf\data\IVisitableObjectAction; -use wcf\data\user\group\UserGroup; use wcf\page\ConversationPage; -use wcf\system\clipboard\ClipboardHandler; -use wcf\system\conversation\command\LeaveConversation; use wcf\system\conversation\ConversationHandler; use wcf\system\database\util\PreparedStatementConditionBuilder; -use wcf\system\event\EventHandler; use wcf\system\exception\IllegalLinkException; use wcf\system\exception\PermissionDeniedException; use wcf\system\exception\UserInputException; use wcf\system\log\modification\ConversationModificationLogHandler; use wcf\system\request\LinkHandler; -use wcf\system\search\SearchIndexManager; use wcf\system\style\FontAwesomeIcon; use wcf\system\user\notification\object\ConversationUserNotificationObject; use wcf\system\user\notification\UserNotificationHandler; @@ -36,9 +30,7 @@ * * @extends AbstractDatabaseObjectAction */ -class ConversationAction extends AbstractDatabaseObjectAction implements - IClipboardAction, - IVisitableObjectAction +class ConversationAction extends AbstractDatabaseObjectAction implements IVisitableObjectAction { /** * @inheritDoc @@ -51,12 +43,6 @@ class ConversationAction extends AbstractDatabaseObjectAction implements */ public $conversation; - /** - * list of conversation data modifications - * @var mixed[][] - */ - protected $conversationData = []; - /** * @inheritDoc */ @@ -359,10 +345,6 @@ public function markAsRead() ); } - if (!empty($conversationIDs)) { - $this->unmarkItems($conversationIDs); - } - $returnValues = [ 'totalCount' => ConversationHandler::getInstance() ->getUnreadConversationCount($this->parameters['userID'], true), @@ -457,141 +439,6 @@ public function validateMarkAllAsRead() // does nothing } - /** - * Validates parameters to close conversations. - * - * @return void - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateClose() - { - // read objects - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - // validate ownership - foreach ($this->getObjects() as $conversation) { - if ($conversation->isClosed || ($conversation->userID != WCF::getUser()->userID)) { - throw new PermissionDeniedException(); - } - } - } - - /** - * Closes conversations. - * - * @return mixed[][] - */ - public function close() - { - foreach ($this->getObjects() as $conversation) { - $conversation->update(['isClosed' => 1]); - $this->addConversationData($conversation->getDecoratedObject(), 'isClosed', 1); - - ConversationModificationLogHandler::getInstance()->close($conversation->getDecoratedObject()); - } - - $this->unmarkItems(); - - return $this->getConversationData(); - } - - /** - * Validates parameters to open conversations. - * - * @return void - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateOpen() - { - // read objects - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - // validate ownership - foreach ($this->getObjects() as $conversation) { - if (!$conversation->isClosed || ($conversation->userID != WCF::getUser()->userID)) { - throw new PermissionDeniedException(); - } - } - } - - /** - * Opens conversations. - * - * @return mixed[][] - */ - public function open() - { - foreach ($this->getObjects() as $conversation) { - $conversation->update(['isClosed' => 0]); - $this->addConversationData($conversation->getDecoratedObject(), 'isClosed', 0); - - ConversationModificationLogHandler::getInstance()->open($conversation->getDecoratedObject()); - } - - $this->unmarkItems(); - - return $this->getConversationData(); - } - - /** - * Validates parameters to hide conversations. - * - * @return void - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateHideConversation() - { - $this->parameters['hideConversation'] = isset($this->parameters['hideConversation']) ? \intval($this->parameters['hideConversation']) : null; - if ( - $this->parameters['hideConversation'] === null - || !\in_array( - $this->parameters['hideConversation'], - [Conversation::STATE_DEFAULT, Conversation::STATE_HIDDEN, Conversation::STATE_LEFT] - ) - ) { - throw new UserInputException('hideConversation'); - } - - if (empty($this->objectIDs)) { - throw new UserInputException('objectIDs'); - } - - // validate participation - if (!Conversation::isParticipant($this->objectIDs)) { - throw new PermissionDeniedException(); - } - } - - /** - * Hides or restores conversations. - * - * @return array{actionName: string, redirectURL: string} - */ - public function hideConversation() - { - (new LeaveConversation($this->objectIDs, $this->parameters['hideConversation']))(); - - return [ - 'actionName' => 'hideConversation', - 'redirectURL' => LinkHandler::getInstance()->getLink('ConversationList'), - ]; - } - /** * @since 5.5 */ @@ -718,284 +565,6 @@ public function getConversations(): array ]; } - /** - * Validates the 'unmarkAll' action. - * - * @return void - */ - public function validateUnmarkAll() - { - // does nothing - } - - /** - * Unmarks all conversations. - * - * @return void - */ - public function unmarkAll() - { - ClipboardHandler::getInstance()->removeItems( - ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation') - ); - } - - /** - * Validates parameters to display the 'add participants' form. - * - * @return void - * @throws PermissionDeniedException - */ - public function validateGetAddParticipantsForm() - { - $this->conversation = $this->getSingleObject(); - if ( - !Conversation::isParticipant([$this->conversation->conversationID]) - || !$this->conversation->canAddParticipants() - ) { - throw new PermissionDeniedException(); - } - } - - /** - * Shows the 'add participants' form. - * - * @return array{ - * excludedSearchValues: string[], - * maxItems: int, - * canAddGroupParticipants: int, - * template: string, - * restrictUserGroupIDs: list, - * } - */ - public function getAddParticipantsForm() - { - $restrictUserGroupIDs = []; - foreach (UserGroup::getAllGroups() as $group) { - // @phpstan-ignore property.notFound - if ($group->canBeAddedAsConversationParticipant) { - $restrictUserGroupIDs[] = $group->groupID; - } - } - - return [ - 'excludedSearchValues' => $this->conversation->getParticipantNames( - false, - true, - $this->conversation->userID == WCF::getUser()->userID - ), - 'maxItems' => WCF::getSession()->getPermission('user.conversation.maxParticipants') - $this->conversation->participants, - 'canAddGroupParticipants' => WCF::getSession()->getPermission('user.conversation.canAddGroupParticipants'), - 'template' => WCF::getTPL()->render( - 'wcf', - 'conversationAddParticipants', - ['conversation' => $this->conversation] - ), - 'restrictUserGroupIDs' => $restrictUserGroupIDs, - ]; - } - - /** - * Validates parameters to add new participants. - * - * @return void - */ - public function validateAddParticipants() - { - $this->validateGetAddParticipantsForm(); - - // validate participants - $this->readStringArray('participants', true); - $this->readIntegerArray('participantsGroupIDs', true); - - if (!$this->conversation->getDecoratedObject()->isDraft) { - $this->readString('visibility'); - if (!\in_array($this->parameters['visibility'], ['all', 'new'])) { - throw new UserInputException('visibility'); - } - - if ($this->parameters['visibility'] === 'all' && !$this->conversation->canAddParticipantsUnrestricted()) { - throw new UserInputException('visibility'); - } - } - } - - /** - * Adds new participants. - * - * @return array{ - * count: int, - * successMessage: string, - * }|array{ - * actionName: 'addParticipants', - * errorMessage: string, - * } - */ - public function addParticipants() - { - // TODO migrate to FormBuilder - try { - $existingParticipants = $this->conversation->getParticipantIDs(true); - $participantIDs = Conversation::validateParticipants( - $this->parameters['participants'], - 'participants', - $existingParticipants - ); - if ( - !empty($this->parameters['participantsGroupIDs']) - && WCF::getSession()->getPermission('user.conversation.canAddGroupParticipants') - ) { - $validGroupParticipants = Conversation::validateGroupParticipants( - $this->parameters['participantsGroupIDs'], - 'participants', - $existingParticipants - ); - $validGroupParticipants = \array_diff($validGroupParticipants, $participantIDs); - if (empty($validGroupParticipants)) { - throw new UserInputException('participants', 'emptyGroup'); - } - $participantIDs = \array_merge($participantIDs, $validGroupParticipants); - } - - $parameters = [ - 'participantIDs' => $participantIDs, - ]; - EventHandler::getInstance()->fireAction($this, 'addParticipants_validateParticipants', $parameters); - $participantIDs = $parameters['participantIDs']; - } catch (UserInputException $e) { - $errorMessage = ''; - $errors = \is_array($e->getType()) ? $e->getType() : [['type' => $e->getType()]]; - foreach ($errors as $type) { - if (!empty($errorMessage)) { - $errorMessage .= ' '; - } - $errorMessage .= WCF::getLanguage()->getDynamicVariable( - 'wcf.conversation.participants.error.' . $type['type'], - ['username' => $type['username']] - ); - } - - return [ - 'actionName' => 'addParticipants', - 'errorMessage' => $errorMessage, - ]; - } - - // validate limit - $newCount = $this->conversation->participants + \count($participantIDs); - if ($newCount > WCF::getSession()->getPermission('user.conversation.maxParticipants')) { - return [ - 'actionName' => 'addParticipants', - 'errorMessage' => WCF::getLanguage()->getDynamicVariable('wcf.conversation.participants.error.tooManyParticipants'), - ]; - } - - $count = 0; - $successMessage = ''; - if (!empty($participantIDs)) { - // check for already added participants - if ($this->conversation->isDraft) { - $draftData = \unserialize($this->conversation->draftData); - $draftData['participants'] = \array_merge($draftData['participants'], $participantIDs); - $data = ['data' => ['draftData' => \serialize($draftData)]]; - } else { - $data = [ - 'participants' => $participantIDs, - 'visibility' => (isset($this->parameters['visibility'])) ? $this->parameters['visibility'] : 'all', - ]; - } - - $conversationAction = new self([$this->conversation], 'update', $data); - $conversationAction->executeAction(); - - $count = \count($participantIDs); - $successMessage = WCF::getLanguage()->getDynamicVariable( - 'wcf.conversation.edit.addParticipants.success', - ['count' => $count] - ); - - ConversationModificationLogHandler::getInstance() - ->addParticipants($this->conversation->getDecoratedObject(), $participantIDs); - - if (!$this->conversation->isDraft) { - // update participant summary - $this->conversation->updateParticipantSummary(); - } - } - - return [ - 'count' => $count, - 'successMessage' => $successMessage, - ]; - } - - /** - * Validates parameters to remove a participant from a conversation. - * - * @return void - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateRemoveParticipant() - { - // The previous request from `WCF.Action.Delete` used `userID`, while the new `Ui/Object/Action` - // module passes `userId`. - try { - $this->readInteger('userID'); - } catch (UserInputException $e) { - $this->readInteger('userId'); - $this->parameters['userID'] = $this->parameters['userId']; - } - - // validate conversation - $this->conversation = $this->getSingleObject(); - if (!$this->conversation->conversationID) { - throw new UserInputException('objectIDs'); - } - - // check ownership - if ($this->conversation->userID != WCF::getUser()->userID) { - throw new PermissionDeniedException(); - } - - // validate participants - if ( - $this->parameters['userID'] == WCF::getUser()->userID - || !Conversation::isParticipant([$this->conversation->conversationID]) - || !Conversation::isParticipant([$this->conversation->conversationID], $this->parameters['userID']) - ) { - throw new PermissionDeniedException(); - } - } - - /** - * Removes a participant from a conversation. - * - * @return array{userID: int} - */ - public function removeParticipant() - { - $this->conversation->removeParticipant($this->parameters['userID']); - $this->conversation->updateParticipantSummary(); - - $userConversation = Conversation::getUserConversation( - $this->conversation->conversationID, - $this->parameters['userID'] - ); - - if (!$userConversation->isInvisible) { - ConversationModificationLogHandler::getInstance() - ->removeParticipant($this->conversation->getDecoratedObject(), $this->parameters['userID']); - } - - // reset storage - UserStorageHandler::getInstance()->reset([$this->parameters['userID']], 'unreadConversationCount'); - - return [ - 'userID' => $this->parameters['userID'], - ]; - } - /** * Rebuilds the conversation data of the relevant conversations. * @@ -1042,94 +611,4 @@ public function rebuild() $conversationAction->executeAction(); } } - - /** - * Validates the parameters to edit a conversation's subject. - * - * @return void - * @throws PermissionDeniedException - */ - public function validateEditSubject() - { - $this->readString('subject'); - - $this->conversation = $this->getSingleObject(); - if ($this->conversation->userID != WCF::getUser()->userID) { - throw new PermissionDeniedException(); - } - } - - /** - * Edits a conversation's subject. - * - * @return array{subject: string} - */ - public function editSubject() - { - $subject = \mb_substr($this->parameters['subject'], 0, 255); - - $this->conversation->update([ - 'subject' => $subject, - ]); - - $message = $this->conversation->getFirstMessage(); - - SearchIndexManager::getInstance()->set( - 'com.woltlab.wcf.conversation.message', - $message->messageID, - $message->message, - $subject, - $message->time, - $message->userID, - $message->username - ); - - return [ - 'subject' => $subject, - ]; - } - - /** - * Adds conversation modification data. - * - * @return void - */ - protected function addConversationData(Conversation $conversation, string $key, mixed $value) - { - if (!isset($this->conversationData[$conversation->conversationID])) { - $this->conversationData[$conversation->conversationID] = []; - } - - $this->conversationData[$conversation->conversationID][$key] = $value; - } - - /** - * Returns conversation data. - * - * @return mixed[][] - */ - protected function getConversationData() - { - return [ - 'conversationData' => $this->conversationData, - ]; - } - - /** - * Unmarks conversations. - * - * @param int[] $conversationIDs - * @return void - */ - protected function unmarkItems(array $conversationIDs = []) - { - if (empty($conversationIDs)) { - $conversationIDs = $this->objectIDs; - } - - ClipboardHandler::getInstance()->unmark( - $conversationIDs, - ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation') - ); - } } diff --git a/files/lib/data/conversation/UserConversationList.class.php b/files/lib/data/conversation/UserConversationList.class.php index f4e0fc20..eb1cacf4 100644 --- a/files/lib/data/conversation/UserConversationList.class.php +++ b/files/lib/data/conversation/UserConversationList.class.php @@ -45,13 +45,13 @@ class UserConversationList extends ConversationList /** * Creates a new UserConversationList - * - * @param int $userID - * @param string $filter - * @param int $labelID */ - public function __construct($userID, $filter = '', $labelID = 0) + public function __construct(?int $userID = null, string $filter = '', ?int $labelID = null) { + if (!$userID) { + $userID = WCF::getUser()->userID; + } + parent::__construct(); $this->filter = $filter; diff --git a/files/lib/data/conversation/label/ConversationLabel.class.php b/files/lib/data/conversation/label/ConversationLabel.class.php index f81ee91c..c5c4d9c6 100644 --- a/files/lib/data/conversation/label/ConversationLabel.class.php +++ b/files/lib/data/conversation/label/ConversationLabel.class.php @@ -4,6 +4,7 @@ use wcf\data\DatabaseObject; use wcf\system\WCF; +use wcf\util\StringUtil; /** * Represents a conversation label. @@ -64,4 +65,14 @@ public static function getLabelCssClassNames() { return self::$availableCssClassNames; } + + public function render(): string + { + $cssClassName = StringUtil::encodeHTML($this->cssClassName ? ' ' . $this->cssClassName : ''); + $title = StringUtil::encodeHTML($this->label); + + return <<{$title} + HTML; + } } diff --git a/files/lib/event/interaction/bulk/user/ConversationBulkInteractionCollecting.class.php b/files/lib/event/interaction/bulk/user/ConversationBulkInteractionCollecting.class.php new file mode 100644 index 00000000..d05cd31d --- /dev/null +++ b/files/lib/event/interaction/bulk/user/ConversationBulkInteractionCollecting.class.php @@ -0,0 +1,21 @@ + + * @since 6.2 + */ +final class ConversationBulkInteractionCollecting implements IPsr14Event +{ + public function __construct(public readonly ConversationBulkInteractions $provider) + { + } +} diff --git a/files/lib/event/interaction/user/ConversationInteractionCollecting.class.php b/files/lib/event/interaction/user/ConversationInteractionCollecting.class.php new file mode 100644 index 00000000..9291f3fe --- /dev/null +++ b/files/lib/event/interaction/user/ConversationInteractionCollecting.class.php @@ -0,0 +1,21 @@ + + * @since 6.2 + */ +final class ConversationInteractionCollecting implements IPsr14Event +{ + public function __construct(public readonly ConversationInteractions $provider) + { + } +} diff --git a/files/lib/event/listView/user/ConversationListViewInitialized.class.php b/files/lib/event/listView/user/ConversationListViewInitialized.class.php new file mode 100644 index 00000000..2c274e7f --- /dev/null +++ b/files/lib/event/listView/user/ConversationListViewInitialized.class.php @@ -0,0 +1,21 @@ + + * @since 6.2 + */ +final class ConversationListViewInitialized implements IPsr14Event +{ + public function __construct(public readonly ConversationListView $provider) + { + } +} diff --git a/files/lib/form/ConversationAddForm.class.php b/files/lib/form/ConversationAddForm.class.php index 5803f874..3610452c 100644 --- a/files/lib/form/ConversationAddForm.class.php +++ b/files/lib/form/ConversationAddForm.class.php @@ -8,8 +8,7 @@ use wcf\data\user\group\UserGroup; use wcf\system\cache\builder\UserGroupCacheBuilder; use wcf\system\cache\runtime\UserProfileRuntimeCache; -use wcf\system\database\util\PreparedStatementConditionBuilder; -use wcf\system\exception\UserInputException; +use wcf\system\conversation\TConversationForm; use wcf\system\flood\FloodControl; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\container\wysiwyg\WysiwygFormContainer; @@ -24,7 +23,6 @@ use wcf\system\form\builder\field\validation\FormFieldValidator; use wcf\system\form\builder\IFormDocument; use wcf\system\page\PageLocationManager; -use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; use wcf\util\HeaderUtil; @@ -39,6 +37,8 @@ */ class ConversationAddForm extends AbstractFormBuilderForm { + use TConversationForm; + /** * @inheritDoc */ @@ -101,8 +101,8 @@ static function (UserGroup $group) { ->label('wcf.conversation.participants') ->description('wcf.conversation.participants.description') ->maximumMultiples(WCF::getSession()->getPermission('user.conversation.maxParticipants')) - ->addValidator(self::getParticipantsValidator()) - ->addValidator(self::getMaximumParticipantsValidator()), + ->addValidator($this->getParticipantsValidator()) + ->addValidator($this->getMaximumParticipantsValidator()), BooleanFormField::create('addGroupParticipants') ->label('wcf.conversation.addGroupParticipants') ->available(\count($groupParticipants) > 0), @@ -120,7 +120,7 @@ static function (UserGroup $group) { ->description('wcf.conversation.invisibleParticipants.description') ->available(WCF::getSession()->getPermission('user.conversation.canAddInvisibleParticipants')) ->maximumMultiples(WCF::getSession()->getPermission('user.conversation.maxParticipants')) - ->addValidator(self::getParticipantsValidator()) + ->addValidator($this->getParticipantsValidator()) ->addValidator( new FormFieldValidator( 'duplicateParticipantsValidator', @@ -219,7 +219,7 @@ protected function finalizeForm() ->addProcessor( new CustomFormDataProcessor( 'participantsProcessor', - static function (IFormDocument $document, array $parameters) { + function (IFormDocument $document, array $parameters) { $participants = $parameters['participants'] ?? []; $invisibleParticipants = $parameters['invisibleParticipants'] ?? []; @@ -228,14 +228,14 @@ static function (IFormDocument $document, array $parameters) { $participants = \array_unique( \array_merge( $participants, - ConversationAddForm::getUserByGroups($groupIDs) + $this->getUserByGroups($groupIDs) ) ); } if (isset($parameters['invisibleParticipantGroups'])) { $groupIDs = $parameters['invisibleParticipantGroups']; - $userIDs = ConversationAddForm::getUserByGroups($groupIDs); + $userIDs = $this->getUserByGroups($groupIDs); $invisibleParticipants = \array_unique( \array_merge( @@ -306,119 +306,4 @@ public function saved() exit; } - - /** - * Returns a validator that checks if the selected participants are valid. - * - * @since 6.2 - */ - public static function getParticipantsValidator(): FormFieldValidator - { - return new FormFieldValidator('participantsValidator', static function (UserFormField $formField) { - $users = $formField->getUsers(); - $userIDs = \array_column($users, 'userID'); - - UserStorageHandler::getInstance()->loadStorage($userIDs); - - foreach ($users as $user) { - try { - if ($user->userID === WCF::getUser()->userID) { - throw new UserInputException('isAuthor'); - } - - Conversation::validateParticipant($user, $formField->getId()); - } catch (UserInputException $e) { - $formField->addValidationError( - new FormFieldValidationError( - $e->getType(), - 'wcf.conversation.participants.error.' . $e->getType(), - [ - 'username' => $user->username, - ] - ) - ); - } - } - }); - } - - /** - * Returns a validator that checks if the maximum number of participants is not exceeded. - * - * @since 6.2 - */ - public static function getMaximumParticipantsValidator( - string $invisibleParticipantsFieldId = 'invisibleParticipants', - string $participantGroupsFieldId = 'participantGroups', - string $invisibleParticipantGroupsFieldId = 'invisibleParticipantGroups' - ): FormFieldValidator { - return new FormFieldValidator( - 'participantsMaximumValidator', - static function (UserFormField $formField) use ( - $invisibleParticipantsFieldId, - $participantGroupsFieldId, - $invisibleParticipantGroupsFieldId - ) { - /** - * @var UserFormField|null $invisibleParticipantsFormField - * @var MultipleSelectionFormField|null $participantGroupsFormField - * @var MultipleSelectionFormField|null $invisibleParticipantGroupsFormField - */ - - $invisibleParticipantsFormField = $formField->getDocument() - ->getNodeById($invisibleParticipantsFieldId); - $participantGroupsFormField = $formField->getDocument() - ->getNodeById($participantGroupsFieldId); - $invisibleParticipantGroupsFormField = $formField->getDocument() - ->getNodeById($invisibleParticipantGroupsFieldId); - $groupIDs = \array_merge( - $participantGroupsFormField?->getValue() ?: [], - $invisibleParticipantGroupsFormField?->getValue() ?: [], - ); - $userIDs = \array_merge( - \array_column($formField->getUsers(), 'userID'), - \array_column($invisibleParticipantsFormField?->getUsers() ?: [], 'userID'), - ConversationAddForm::getUserByGroups($groupIDs) - ); - - if (\count($userIDs) > WCF::getSession()->getPermission('user.conversation.maxParticipants')) { - $formField->addValidationError( - new FormFieldValidationError( - 'tooManyParticipants', - 'wcf.conversation.participants.error.tooManyParticipants' - ) - ); - } - } - ); - } - - /** - * Returns the user IDs of the users that are in the given groups. - * - * @param int[] $groupIDs - * - * @return int[] - */ - public static function getUserByGroups(array $groupIDs): array - { - if ($groupIDs === []) { - return []; - } - - $conditionBuilder = new PreparedStatementConditionBuilder(); - $conditionBuilder->add('groupID IN (?)', [$groupIDs]); - $sql = "SELECT DISTINCT userID - FROM wcf1_user_to_group - " . $conditionBuilder; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditionBuilder->getParameters()); - - $userIDs = []; - while ($userID = $statement->fetchColumn()) { - $userIDs[] = $userID; - } - - return $userIDs; - } } diff --git a/files/lib/page/ConversationListPage.class.php b/files/lib/page/ConversationListPage.class.php index 69c0ac1f..6ce0b39a 100644 --- a/files/lib/page/ConversationListPage.class.php +++ b/files/lib/page/ConversationListPage.class.php @@ -2,115 +2,33 @@ namespace wcf\page; -use wcf\data\conversation\label\ConversationLabel; -use wcf\data\conversation\label\ConversationLabelList; use wcf\data\conversation\UserConversationList; -use wcf\system\clipboard\ClipboardHandler; -use wcf\system\database\util\PreparedStatementConditionBuilder; -use wcf\system\exception\IllegalLinkException; -use wcf\system\page\PageLocationManager; +use wcf\system\listView\user\ConversationListView; use wcf\system\request\LinkHandler; use wcf\system\WCF; -use wcf\util\ArrayUtil; -use wcf\util\HeaderUtil; /** * Shows a list of conversations. * - * @author Marcel Werk - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Marcel Werk + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License * - * @extends SortablePage + * @extends AbstractListViewPage */ -class ConversationListPage extends SortablePage +final class ConversationListPage extends AbstractListViewPage { - /** - * @inheritDoc - */ - public $defaultSortField = CONVERSATION_LIST_DEFAULT_SORT_FIELD; - - /** - * @inheritDoc - */ - public $defaultSortOrder = CONVERSATION_LIST_DEFAULT_SORT_ORDER; - - /** - * @inheritDoc - */ - public $validSortFields = ['subject', 'time', 'username', 'lastPostTime', 'replies', 'participants']; - - /** - * @inheritDoc - */ - public $itemsPerPage = CONVERSATIONS_PER_PAGE; - - /** - * @inheritDoc - */ - public $loginRequired = true; - - /** - * @inheritDoc - */ - public $neededModules = ['MODULE_CONVERSATION']; - - /** - * @inheritDoc - */ - public $neededPermissions = ['user.conversation.canUseConversation']; + public string $filter = ''; - /** - * list filter - * @var string - */ - public $filter = ''; + public int $conversationCount = 0; - /** - * label id - * @var int - */ - public $labelID = 0; + public int $draftCount = 0; - /** - * label list object - * @var ConversationLabelList - */ - public $labelList; - - /** - * number of conversations (no filter) - * @var int - */ - public $conversationCount = 0; + public int $hiddenCount = 0; - /** - * number of drafts - * @var int - */ - public $draftCount = 0; + public int $outboxCount = 0; - /** - * number of hidden conversations - * @var int - */ - public $hiddenCount = 0; - - /** - * number of sent conversations - * @var int - */ - public $outboxCount = 0; - - /** - * participant that - * @var string[] - */ - public $participants = []; - - /** - * @inheritDoc - */ + #[\Override] public function readParameters() { parent::readParameters(); @@ -121,131 +39,6 @@ public function readParameters() if (!\in_array($this->filter, UserConversationList::$availableFilters)) { $this->filter = ''; } - - // user settings - /** @noinspection PhpUndefinedFieldInspection */ - if (WCF::getUser()->conversationsPerPage) { - /** @noinspection PhpUndefinedFieldInspection */ - $this->itemsPerPage = WCF::getUser()->conversationsPerPage; - } - - // labels - $this->labelList = ConversationLabel::getLabelsByUser(); - if (!empty($_REQUEST['labelID'])) { - $this->labelID = \intval($_REQUEST['labelID']); - - $validLabel = false; - foreach ($this->labelList as $label) { - if ($label->labelID == $this->labelID) { - $validLabel = true; - break; - } - } - - if (!$validLabel) { - throw new IllegalLinkException(); - } - } - - if (isset($_REQUEST['participants'])) { - $this->participants = \array_slice(ArrayUtil::trim(\explode(',', $_REQUEST['participants'])), 0, 20); - } - - if (!empty($_POST)) { - $participantsParameter = ''; - foreach ($this->participants as $participant) { - if (!empty($participantsParameter)) { - $participantsParameter .= ','; - } - $participantsParameter .= \rawurlencode($participant); - } - if (!empty($participantsParameter)) { - $participantsParameter = '&participants=' . $participantsParameter; - } - - HeaderUtil::redirect( - LinkHandler::getInstance()->getLink( - 'ConversationList', - [], - 'sortField=' . $this->sortField . '&sortOrder=' . $this->sortOrder . '&filter=' . $this->filter . '&labelID=' . $this->labelID . '&pageNo=' . $this->pageNo . $participantsParameter - ) - ); - - exit; - } - } - - /** - * @inheritDoc - */ - protected function initObjectList() - { - $this->objectList = new UserConversationList(WCF::getUser()->userID, $this->filter, $this->labelID); - $this->objectList->setLabelList($this->labelList); - - if (!empty($this->participants)) { - // The column `conversation_to_user.username` has no index, causing full table scans when - // trying to filter by it, therefore we'll read the user ids in advance. - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add('username IN (?)', [$this->participants]); - $sql = "SELECT userID - FROM wcf1_user - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - $userIDs = []; - while ($userID = $statement->fetchColumn()) { - $userIDs[] = $userID; - } - - if (!empty($userIDs)) { - // The condition is split into two branches in order to account for invisible participants. - // Invisible participants are only visible to the conversation starter and remain invisible - // until they write their first message. - // - // We need to protect these users from being exposed as participants by including them for - // any conversation that the current user has started. For all other conversations, users - // flagged with `isInvisible = 0` must be excluded. - // - // See https://github.com/WoltLab/com.woltlab.wcf.conversation/issues/131 - $this->objectList->getConditionBuilder()->add(' - ( - ( - conversation.userID = ? - AND conversation.conversationID IN ( - SELECT conversationID - FROM wcf1_conversation_to_user - WHERE participantID IN (?) - GROUP BY conversationID - HAVING COUNT(conversationID) = ? - ) - ) - OR - ( - conversation.userID <> ? - AND conversation.conversationID IN ( - SELECT conversationID - FROM wcf1_conversation_to_user - WHERE participantID IN (?) - AND isInvisible = ? - GROUP BY conversationID - HAVING COUNT(conversationID) = ? - ) - ) - )', [ - // Parameters for the first condition. - WCF::getUser()->userID, - $userIDs, - \count($userIDs), - - // Parameters for the second condition. - WCF::getUser()->userID, - $userIDs, - 0, - \count($userIDs), - ]); - } - } } /** @@ -253,83 +46,46 @@ protected function initObjectList() */ public function readData() { - // if sort field is `username`, `conversation.` has to prepended because `username` - // alone is ambiguous - if ($this->sortField === 'username') { - $this->sortField = 'conversation.username'; - } - parent::readData(); - // change back to old value - if ($this->sortField === 'conversation.username') { - $this->sortField = 'username'; - } - - if ($this->filter != '') { - // `-1` = pseudo object id to have to pages with identifier `com.woltlab.wcf.conversation.ConversationList` - PageLocationManager::getInstance()->addParentLocation('com.woltlab.wcf.conversation.ConversationList', -1); - } - - // read stats - if (!$this->labelID && empty($this->participants)) { - switch ($this->filter) { - case '': - $this->conversationCount = $this->items; - break; - - case 'draft': - $this->draftCount = $this->items; - break; - - case 'hidden': - $this->hiddenCount = $this->items; - break; - - case 'outbox': - $this->outboxCount = $this->items; - break; - } - } + $this->conversationCount = $this->getConversationCount(''); + $this->draftCount = $this->getConversationCount('draft'); + $this->hiddenCount = $this->getConversationCount('hidden'); + $this->outboxCount = $this->getConversationCount('outbox'); + } - if ($this->filter != '' || $this->labelID || !empty($this->participants)) { - $conversationList = new UserConversationList(WCF::getUser()->userID, ''); - $this->conversationCount = $conversationList->countObjects(); - } - if ($this->filter != 'draft' || $this->labelID || !empty($this->participants)) { - $conversationList = new UserConversationList(WCF::getUser()->userID, 'draft'); - $this->draftCount = $conversationList->countObjects(); - } - if ($this->filter != 'hidden' || $this->labelID || !empty($this->participants)) { - $conversationList = new UserConversationList(WCF::getUser()->userID, 'hidden'); - $this->hiddenCount = $conversationList->countObjects(); - } - if ($this->filter != 'outbox' || $this->labelID || !empty($this->participants)) { - $conversationList = new UserConversationList(WCF::getUser()->userID, 'outbox'); - $this->outboxCount = $conversationList->countObjects(); - } + private function getConversationCount(string $filter): int + { + return (new UserConversationList(WCF::getUser()->userID, $filter))->countObjects(); } - /** - * @inheritDoc - */ + #[\Override] public function assignVariables() { parent::assignVariables(); WCF::getTPL()->assign([ 'filter' => $this->filter, - 'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems( - ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation') - ), - 'labelID' => $this->labelID, - 'labelList' => $this->labelList, 'conversationCount' => $this->conversationCount, 'draftCount' => $this->draftCount, 'hiddenCount' => $this->hiddenCount, 'outboxCount' => $this->outboxCount, - 'participants' => $this->participants, - 'validSortFields' => $this->validSortFields, ]); } + + #[\Override] + protected function createListView(): ConversationListView + { + return new ConversationListView($this->filter); + } + + #[\Override] + protected function initListView(): void + { + parent::initListView(); + + $this->listView->setBaseUrl(LinkHandler::getInstance()->getControllerLink(static::class, [ + 'filter' => $this->filter, + ])); + } } diff --git a/files/lib/page/ConversationPage.class.php b/files/lib/page/ConversationPage.class.php index 931be5f3..a3245d52 100644 --- a/files/lib/page/ConversationPage.class.php +++ b/files/lib/page/ConversationPage.class.php @@ -17,6 +17,8 @@ use wcf\system\bbcode\BBCodeHandler; use wcf\system\exception\IllegalLinkException; use wcf\system\exception\PermissionDeniedException; +use wcf\system\interaction\StandaloneInteractionContextMenuComponent; +use wcf\system\interaction\user\ConversationInteractions; use wcf\system\message\quote\MessageQuoteManager; use wcf\system\page\PageLocationManager; use wcf\system\page\ParentPageLocation; @@ -355,6 +357,13 @@ public function assignVariables() 'conversationID' => $this->conversationID, 'participants' => $this->participantList->getObjects(), 'defaultSmilies' => SmileyCache::getInstance()->getCategorySmilies(), + 'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentInteractionButton( + new ConversationInteractions(), + $this->conversation, + LinkHandler::getInstance()->getControllerLink(ConversationListPage::class), + WCF::getLanguage()->getDynamicVariable('wcf.conversation.edit.conversation'), + "core/conversations/{$this->conversationID}/content-header-title" + ), ]); BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode( diff --git a/files/lib/system/cache/runtime/UserConversationRuntimeCache.class.php b/files/lib/system/cache/runtime/UserConversationRuntimeCache.class.php index 34e6ed2c..a99619aa 100644 --- a/files/lib/system/cache/runtime/UserConversationRuntimeCache.class.php +++ b/files/lib/system/cache/runtime/UserConversationRuntimeCache.class.php @@ -2,8 +2,8 @@ namespace wcf\system\cache\runtime; -use wcf\data\conversation\Conversation; use wcf\data\conversation\UserConversationList; +use wcf\data\conversation\ViewableConversation; use wcf\system\WCF; /** @@ -14,7 +14,7 @@ * @license GNU Lesser General Public License * @since 3.0 * - * @extends AbstractRuntimeCache + * @extends AbstractRuntimeCache */ class UserConversationRuntimeCache extends AbstractRuntimeCache { diff --git a/files/lib/system/clipboard/action/ConversationClipboardAction.class.php b/files/lib/system/clipboard/action/ConversationClipboardAction.class.php deleted file mode 100644 index d5add99b..00000000 --- a/files/lib/system/clipboard/action/ConversationClipboardAction.class.php +++ /dev/null @@ -1,307 +0,0 @@ - - * - * @extends AbstractClipboardAction - */ -class ConversationClipboardAction extends AbstractClipboardAction -{ - /** - * @inheritDoc - */ - protected $actionClassActions = ['close', 'markAsRead', 'open']; - - /** - * list of conversations - * @var Conversation[] - */ - public $conversations; - - /** - * @inheritDoc - */ - protected $supportedActions = [ - 'assignLabel', - 'close', - 'leave', - 'leavePermanently', - 'markAsRead', - 'open', - 'restore', - ]; - - /** - * @inheritDoc - */ - public function execute(array $objects, ClipboardAction $action) - { - if ($this->conversations === null) { - // validate conversations - $this->validateParticipation($objects); - } - - // check if no conversation was accessible - if (empty($this->conversations)) { - return null; - } - - $item = parent::execute($objects, $action); - - if ($item === null) { - return null; - } - - switch ($action->actionName) { - case 'assignLabel': - // check if user has labels - $sql = "SELECT COUNT(*) AS count - FROM wcf1_conversation_label - WHERE userID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([WCF::getUser()->userID]); - $row = $statement->fetchArray(); - if ($row['count'] == 0) { - return null; - } - - $item->addParameter('objectIDs', \array_keys($this->conversations)); - break; - - case 'leave': - $item->addInternalData('parameters', ['hideConversation' => 1]); - $item->addParameter('actionName', 'hideConversation'); - $item->addParameter('className', $this->getClassName()); - break; - - case 'leavePermanently': - $item->addParameter('objectIDs', \array_keys($this->conversations)); - $item->addInternalData('parameters', ['hideConversation' => 2]); - $item->addParameter('actionName', 'hideConversation'); - $item->addParameter('className', $this->getClassName()); - break; - - case 'markAsRead': - $item->addParameter('objectIDs', \array_keys($this->conversations)); - $item->addParameter('actionName', 'markAsRead'); - $item->addParameter('className', $this->getClassName()); - $item->addInternalData( - 'confirmMessage', - WCF::getLanguage()->getDynamicVariable( - 'wcf.clipboard.item.com.woltlab.wcf.conversation.conversation.markAsRead.confirmMessage', - [ - 'count' => $item->getCount(), - ] - ) - ); - break; - - case 'restore': - $item->addInternalData('parameters', ['hideConversation' => 0]); - $item->addParameter('actionName', 'hideConversation'); - $item->addParameter('className', $this->getClassName()); - break; - } - - return $item; - } - - /** - * @inheritDoc - */ - public function getClassName() - { - return ConversationAction::class; - } - - /** - * @inheritDoc - */ - public function getTypeName() - { - return 'com.woltlab.wcf.conversation.conversation'; - } - - /** - * Returns a list of conversations with user participation. - * - * @param Conversation[] $conversations - * @return void - */ - protected function validateParticipation(array $conversations) - { - $conversationIDs = []; - - // validate ownership - foreach ($conversations as $conversation) { - if ($conversation->userID != WCF::getUser()->userID) { - $conversationIDs[] = $conversation->conversationID; - } - } - - // validate participation as non-owner - if (!empty($conversationIDs)) { - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID IN (?)", [$conversationIDs]); - $conditions->add("participantID = ?", [WCF::getUser()->userID]); - - $sql = "SELECT conversationID - FROM wcf1_conversation_to_user - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - while ($row = $statement->fetchArray()) { - $index = \array_search($row['conversationID'], $conversationIDs); - unset($conversationIDs[$index]); - } - - // remove unaccessible conversations - if (!empty($conversationIDs)) { - foreach ($conversations as $index => $conversation) { - if (\in_array($conversation->conversationID, $conversationIDs)) { - unset($conversations[$index]); - } - } - } - } - - foreach ($conversations as $conversation) { - $this->conversations[$conversation->conversationID] = $conversation; - } - } - - /** - * Validates if user may close the given conversations. - * - * @return int[] - */ - protected function validateClose() - { - $conversationIDs = []; - - foreach ($this->conversations as $conversation) { - if (!$conversation->isClosed && $conversation->userID == WCF::getUser()->userID) { - $conversationIDs[] = $conversation->conversationID; - } - } - - return $conversationIDs; - } - - /** - * Validates conversations available for leaving. - * - * @return int[] - */ - public function validateLeave() - { - $tmpIDs = []; - foreach ($this->conversations as $conversation) { - $tmpIDs[] = $conversation->conversationID; - } - - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID IN (?)", [$tmpIDs]); - $conditions->add("participantID = ?", [WCF::getUser()->userID]); - $conditions->add("hideConversation <> ?", [1]); - - $sql = "SELECT conversationID - FROM wcf1_conversation_to_user - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - - return $statement->fetchAll(\PDO::FETCH_COLUMN); - } - - /** - * Validates conversations applicable for mark as read. - * - * @return int[] - */ - public function validateMarkAsRead() - { - $conversationIDs = []; - - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID IN (?)", [\array_keys($this->conversations)]); - $conditions->add("participantID = ?", [WCF::getUser()->userID]); - - $sql = "SELECT conversationID, lastVisitTime - FROM wcf1_conversation_to_user - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - $lastVisitTime = []; - while ($row = $statement->fetchArray()) { - $lastVisitTime[$row['conversationID']] = $row['lastVisitTime']; - } - - foreach ($this->conversations as $conversation) { - if ( - isset($lastVisitTime[$conversation->conversationID]) - && $lastVisitTime[$conversation->conversationID] < $conversation->lastPostTime - ) { - $conversationIDs[] = $conversation->conversationID; - } - } - - return $conversationIDs; - } - - /** - * Validates if user may open the given conversations. - * - * @return int[] - */ - protected function validateOpen() - { - $conversationIDs = []; - - foreach ($this->conversations as $conversation) { - if ($conversation->isClosed && $conversation->userID == WCF::getUser()->userID) { - $conversationIDs[] = $conversation->conversationID; - } - } - - return $conversationIDs; - } - - /** - * Validates conversations available for restore. - * - * @return int[] - */ - public function validateRestore() - { - $tmpIDs = []; - foreach ($this->conversations as $conversation) { - $tmpIDs[] = $conversation->conversationID; - } - - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID IN (?)", [$tmpIDs]); - $conditions->add("participantID = ?", [WCF::getUser()->userID]); - $conditions->add("hideConversation <> ?", [0]); - - $sql = "SELECT conversationID - FROM wcf1_conversation_to_user - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - - return $statement->fetchAll(\PDO::FETCH_COLUMN); - } -} diff --git a/files/lib/system/conversation/TConversationForm.class.php b/files/lib/system/conversation/TConversationForm.class.php new file mode 100644 index 00000000..20a7d61a --- /dev/null +++ b/files/lib/system/conversation/TConversationForm.class.php @@ -0,0 +1,142 @@ + + * @since 6.2 + */ +trait TConversationForm +{ + /** + * Returns the user IDs of the users that are in the given groups. + * + * @param int[] $groupIDs + * + * @return int[] + */ + protected function getUserByGroups(array $groupIDs): array + { + if ($groupIDs === []) { + return []; + } + + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('groupID IN (?)', [$groupIDs]); + $sql = "SELECT DISTINCT userID + FROM wcf1_user_to_group + " . $conditionBuilder; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + + $userIDs = []; + while ($userID = $statement->fetchColumn()) { + $userIDs[] = $userID; + } + + return $userIDs; + } + + /** + * Returns a validator that checks if the selected participants are valid. + */ + protected function getParticipantsValidator(): FormFieldValidator + { + return new FormFieldValidator('participantsValidator', static function (UserFormField $formField) { + $users = $formField->getUsers(); + $userIDs = \array_column($users, 'userID'); + + UserStorageHandler::getInstance()->loadStorage($userIDs); + + foreach ($users as $user) { + try { + if ($user->userID === WCF::getUser()->userID) { + throw new UserInputException('isAuthor'); + } + + Conversation::validateParticipant($user, $formField->getId()); + } catch (UserInputException $e) { + $formField->addValidationError( + new FormFieldValidationError( + $e->getType(), + 'wcf.conversation.participants.error.' . $e->getType(), + [ + 'username' => $user->username, + ] + ) + ); + } + } + }); + } + + /** + * Returns a validator that checks if the maximum number of participants is not exceeded. + */ + protected function getMaximumParticipantsValidator( + string $invisibleParticipantsFieldId = 'invisibleParticipants', + string $participantGroupsFieldId = 'participantGroups', + ?string $invisibleParticipantGroupsFieldId = 'invisibleParticipantGroups' + ): FormFieldValidator { + return new FormFieldValidator( + 'participantsMaximumValidator', + function (UserFormField $formField) use ( + $invisibleParticipantsFieldId, + $participantGroupsFieldId, + $invisibleParticipantGroupsFieldId + ) { + $invisibleParticipantsFormField = $formField->getDocument() + ->getNodeById($invisibleParticipantsFieldId); + $participantGroupsFormField = $formField->getDocument() + ->getNodeById($participantGroupsFieldId); + $isDraftFormField = $formField->getDocument()->getNodeById('isDraft'); + $invisibleParticipantGroupsFormField = $invisibleParticipantGroupsFieldId !== null ? $formField->getDocument() + ->getNodeById($invisibleParticipantGroupsFieldId) : null; + + \assert($invisibleParticipantsFormField === null || $invisibleParticipantsFormField instanceof UserFormField); + \assert($isDraftFormField === null || $isDraftFormField instanceof BooleanFormField); + \assert($participantGroupsFormField === null || $participantGroupsFormField instanceof MultipleSelectionFormField); + \assert($invisibleParticipantGroupsFormField === null || $invisibleParticipantGroupsFormField instanceof MultipleSelectionFormField); + + $groupIDs = \array_merge( + $participantGroupsFormField?->getValue() ?: [], + $invisibleParticipantGroupsFormField?->getValue() ?: [], + ); + + $userIDs = \array_merge( + \array_column($formField->getUsers(), 'userID'), + \array_column($invisibleParticipantsFormField?->getUsers() ?: [], 'userID'), + $this->getUserByGroups($groupIDs) + ); + + if (\count($userIDs) > WCF::getSession()->getPermission('user.conversation.maxParticipants')) { + $formField->addValidationError( + new FormFieldValidationError( + 'tooManyParticipants', + 'wcf.conversation.participants.error.tooManyParticipants' + ) + ); + } + + if (!$isDraftFormField?->getValue() && $userIDs === []) { + $formField->addValidationError(new FormFieldValidationError('empty')); + } + } + ); + } +} diff --git a/files/lib/system/conversation/command/AddParticipantConversation.class.php b/files/lib/system/conversation/command/AddParticipantConversation.class.php new file mode 100644 index 00000000..f36f9210 --- /dev/null +++ b/files/lib/system/conversation/command/AddParticipantConversation.class.php @@ -0,0 +1,58 @@ + + * @since 6.2 + */ +final class AddParticipantConversation +{ + public function __construct( + public readonly Conversation $conversation, + /** + * @var int[] + */ + public readonly array $participants, + /** + * @var 'new'|'all' + */ + public readonly ?string $messageVisibility + ) { + } + + public function __invoke(): void + { + if ($this->participants === []) { + return; + } + + if ($this->conversation->isDraft) { + $draftData = \unserialize($this->conversation->draftData); + $draftData['participants'] = \array_merge($draftData['participants'], $this->participants); + $data = ['data' => ['draftData' => \serialize($draftData)]]; + } else { + $data = [ + 'participants' => $this->participants, + 'visibility' => $this->messageVisibility, + ]; + } + + (new ConversationAction([$this->conversation], 'update', $data))->executeAction(); + + ConversationModificationLogHandler::getInstance()->addParticipants($this->conversation, $this->participants); + + if (!$this->conversation->isDraft) { + (new ConversationEditor($this->conversation))->updateParticipantSummary(); + } + } +} diff --git a/files/lib/system/conversation/command/AssignConversationLabel.class.php b/files/lib/system/conversation/command/AssignConversationLabel.class.php new file mode 100644 index 00000000..63a08291 --- /dev/null +++ b/files/lib/system/conversation/command/AssignConversationLabel.class.php @@ -0,0 +1,80 @@ + + * @since 6.2 + */ +final class AssignConversationLabel +{ + public function __construct( + public readonly ConversationLabelList $labelList, + /** + * @var int[] + */ + public readonly array $conversationIDs, + /** + * @var int[] + */ + public readonly array $labelIDs + ) { + } + + public function __invoke(): void + { + $this->removeOldLabels($this->conversationIDs, $this->labelList->getObjectIDs()); + $this->assignLabels($this->conversationIDs, $this->labelIDs); + } + + /** + * @param int[] $conversationIDs + * @param int[] $labelIDs + */ + private function removeOldLabels(array $conversationIDs, array $labelIDs): void + { + if ($labelIDs === []) { + return; + } + + $conditions = new PreparedStatementConditionBuilder(); + $conditions->add("conversationID IN (?)", [$conversationIDs]); + $conditions->add("labelID IN (?)", [$labelIDs]); + + $sql = "DELETE FROM wcf1_conversation_label_to_object + " . $conditions; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditions->getParameters()); + } + + /** + * @param int[] $conversationIDs + * @param int[] $labelIDs + */ + private function assignLabels(array $conversationIDs, array $labelIDs): void + { + if ($labelIDs === []) { + return; + } + + $sql = "INSERT INTO wcf1_conversation_label_to_object + (labelID, conversationID) + VALUES (?, ?)"; + $statement = WCF::getDB()->prepare($sql); + + foreach ($labelIDs as $labelID) { + foreach ($conversationIDs as $conversationID) { + $statement->execute([ + $labelID, + $conversationID, + ]); + } + } + } +} diff --git a/files/lib/system/conversation/command/RemoveConversationParticipant.class.php b/files/lib/system/conversation/command/RemoveConversationParticipant.class.php new file mode 100644 index 00000000..a3336f2f --- /dev/null +++ b/files/lib/system/conversation/command/RemoveConversationParticipant.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +final class RemoveConversationParticipant +{ + public function __construct( + public readonly Conversation $conversation, + public readonly int $participantID, + ) { + } + + public function __invoke(): void + { + $editor = new ConversationEditor($this->conversation); + $editor->removeParticipant($this->participantID); + $editor->updateParticipantSummary(); + + $userConversation = Conversation::getUserConversation( + $this->conversation->conversationID, + $this->participantID + ); + + if (!$userConversation->isInvisible) { + ConversationModificationLogHandler::getInstance()->removeParticipant($this->conversation, $this->participantID); + } + + UserStorageHandler::getInstance()->reset([$this->participantID], 'unreadConversationCount'); + } +} diff --git a/files/lib/system/conversation/command/SetConversationSubject.class.php b/files/lib/system/conversation/command/SetConversationSubject.class.php new file mode 100644 index 00000000..048f35f5 --- /dev/null +++ b/files/lib/system/conversation/command/SetConversationSubject.class.php @@ -0,0 +1,42 @@ + + * @since 6.2 + */ +final class SetConversationSubject +{ + public function __construct( + public readonly Conversation $conversation, + public readonly string $subject, + ) { + } + + public function __invoke(): void + { + $editor = new ConversationEditor($this->conversation); + $editor->update([ + 'subject' => $this->subject, + ]); + + $message = $this->conversation->getFirstMessage(); + + SearchIndexManager::getInstance()->set( + 'com.woltlab.wcf.conversation.message', + $message->messageID, + $message->message, + $this->subject, + $message->time, + $message->userID, + $message->username + ); + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/AssignConversationLabels.class.php b/files/lib/system/endpoint/controller/core/conversations/AssignConversationLabels.class.php deleted file mode 100644 index f983ad67..00000000 --- a/files/lib/system/endpoint/controller/core/conversations/AssignConversationLabels.class.php +++ /dev/null @@ -1,128 +0,0 @@ - - * @since 6.2 - */ -#[PostRequest('/core/conversations/assign-labels')] -final class AssignConversationLabels implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - $parameters = Helper::mapApiParameters($request, AssignConversationLabelsParameters::class); - $conversationIDs = $parameters->conversationIDs; - - if ($conversationIDs === []) { - throw new IllegalLinkException(); - } - if (!Conversation::isParticipant($conversationIDs)) { - throw new PermissionDeniedException(); - } - - $labelIDs = $parameters->labelIDs; - - $labelList = ConversationLabel::getLabelsByUser(); - if (!\count($labelList)) { - throw new IllegalLinkException(); - } - - foreach ($labelIDs as $labelID) { - if (!\in_array($labelID, $labelList->getObjectIDs())) { - throw new PermissionDeniedException(); - } - } - - $this->removeOldLabels($labelList, $conversationIDs); - $this->assignLabels($conversationIDs, $labelIDs); - - ClipboardHandler::getInstance()->unmark( - $conversationIDs, - ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation') - ); - - return new JsonResponse([]); - } - - /** - * @param int[] $conversationIDs - */ - private function removeOldLabels(ConversationLabelList $labelList, array $conversationIDs): void - { - // remove previous labels (if any) - $labelIDs = []; - foreach ($labelList as $label) { - $labelIDs[] = $label->labelID; - } - - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID IN (?)", [$conversationIDs]); - $conditions->add("labelID IN (?)", [$labelIDs]); - - $sql = "DELETE FROM wcf1_conversation_label_to_object - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - } - - /** - * @param int[] $conversationIDs - * @param int[] $labelIDs - */ - private function assignLabels(array $conversationIDs, array $labelIDs): void - { - if ($labelIDs === []) { - return; - } - - // assign label ids - $sql = "INSERT INTO wcf1_conversation_label_to_object - (labelID, conversationID) - VALUES (?, ?)"; - $statement = WCF::getDB()->prepare($sql); - - WCF::getDB()->beginTransaction(); - foreach ($labelIDs as $labelID) { - foreach ($conversationIDs as $conversationID) { - $statement->execute([ - $labelID, - $conversationID, - ]); - } - } - WCF::getDB()->commitTransaction(); - } -} - -/** @internal */ -final class AssignConversationLabelsParameters -{ - public function __construct( - /** @var array * */ - public readonly array $conversationIDs, - /** @var array * */ - public readonly array $labelIDs - ) { - } -} diff --git a/files/lib/system/endpoint/controller/core/conversations/CloseConversation.class.php b/files/lib/system/endpoint/controller/core/conversations/CloseConversation.class.php new file mode 100644 index 00000000..5653c148 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/CloseConversation.class.php @@ -0,0 +1,59 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/{id:\d+}/close')] +final class CloseConversation implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); + $this->assertConversationCanClosed($conversation); + + if (!$conversation->isClosed) { + $this->closeConversation($conversation); + } + + return new JsonResponse([]); + } + + private function assertConversationCanClosed(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + + if ($conversation->userID !== WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + } + + private function closeConversation(Conversation $conversation): void + { + $editor = new ConversationEditor($conversation); + $editor->update(['isClosed' => 1]); + + ConversationModificationLogHandler::getInstance()->close($conversation); + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/GetConversationHeaderTitle.class.php b/files/lib/system/endpoint/controller/core/conversations/GetConversationHeaderTitle.class.php new file mode 100644 index 00000000..7bad60bd --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/GetConversationHeaderTitle.class.php @@ -0,0 +1,50 @@ + + * @since 6.2 + */ +#[GetRequest('/core/conversations/{id:\d+}/content-header-title')] +final class GetConversationHeaderTitle implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = UserConversationRuntimeCache::getInstance()->getObject(\intval($variables['id'])); + if ($conversation === null) { + throw new IllegalLinkException(); + } + + $this->assertConversationIsAccessible($conversation); + + return new JsonResponse([ + 'template' => WCF::getTPL()->render('wcf', 'conversationContentHeaderTitle', [ + 'conversation' => $conversation, + ]), + ]); + } + + private function assertConversationIsAccessible(ViewableConversation $conversation): void + { + if (!$conversation->isActiveParticipant()) { + throw new PermissionDeniedException(); + } + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/GetConversationLabelManager.class.php b/files/lib/system/endpoint/controller/core/conversations/GetConversationLabelManager.class.php deleted file mode 100644 index d407a595..00000000 --- a/files/lib/system/endpoint/controller/core/conversations/GetConversationLabelManager.class.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @since 6.2 - */ -#[GetRequest('/core/conversations/label-manager')] -final class GetConversationLabelManager implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - if (!WCF::getUser()->userID) { - throw new PermissionDeniedException(); - } - - if (!WCF::getSession()->getPermission('user.conversation.canUseConversation')) { - throw new PermissionDeniedException(); - } - - return new JsonResponse([ - 'template' => WCF::getTPL()->render('wcf', 'conversationLabelManagement', [ - 'cssClassNames' => ConversationLabel::getLabelCssClassNames(), - 'labelList' => ConversationLabel::getLabelsByUser(), - ]), - 'maxLabels' => WCF::getSession()->getPermission('user.conversation.maxLabels'), - 'labelCount' => \count(ConversationLabel::getLabelsByUser()), - ]); - } -} diff --git a/files/lib/system/endpoint/controller/core/conversations/GetConversationLabels.class.php b/files/lib/system/endpoint/controller/core/conversations/GetConversationLabels.class.php deleted file mode 100644 index 1fbd2e6a..00000000 --- a/files/lib/system/endpoint/controller/core/conversations/GetConversationLabels.class.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @since 6.2 - */ -#[GetRequest('/core/conversations/labels')] -final class GetConversationLabels implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - $parameters = Helper::mapApiParameters($request, GetConversationLabelsParameters::class); - $conversationIDs = $parameters->conversationIDs; - - if ($conversationIDs === []) { - throw new IllegalLinkException(); - } - if (!Conversation::isParticipant($conversationIDs)) { - throw new PermissionDeniedException(); - } - - // Get the conversationID if only one conversation is selected - // to preselect the label. - $conversationID = null; - if (\count($conversationIDs) === 1) { - $conversationID = \reset($conversationIDs); - } - - $labelList = ConversationLabel::getLabelsByUser(); - if (!\count($labelList)) { - throw new IllegalLinkException(); - } - $id = "conversationLabel"; - - return new JsonResponse([ - 'formId' => $id, - 'title' => WCF::getLanguage()->get('wcf.conversation.label.assignLabels'), - 'template' => $this->getForm($id, $labelList, $conversationID)->getHtml(), - ]); - } - - private function getForm(string $id, ConversationLabelList $labelList, ?int $conversationID): DialogFormDocument - { - return DialogFormDocument::create($id) - ->ajax() - ->prefix($id) - ->appendChildren([ - MultipleSelectionFormField::create('labelIDs') - ->options( - \array_map(static function (ConversationLabel $label) { - return \sprintf( - '%s', - empty($label->cssClassName) ? '' : ' ' . $label->cssClassName, - StringUtil::encodeHTML($label->label) - ); - }, $labelList->getObjects()) - ) - ->value($this->getAssignedLabelIDs($labelList->getObjectIDs(), $conversationID)), - ]) - ->addDefaultButton(false) - ->build(); - } - - /** - * @param int[] $labelIDs - * @return int[] - */ - private function getAssignedLabelIDs(array $labelIDs, ?int $conversationID): array - { - if ($conversationID === null) { - return []; - } - - $conditions = new PreparedStatementConditionBuilder(); - $conditions->add("conversationID = ?", [$conversationID]); - $conditions->add("labelID IN (?)", [$labelIDs]); - - $sql = "SELECT labelID - FROM wcf1_conversation_label_to_object - " . $conditions; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditions->getParameters()); - - return $statement->fetchAll(\PDO::FETCH_COLUMN); - } -} - -/** @internal */ -final class GetConversationLabelsParameters -{ - public function __construct( - /** @var array * */ - public readonly array $conversationIDs - ) { - } -} diff --git a/files/lib/system/endpoint/controller/core/conversations/GetConversationLeaveDialog.class.php b/files/lib/system/endpoint/controller/core/conversations/GetConversationLeaveDialog.class.php deleted file mode 100644 index 78e1deba..00000000 --- a/files/lib/system/endpoint/controller/core/conversations/GetConversationLeaveDialog.class.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @since 6.2 - */ -#[GetRequest('/core/conversations/{id:\d+}/leave-dialog')] -final class GetConversationLeaveDialog implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); - - $this->assertConversationIsAccessible($conversation); - - return new JsonResponse([ - 'template' => WCF::getTPL()->render('wcf', 'conversationLeave', [ - 'hideConversation' => $this->isConversationHidden($conversation), - ]) - ]); - } - - private function assertConversationIsAccessible(Conversation $conversation): void - { - if (!Conversation::isParticipant([$conversation->conversationID])) { - throw new PermissionDeniedException(); - } - } - - private function isConversationHidden(Conversation $conversation): bool - { - $sql = "SELECT hideConversation - FROM wcf1_conversation_to_user - WHERE conversationID = ? - AND participantID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([ - $conversation->conversationID, - WCF::getUser()->userID, - ]); - - return \boolval($statement->fetchSingleColumn()); - } -} diff --git a/files/lib/system/endpoint/controller/core/conversations/GetConversationParticipantList.class.php b/files/lib/system/endpoint/controller/core/conversations/GetConversationParticipantList.class.php new file mode 100644 index 00000000..f5f2b3c1 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/GetConversationParticipantList.class.php @@ -0,0 +1,60 @@ + + * @since 6.2 + */ +#[GetRequest('/core/conversations/{conversationId:\d+}/participants')] +final class GetConversationParticipantList implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['conversationId'], Conversation::class); + + $this->assertCanRetrieveParticipantList($conversation); + + return new JsonResponse([ + 'template' => WCF::getTPL()->render('wcf', 'conversationParticipantList', [ + 'conversation' => $conversation, + 'participants' => $this->getParticipantList($conversation), + ]), + ]); + } + + private function assertCanRetrieveParticipantList(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + } + + private function getParticipantList(Conversation $conversation): ConversationParticipantList + { + $participantList = new ConversationParticipantList( + $conversation->conversationID, + WCF::getUser()->userID, + $conversation->userID === WCF::getUser()->userID + ); + $participantList->readObjects(); + + return $participantList; + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/LeaveConversation.class.php b/files/lib/system/endpoint/controller/core/conversations/LeaveConversation.class.php index df9d14a8..c08f7e90 100644 --- a/files/lib/system/endpoint/controller/core/conversations/LeaveConversation.class.php +++ b/files/lib/system/endpoint/controller/core/conversations/LeaveConversation.class.php @@ -10,7 +10,6 @@ use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; use wcf\system\exception\PermissionDeniedException; -use wcf\system\request\LinkHandler; /** * API endpoint for leaving a conversation. @@ -29,14 +28,9 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); $this->assertConversationIsAccessible($conversation); - $parameters = Helper::mapApiParameters($request, LeaveConversationParameters::class); - $hideConversation = $parameters->hideConversation; + (new \wcf\system\conversation\command\LeaveConversation([$conversation->conversationID], Conversation::STATE_HIDDEN))(); - (new \wcf\system\conversation\command\LeaveConversation([$conversation->conversationID], $hideConversation))(); - - return new JsonResponse([ - 'redirectUrl' => LinkHandler::getInstance()->getLink('ConversationList'), - ]); + return new JsonResponse([]); } private function assertConversationIsAccessible(Conversation $conversation): void @@ -46,15 +40,3 @@ private function assertConversationIsAccessible(Conversation $conversation): voi } } } - -// @codingStandardsIgnoreStart -/** @internal */ -final class LeaveConversationParameters -{ - public function __construct( - /** @var Conversation::STATE_DEFAULT|Conversation::STATE_HIDDEN|Conversation::STATE_LEFT */ - public readonly int $hideConversation, - ) { - } -} -// @codingStandardsIgnoreEnd diff --git a/files/lib/system/endpoint/controller/core/conversations/LeavePermanentlyConversation.class.php b/files/lib/system/endpoint/controller/core/conversations/LeavePermanentlyConversation.class.php new file mode 100644 index 00000000..731aa391 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/LeavePermanentlyConversation.class.php @@ -0,0 +1,42 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/{id:\d+}/leave-permanently')] +final class LeavePermanentlyConversation implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); + $this->assertConversationIsAccessible($conversation); + + (new \wcf\system\conversation\command\LeaveConversation([$conversation->conversationID], Conversation::STATE_LEFT))(); + + return new JsonResponse([]); + } + + private function assertConversationIsAccessible(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/OpenConversation.class.php b/files/lib/system/endpoint/controller/core/conversations/OpenConversation.class.php new file mode 100644 index 00000000..c1d2c0f6 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/OpenConversation.class.php @@ -0,0 +1,59 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/{id:\d+}/open')] +final class OpenConversation implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); + $this->assertConversationCanOpened($conversation); + + if ($conversation->isClosed) { + $this->openConversation($conversation); + } + + return new JsonResponse([]); + } + + private function assertConversationCanOpened(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + + if ($conversation->userID !== WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + } + + private function openConversation(Conversation $conversation): void + { + $editor = new ConversationEditor($conversation); + $editor->update(['isClosed' => 0]); + + ConversationModificationLogHandler::getInstance()->open($conversation); + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/RemoveConversationParticipant.class.php b/files/lib/system/endpoint/controller/core/conversations/RemoveConversationParticipant.class.php new file mode 100644 index 00000000..2a84166e --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/RemoveConversationParticipant.class.php @@ -0,0 +1,77 @@ + + * @since 6.2 + */ +#[DeleteRequest('/core/conversations/{conversationId:\d+}/participants/{participantId:\d+}')] +final class RemoveConversationParticipant implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['conversationId'], Conversation::class); + $participantUserID = \intval($variables['participantId']); + + $this->assertCanRemoveParticipant($conversation, $participantUserID); + + (new \wcf\system\conversation\command\RemoveConversationParticipant($conversation, $participantUserID))(); + + return new JsonResponse([ + 'template' => WCF::getTPL()->render('wcf', 'conversationParticipantList', [ + 'conversation' => $conversation, + 'participants' => $this->getParticipantList($conversation), + ]), + ]); + } + + private function assertCanRemoveParticipant(Conversation $conversation, int $participantUserID): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + + if ($conversation->userID !== WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + + if ($participantUserID === WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + + $participantUserIDs = $conversation->getParticipantIDs(true); + if (!\in_array($participantUserID, $participantUserIDs)) { + throw new IllegalLinkException(); + } + } + + private function getParticipantList(Conversation $conversation): ConversationParticipantList + { + $participantList = new ConversationParticipantList( + $conversation->conversationID, + WCF::getUser()->userID, + $conversation->userID === WCF::getUser()->userID + ); + $participantList->readObjects(); + + return $participantList; + } +} diff --git a/files/lib/system/endpoint/controller/core/conversations/RestoreConversation.class.php b/files/lib/system/endpoint/controller/core/conversations/RestoreConversation.class.php new file mode 100644 index 00000000..de62d829 --- /dev/null +++ b/files/lib/system/endpoint/controller/core/conversations/RestoreConversation.class.php @@ -0,0 +1,42 @@ + + * @since 6.2 + */ +#[PostRequest('/core/conversations/{id:\d+}/restore')] +final class RestoreConversation implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $conversation = Helper::fetchObjectFromRequestParameter($variables['id'], Conversation::class); + $this->assertConversationIsAccessible($conversation); + + (new \wcf\system\conversation\command\LeaveConversation([$conversation->conversationID], Conversation::STATE_DEFAULT))(); + + return new JsonResponse([]); + } + + private function assertConversationIsAccessible(Conversation $conversation): void + { + if (!Conversation::isParticipant([$conversation->conversationID])) { + throw new PermissionDeniedException(); + } + } +} diff --git a/files/lib/system/form/builder/field/ConversationLabelFormField.class.php b/files/lib/system/form/builder/field/ConversationLabelFormField.class.php new file mode 100644 index 00000000..47c045be --- /dev/null +++ b/files/lib/system/form/builder/field/ConversationLabelFormField.class.php @@ -0,0 +1,75 @@ + + * @since 6.2 + */ +final class ConversationLabelFormField extends AbstractFormField +{ + /** + * @var ConversationLabel[] + */ + public array $labels = []; + + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; + + /** + * @inheritDoc + */ + protected $templateName = 'shared_conversationLabelFormField'; + + /** + * @param ConversationLabel[] $labels + */ + public function labels(array $labels): static + { + $this->labels = $labels; + + return $this; + } + + /** + * @return ConversationLabel[] + */ + public function getLabels(): array + { + return $this->labels; + } + + #[\Override] + public function readValue() + { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $this->value = \intval($this->getDocument()->getRequestData($this->getPrefixedId())); + } + + return $this; + } + + #[\Override] + public function validate() + { + if ($this->isRequired()) { + if ($this->value <= 0) { + $this->addValidationError(new FormFieldValidationError('empty')); + } + } elseif ($this->value > 0 && !\array_key_exists($this->value, $this->labels)) { + $this->addValidationError(new FormFieldValidationError( + 'invalidValue', + 'wcf.global.form.error.noValidSelection' + )); + } + } +} diff --git a/files/lib/system/interaction/bulk/user/ConversationBulkInteractions.class.php b/files/lib/system/interaction/bulk/user/ConversationBulkInteractions.class.php new file mode 100644 index 00000000..0862d776 --- /dev/null +++ b/files/lib/system/interaction/bulk/user/ConversationBulkInteractions.class.php @@ -0,0 +1,85 @@ + + * @since 6.2 + */ +final class ConversationBulkInteractions extends AbstractBulkInteractionProvider +{ + public function __construct() + { + $labelList = ConversationLabel::getLabelsByUser(); + + $this->addInteractions([ + new BulkRpcInteraction( + 'open', + 'core/conversations/%s/open', + 'wcf.conversation.edit.open', + isAvailableCallback: static fn (ViewableConversation $conversation) => $conversation->isClosed && $conversation->userID === WCF::getUser()->userID + ), + new BulkRpcInteraction( + 'close', + 'core/conversations/%s/close', + 'wcf.conversation.edit.close', + isAvailableCallback: static fn (ViewableConversation $conversation) => !$conversation->isClosed && $conversation->userID === WCF::getUser()->userID + ), + new BulkFormBuilderDialogInteraction( + 'assignLabel', + AssignConversationLabelDialogAction::class, + 'wcf.conversation.edit.assignLabel', + static fn () => $labelList->count() > 0, + ), + new BulkRpcInteraction( + 'restore', + 'core/conversations/%s/restore', + 'wcf.conversation.hideConversation.restore', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.restore.confirmationMessage', + static fn (ViewableConversation $conversation) => (bool)$conversation->hideConversation + ), + new BulkRpcInteraction( + 'leave', + 'core/conversations/%s/leave', + 'wcf.conversation.hideConversation.leave', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.leave.confirmationMessage', + static fn (ViewableConversation $conversation) => !$conversation->hideConversation + ), + new BulkRpcInteraction( + 'leave-permanently', + 'core/conversations/%s/leave-permanently', + 'wcf.conversation.hideConversation.leavePermanently', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.leavePermanently.confirmationMessage', + ), + ]); + + EventHandler::getInstance()->fire( + new ConversationBulkInteractionCollecting($this) + ); + } + + #[\Override] + public function getObjectListClassName(): string + { + return UserConversationList::class; + } +} diff --git a/files/lib/system/interaction/user/ConversationInteractions.class.php b/files/lib/system/interaction/user/ConversationInteractions.class.php new file mode 100644 index 00000000..7581620c --- /dev/null +++ b/files/lib/system/interaction/user/ConversationInteractions.class.php @@ -0,0 +1,125 @@ + + * @since 6.2 + */ +final class ConversationInteractions extends AbstractInteractionProvider +{ + public function __construct() + { + $labelList = ConversationLabel::getLabelsByUser(); + + $this->addInteractions([ + new FormBuilderDialogInteraction( + 'editSubject', + LinkHandler::getInstance()->getControllerLink(EditSubjectConversationDialogAction::class, ['id' => '%s']), + 'wcf.conversation.edit.subject', + static fn (ViewableConversation|Conversation $conversation) => WCF::getUser()->userID === $conversation->userID, + ), + new RpcInteraction( + 'open', + 'core/conversations/%s/open', + 'wcf.conversation.edit.open', + isAvailableCallback: static fn (ViewableConversation|Conversation $conversation) => $conversation->isClosed && $conversation->userID === WCF::getUser()->userID + ), + new RpcInteraction( + 'close', + 'core/conversations/%s/close', + 'wcf.conversation.edit.close', + isAvailableCallback: static fn (ViewableConversation|Conversation $conversation) => !$conversation->isClosed && $conversation->userID === WCF::getUser()->userID + ), + new FormBuilderDialogInteraction( + 'assignLabel', + LinkHandler::getInstance()->getControllerLink(AssignConversationLabelDialogAction::class, ['id' => '%s']), + 'wcf.conversation.edit.assignLabel', + static fn () => $labelList->count() > 0, + ), + new Divider(), + new FormBuilderDialogInteraction( + 'addParticipants', + LinkHandler::getInstance()->getControllerLink(AddParticipantConversationDialogAction::class, ['id' => '%s']), + 'wcf.conversation.edit.addParticipants', + static fn (ViewableConversation|Conversation $conversation) => $conversation->canAddParticipants(), + ), + new RpcInteraction( + 'restore', + 'core/conversations/%s/restore', + 'wcf.conversation.hideConversation.restore', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.restore.confirmationMessage', + static function (ViewableConversation|Conversation $conversation) { + if (!($conversation instanceof ViewableConversation)) { + $conversation = UserConversationRuntimeCache::getInstance()->getObject($conversation->conversationID); + } + + return (bool)$conversation->hideConversation; + }, + ), + new RpcInteraction( + 'leave', + 'core/conversations/%s/leave', + 'wcf.conversation.hideConversation.leave', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.leave.confirmationMessage', + static function (ViewableConversation|Conversation $conversation) { + if (!($conversation instanceof ViewableConversation)) { + $conversation = UserConversationRuntimeCache::getInstance()->getObject($conversation->conversationID); + } + + return !$conversation->hideConversation; + }, + ), + new RpcInteraction( + 'leave-permanently', + 'core/conversations/%s/leave-permanently', + 'wcf.conversation.hideConversation.leavePermanently', + InteractionConfirmationType::Custom, + 'wcf.conversation.hideConversation.leavePermanently.confirmationMessage', + interactionEffect: InteractionEffect::RemoveItem, + ), + new EditInteraction( + ConversationDraftEditForm::class, + static function (ViewableConversation|Conversation $conversation) { + return $conversation->isDraft; + } + ), + ]); + + EventHandler::getInstance()->fire( + new ConversationInteractionCollecting($this) + ); + } + + #[\Override] + public function getObjectClassName(): string + { + return Conversation::class; + } +} diff --git a/files/lib/system/listView/user/ConversationListView.class.php b/files/lib/system/listView/user/ConversationListView.class.php new file mode 100644 index 00000000..832866c6 --- /dev/null +++ b/files/lib/system/listView/user/ConversationListView.class.php @@ -0,0 +1,127 @@ + + * @since 6.2 + * + * @extends AbstractListView + */ +final class ConversationListView extends AbstractListView +{ + public readonly string $filter; + + public function __construct(string $filter = '') + { + if ($filter === '' || \in_array($filter, UserConversationList::$availableFilters)) { + $this->filter = $filter; + } else { + $this->filter = ''; + } + + $this->addAvailableSortFields([ + new ListViewSortField('time', 'wcf.global.date'), + new ListViewSortField('subject', 'wcf.global.title'), + new ListViewSortField('lastPostTime', 'wcf.conversation.lastPostTime'), + new ListViewSortField('username', 'wcf.user.username'), + new ListViewSortField('replies', 'wcf.conversation.replies'), + new ListViewSortField('participants', 'wcf.conversation.participants'), + ]); + + $this->addAvailableFilters([ + new TextFilter('subject', 'wcf.global.title'), + new UserFilter('participants', 'wcf.conversation.participants'), + $this->getLabelFilter(), + ]); + + $this->setInteractionProvider(new ConversationInteractions()); + $this->setBulkInteractionProvider(new ConversationBulkInteractions()); + + $this->setItemsPerPage(WCF::getUser()->conversationsPerPage ?: \CONVERSATIONS_PER_PAGE); + $this->setSortField(\CONVERSATION_LIST_DEFAULT_SORT_FIELD); + $this->setSortOrder(\CONVERSATION_LIST_DEFAULT_SORT_ORDER); + $this->setCssClassName("tabularList"); + } + + #[\Override] + protected function createObjectList(): UserConversationList + { + return new UserConversationList(WCF::getUser()->userID, $this->filter); + } + + #[\Override] + public function renderItems(): string + { + return WCF::getTPL()->render('wcf', 'conversationListItems', ['view' => $this]); + } + + #[\Override] + protected function getInitializedEvent(): ConversationListViewInitialized + { + return new ConversationListViewInitialized($this); + } + + #[\Override] + public function getParameters(): array + { + return ['filter' => $this->filter]; + } + + private function getLabelFilter(): AbstractFilter + { + return new class extends AbstractFilter { + public readonly ConversationLabelList $labelList; + + public function __construct() + { + $this->labelList = ConversationLabel::getLabelsByUser(); + parent::__construct('label', 'wcf.label.label'); + } + + public function getFormField(): AbstractFormField + { + return ConversationLabelFormField::create('label') + ->label($this->languageItem) + ->labels($this->labelList->getObjects()); + } + + public function applyFilter(DatabaseObjectList $list, string $value): void + { + $list->getConditionBuilder()->add( + "{$list->getDatabaseTableAlias()}.{$list->getDatabaseTableIndexName()} IN ( + SELECT conversationID + FROM wcf1_conversation_label_to_object + WHERE labelID = ? + )", + [$value] + ); + } + + #[\Override] + public function renderValue(string $value): string + { + return $this->labelList->search((int)$value)->label; + } + }; + } +} diff --git a/files/style/conversation.scss b/files/style/conversation.scss index 1b981e90..bcf0ef87 100644 --- a/files/style/conversation.scss +++ b/files/style/conversation.scss @@ -38,3 +38,34 @@ margin-left: 10px; } } + +.conversationList { + .columnInteractions { + display: flex; + flex-direction: column; + } + + @include screen-sm-down { + .conversationList_columnSubject { + flex-basis: calc(100% - 80px) !important; + max-width: calc(100% - 80px) !important; + } + .conversationList_columnAvatar { + margin-left: 5px; + margin-right: 5px !important; + } + } + + /* revert style from `messageGroup.scss` */ + @include screen-md-up { + &.messageGroupList .pagination { + font-size: inherit; + } + } + .pagination { + flex: inherit; + opacity: inherit; + transition: inherit; + font-weight: inherit; + } +} diff --git a/language/de.xml b/language/de.xml index bf0a1622..c58d93a4 100644 --- a/language/de.xml +++ b/language/de.xml @@ -83,10 +83,11 @@ - - + + + @@ -159,6 +160,7 @@ + @@ -285,5 +287,16 @@ + + + + + + + + + + + diff --git a/language/en.xml b/language/en.xml index da8be996..9bc45d5a 100644 --- a/language/en.xml +++ b/language/en.xml @@ -60,17 +60,6 @@ - - - - - - - - - - - @@ -83,10 +72,11 @@ - - + + + @@ -159,6 +149,7 @@ + @@ -276,5 +267,16 @@ + + + + + + + + + + + diff --git a/objectType.xml b/objectType.xml index 4770498e..60e28252 100644 --- a/objectType.xml +++ b/objectType.xml @@ -17,11 +17,6 @@ com.woltlab.wcf.conversation.message com.woltlab.wcf.message - - com.woltlab.wcf.conversation.conversation - com.woltlab.wcf.clipboardItem - wcf\data\conversation\ConversationList - com.woltlab.wcf.conversation.notification com.woltlab.wcf.notification.objectType @@ -125,5 +120,8 @@ com.woltlab.wcf.rebuildData + + com.woltlab.wcf.clipboardItem + diff --git a/templateDelete.xml b/templateDelete.xml index 75fc3546..d9dcb756 100644 --- a/templateDelete.xml +++ b/templateDelete.xml @@ -3,5 +3,8 @@ + + + diff --git a/templates/__userPanelConversationDropdown.tpl b/templates/__userPanelConversationDropdown.tpl index 4fdc3d7b..a62b52fa 100644 --- a/templates/__userPanelConversationDropdown.tpl +++ b/templates/__userPanelConversationDropdown.tpl @@ -16,7 +16,6 @@ {/if} {if !OFFLINE || $__wcf->session->getPermission('admin.general.canViewPageDuringOfflineMode')} - {/if} - diff --git a/templates/conversationAddParticipants.tpl b/templates/conversationAddParticipants.tpl deleted file mode 100644 index fecdd1ad..00000000 --- a/templates/conversationAddParticipants.tpl +++ /dev/null @@ -1,26 +0,0 @@ -
            -
            -
            - - {lang}wcf.conversation.participants.description{/lang} -
            -
            -{if !$conversation->isDraft} - {if $conversation->canAddParticipantsUnrestricted()} -
            -
            -
            - - {lang}wcf.conversation.visibility.all.description{/lang} - - {lang}wcf.conversation.visibility.new.description{/lang} -
            -
            - {else} - - {/if} -{/if} - -
            - -
            \ No newline at end of file diff --git a/templates/conversationContentHeaderTitle.tpl b/templates/conversationContentHeaderTitle.tpl new file mode 100644 index 00000000..95c30394 --- /dev/null +++ b/templates/conversationContentHeaderTitle.tpl @@ -0,0 +1,37 @@ +
            +

            {$conversation->subject}

            + + +
            diff --git a/templates/conversationLabelManagement.tpl b/templates/conversationLabelManagement.tpl deleted file mode 100644 index bdc37944..00000000 --- a/templates/conversationLabelManagement.tpl +++ /dev/null @@ -1,21 +0,0 @@ -{hascontent} -
            -
            -

            {lang}wcf.conversation.label.management.existingLabels{/lang}

            -

            {lang}wcf.conversation.label.management.edit.description{/lang}

            -
            -
              - {content} - {foreach from=$labelList item=label} -
            • - -
            • - {/foreach} - {/content} -
            -
            -{/hascontent} - -
            - -
            diff --git a/templates/conversationLeave.tpl b/templates/conversationLeave.tpl deleted file mode 100644 index 1ee1e111..00000000 --- a/templates/conversationLeave.tpl +++ /dev/null @@ -1,16 +0,0 @@ -
            - {if $hideConversation} -
            - -
            - {else} -
            - - {lang}wcf.conversation.hideConversation.leave.description{/lang} -
            - {/if} -
            - - {lang}wcf.conversation.hideConversation.leavePermanently.description{/lang} -
            -
            diff --git a/templates/conversationList.tpl b/templates/conversationList.tpl index bd0dd3ca..8e37bfd2 100644 --- a/templates/conversationList.tpl +++ b/templates/conversationList.tpl @@ -1,4 +1,4 @@ -{capture assign='pageTitle'}{if $filter}{lang}wcf.conversation.folder.{$filter}{/lang}{else}{$__wcf->getActivePage()->getTitle()}{/if}{if $pageNo > 1} - {lang}wcf.page.pageNo{/lang}{/if}{/capture} +{capture assign='pageTitle'}{if $filter}{lang}wcf.conversation.folder.{$filter}{/lang}{else}{$__wcf->getActivePage()->getTitle()}{/if}{/capture} {capture assign='contentHeader'}
            @@ -27,7 +27,7 @@ {/capture} {capture assign='headContent'} - + {/capture} {capture assign='sidebarRight'} @@ -54,60 +54,6 @@
            -
            -

            {lang}wcf.conversation.filter.participants{/lang}

            - -
            -
            -
            -
            -
            -
            - -
            - - {csrfToken} -
            -
            -
            -
            - -
            -

            {lang}wcf.conversation.label{/lang}

            - -
            - -
            - -
            - -
            -
            - {event name='beforeQuotaBox'}
            @@ -130,276 +76,30 @@ {event name='boxes'} {/capture} -{capture assign='contentInteractionPagination'} - {assign var='participantsParameter' value=''} - {if $participants}{capture assign='participantsParameter'}&participants={implode from=$participants item=participant}{$participant|rawurlencode}{/implode}{/capture}{/if} - {assign var='labelIDParameter' value=''} - {if $labelID}{assign var='labelIDParameter' value="&labelID=$labelID"}{/if} - {pages print=true assign=pagesLinks controller='ConversationList' link="filter=$filter$participantsParameter&pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$labelIDParameter"} -{/capture} - {capture assign='contentInteractionButtons'} - + {if $__wcf->user->userID} + + {/if} {/capture} {capture assign='contentInteractionDropdownItems'} -
          • {lang}wcf.global.button.rss{/lang}
          • +
          • {lang}wcf.global.button.rss{/lang}
          • {/capture} {include file='header'} -{if !$items} - {lang}wcf.conversation.noConversations{/lang} -{else} -
            -
              -
            1. -
                -
              1. - -
              2. - -
              3. -
              -
            2. - - {foreach from=$objects item=conversation} -
            3. -
                -
              1. - -
              2. -
              3. - {if $conversation->getUserProfile()->getAvatar()} -
                - isNew()} title="{lang}wcf.conversation.markAsRead.doubleClick{/lang}"{/if}>{@$conversation->getUserProfile()->getAvatar()->getImageTag(48)}

                - - {if $conversation->ownPosts && $conversation->userID != $__wcf->user->userID} - {if $__wcf->getUserProfileHandler()->getAvatar()} - {@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(24)} - {/if} - {/if} -
                - {/if} -
              4. -
              5. - {hascontent} -
                  - {content} - {foreach from=$conversation->getAssignedLabels() item=label} -
                • {$label->label}
                • - {/foreach} - {/content} -
                - {/hascontent} - -

                - {$conversation->subject} - {if $conversation->replies} - {@$conversation->replies|shortUnit} - {/if} -

                - - - - - -
                  -
                • {$conversation->username}
                • -
                • {time time=$conversation->lastPostTime}
                • -
                - - {if $conversation->getParticipantSummary()|count} - - {assign var='participantSummaryCount' value=$conversation->getParticipantSummary()|count} - {lang}wcf.conversation.participants{/lang}: {implode from=$conversation->getParticipantSummary() item=participant}{$participant->username}{/implode} - {if $participantSummaryCount < $conversation->participants}{lang}wcf.conversation.participants.other{/lang}{/if} - - {/if} - - {event name='conversationData'} -
              6. -
              7. -
                -
                {lang}wcf.conversation.replies{/lang}
                -
                {@$conversation->replies|shortUnit}
                -
                -
                -
                {lang}wcf.conversation.participants{/lang}
                -
                {@$conversation->participants|shortUnit}
                -
                - -
                - {if $conversation->replies} - - {icon name='comment'} - - {@$conversation->replies|shortUnit} - {/if} -
                -
              8. -
              9. - {if $conversation->replies != 0 && $conversation->lastPostTime} -
                - {@$conversation->getLastPosterProfile()->getAvatar()->getImageTag(32)} - -
                -

                - {user object=$conversation->getLastPosterProfile()} -

                - {time time=$conversation->lastPostTime} -
                -
                - {/if} -
              10. - - {event name='columns'} -
              -
            4. - {/foreach} -
            -
            -{/if} - -
            - {hascontent} -
            - {content}{@$pagesLinks}{/content} -
            - {/hascontent} - - {hascontent} - - {/hascontent} -
            - - - - + +{/if} {include file='footer'} diff --git a/templates/conversationListItems.tpl b/templates/conversationListItems.tpl new file mode 100644 index 00000000..008e76d7 --- /dev/null +++ b/templates/conversationListItems.tpl @@ -0,0 +1,121 @@ +{foreach from=$view->getItems() item=conversation} +
            +
              +
            1. + {if $view->hasBulkInteractions()} + + {/if} + + {unsafe:$view->renderInteractionContextMenuButton($conversation)} +
            2. +
            3. + {if $conversation->getUserProfile()->getAvatar()} +
              + isNew()} title="{lang}wcf.conversation.markAsRead.doubleClick{/lang}"{/if}>{unsafe:$conversation->getUserProfile()->getAvatar()->getImageTag(48)}

              + + {if $conversation->ownPosts && $conversation->userID != $__wcf->user->userID} + {if $__wcf->getUserProfileHandler()->getAvatar()} + {unsafe:$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(24)} + {/if} + {/if} +
              + {/if} +
            4. +
            5. + {hascontent} +
                + {content} + {foreach from=$conversation->getAssignedLabels() item=label} +
              • {$label->label}
              • + {/foreach} + {/content} +
              + {/hascontent} + +

              + {$conversation->subject} + {if $conversation->replies} + {$conversation->replies|shortUnit} + {/if} +

              + + + +
                +
              • {user object=$conversation->getUserProfile()}
              • +
              • {time time=$conversation->time}
              • + {event name='messageGroupInfo'} +
              + +
                +
              • {$conversation->username}
              • +
              • {time time=$conversation->lastPostTime}
              • +
              + + {if $conversation->getParticipantSummary()|count} + + {assign var='participantSummaryCount' value=$conversation->getParticipantSummary()|count} + {lang}wcf.conversation.participants{/lang}: {implode from=$conversation->getParticipantSummary() item=participant}{$participant->username}{/implode} + {if $participantSummaryCount < $conversation->participants}{lang}wcf.conversation.participants.other{/lang}{/if} + + {/if} + + {event name='conversationData'} +
            6. +
            7. +
              +
              {lang}wcf.conversation.replies{/lang}
              +
              {$conversation->replies|shortUnit}
              +
              +
              +
              {lang}wcf.conversation.participants{/lang}
              +
              {$conversation->participants|shortUnit}
              +
              + +
              + {if $conversation->replies} + + {icon name='comment'} + + {$conversation->replies|shortUnit} + {/if} +
              +
            8. +
            9. + {if $conversation->replies != 0 && $conversation->lastPostTime} +
              + {unsafe:$conversation->getLastPosterProfile()->getAvatar()->getImageTag(32)} + +
              +

              + {user object=$conversation->getLastPosterProfile()} +

              + {time time=$conversation->lastPostTime} +
              +
              + {/if} +
            10. + + {event name='columns'} +
            +
            +{/foreach} diff --git a/templates/conversationParticipantList.tpl b/templates/conversationParticipantList.tpl new file mode 100644 index 00000000..37cfd140 --- /dev/null +++ b/templates/conversationParticipantList.tpl @@ -0,0 +1,31 @@ +
              + {foreach from=$participants item=participant} +
            • +
              + {user object=$participant type='avatar24' ariaHidden='true' tabindex='-1'} +
              +

              + {user object=$participant} + {if $participant->isInvisible}({lang}wcf.conversation.invisible{/lang}){/if} + {if $participant->userID && ($conversation->userID == $__wcf->getUser()->userID) && ($participant->userID != $__wcf->getUser()->userID) && $participant->hideConversation != 2 && $participant->leftAt == 0} + + {/if} +

              +
              +
              {lang}wcf.conversation.lastVisitTime{/lang}
              +
              {if $participant->lastVisitTime}{time time=$participant->lastVisitTime}{else}-{/if}
              +
              +
              +
              +
            • + {/foreach} +
            diff --git a/templates/shared_conversationLabelFormField.tpl b/templates/shared_conversationLabelFormField.tpl new file mode 100644 index 00000000..101495cf --- /dev/null +++ b/templates/shared_conversationLabelFormField.tpl @@ -0,0 +1,38 @@ +
              + +
            + + + + diff --git a/ts/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.ts b/ts/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.ts deleted file mode 100644 index 3cd6483f..00000000 --- a/ts/WoltLabSuite/Core/Api/Conversations/AssignConversationLabels.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Assign the labels for the given conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; -import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; - -export async function assignConversationLabels(conversationIDs: number[], labelIDs: number[]): Promise> { - try { - await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/assign-labels`) - .post({ - conversationIDs: conversationIDs, - labelIDs: labelIDs, - }) - .fetchAsJson(); - } catch (e) { - return apiResultFromError(e); - } - - return apiResultFromValue([]); -} diff --git a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.ts b/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.ts deleted file mode 100644 index d2d27173..00000000 --- a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabels.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Gets the form data to assign labels to conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; -import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; - -export type Response = { - template: string; - formId: string; - title: string; -}; - -export async function getConversationLabels(conversationIDs: number[]): Promise> { - const url = new URL(`${window.WSC_RPC_API_URL}core/conversations/labels`); - for (const conversationID of conversationIDs) { - url.searchParams.append("conversationIDs[]", conversationID.toString()); - } - - let response: Response; - try { - response = (await prepareRequest(url).get().fetchAsJson()) as Response; - } catch (e) { - return apiResultFromError(e); - } - - return apiResultFromValue(response); -} diff --git a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.ts b/ts/WoltLabSuite/Core/Api/Conversations/GetParticipantList.ts similarity index 73% rename from ts/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.ts rename to ts/WoltLabSuite/Core/Api/Conversations/GetParticipantList.ts index d1236d00..ca85885d 100644 --- a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLeaveDialog.ts +++ b/ts/WoltLabSuite/Core/Api/Conversations/GetParticipantList.ts @@ -1,5 +1,5 @@ /** - * Gets the html code for the rendering the conversation leave dialog. + * Gets the html code for the rendering of the participant list of a conversation. * * @author Olaf Braun * @copyright 2001-2025 WoltLab GmbH @@ -14,10 +14,10 @@ type Response = { template: string; }; -export async function getConversationLeaveDialog(conversationId: number): Promise> { +export async function getParticipantList(conversationId: number): Promise> { let response: Response; try { - response = (await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/leave-dialog`) + response = (await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/participants`) .get() .fetchAsJson()) as Response; } catch (e) { diff --git a/ts/WoltLabSuite/Core/Api/Conversations/LeaveConversation.ts b/ts/WoltLabSuite/Core/Api/Conversations/LeaveConversation.ts deleted file mode 100644 index b1ece5dc..00000000 --- a/ts/WoltLabSuite/Core/Api/Conversations/LeaveConversation.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Leaves a conversation. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; -import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; - -type Response = { - redirectUrl: string; -}; - -export type HideConversation = 0 | 1 | 2; - -export async function leaveConversation( - conversationId: number, - hideConversation: HideConversation, -): Promise> { - let response: Response; - try { - response = (await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/${conversationId}/leave`) - .post({ - hideConversation, - }) - .fetchAsJson()) as Response; - } catch (e) { - return apiResultFromError(e); - } - - return apiResultFromValue(response.redirectUrl); -} diff --git a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.ts b/ts/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.ts similarity index 56% rename from ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.ts rename to ts/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.ts index 386d560e..9cfd74ed 100644 --- a/ts/WoltLabSuite/Core/Api/Conversations/GetConversationLabelManager.ts +++ b/ts/WoltLabSuite/Core/Api/Conversations/RemoveParticipant.ts @@ -1,5 +1,5 @@ /** - * Gets the data for the conversation label manager. + * Remove a participant from a conversation. * * @author Olaf Braun * @copyright 2001-2025 WoltLab GmbH @@ -10,21 +10,21 @@ import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; -export type Response = { +type Response = { template: string; - maxLabels: number; - labelCount: number; }; -export async function getConversationLabelManager(): Promise> { +export async function removeParticipant(conversationId: number, participantId: number): Promise> { let response: Response; try { - response = (await prepareRequest(`${window.WSC_RPC_API_URL}core/conversations/label-manager`) - .get() + response = (await prepareRequest( + `${window.WSC_RPC_API_URL}core/conversations/${conversationId}/participants/${participantId}`, + ) + .delete() .fetchAsJson()) as Response; } catch (e) { return apiResultFromError(e); } - return apiResultFromValue(response); + return apiResultFromValue(response.template); } diff --git a/ts/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.ts b/ts/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.ts new file mode 100644 index 00000000..c3a703b7 --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Conversation/RemoveParticipant.ts @@ -0,0 +1,39 @@ +/** + * Reacts to participants being removed from a conversation. + * + * @author Olaf Braun, Matthias Schmidt + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + */ + +import { wheneverSeen } from "WoltLabSuite/Core/Helper/Selector"; +import { removeParticipant } from "../../Api/Conversations/RemoveParticipant"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; +import { confirmationFactory } from "WoltLabSuite/Core/Component/Confirmation"; +import { getPhrase } from "WoltLabSuite/Core/Language"; + +export function setup(): void { + wheneverSeen(".conversationRemoveParticipant", (element: HTMLButtonElement) => { + const participantId = parseInt(element.dataset.participantId || "0", 10); + const conversationId = parseInt(element.dataset.conversationId || "0", 10); + const confirmMessage = element.dataset.confirmMessage!; + + element.addEventListener( + "click", + promiseMutex(async () => { + const confirmed = await confirmationFactory() + .custom(getPhrase("wcf.global.confirmation.title")) + .message(confirmMessage); + + if (confirmed) { + const response = await removeParticipant(conversationId, participantId); + if (!response.ok) { + return; + } + + document.querySelector(".conversationParticipantList")!.outerHTML = response.value; + } + }), + ); + }); +} diff --git a/ts/WoltLabSuite/Core/Conversation/Bootstrap.ts b/ts/WoltLabSuite/Core/Conversation/Bootstrap.ts index 07dc87db..ad163dcf 100644 --- a/ts/WoltLabSuite/Core/Conversation/Bootstrap.ts +++ b/ts/WoltLabSuite/Core/Conversation/Bootstrap.ts @@ -22,6 +22,12 @@ function setupPopover(): void { }); }); }); + + whenFirstSeen(".conversationRemoveParticipant", () => { + void import("../Component/Conversation/RemoveParticipant").then(({ setup }) => { + setup(); + }); + }); } export function setup(): void { diff --git a/ts/WoltLabSuite/Core/Conversation/Clipboard.ts b/ts/WoltLabSuite/Core/Conversation/Clipboard.ts deleted file mode 100644 index 73c3b841..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Clipboard.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Clipboard for conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { add as addEvent } from "WoltLabSuite/Core/Event/Handler"; -import { AjaxResponse, ClipboardActionData } from "WoltLabSuite/Core/Controller/Clipboard/Data"; -import { openDialog as openAssignLabelDialog } from "./Component/Label/Editor"; -import { getConversationEditor } from "./Component/EditorHandler"; - -interface ConversationData { - isClosed: boolean; -} - -interface ResponseData { - returnValues: { - conversationData: { - [key: string]: ConversationData; - }; - }; -} - -interface EventData { - data: ClipboardActionData; - responseData: null | (ResponseData & AjaxResponse); -} - -export function setup() { - addEvent("com.woltlab.wcf.clipboard", "com.woltlab.wcf.conversation.conversation", (data: EventData) => { - if (data.responseData === null) { - execute(data.data.actionName, data.data.parameters); - } else { - evaluateResponse(data.data.actionName, data.responseData); - } - }); -} - -function execute(actionName: string, parameters) { - if (actionName === "com.woltlab.wcf.conversation.conversation.assignLabel") { - void openAssignLabelDialog(parameters.objectIDs); - } -} - -function evaluateResponse(actionName: string, data: ResponseData) { - switch (actionName) { - case "com.woltlab.wcf.conversation.conversation.leave": - case "com.woltlab.wcf.conversation.conversation.leavePermanently": - case "com.woltlab.wcf.conversation.conversation.markAsRead": - case "com.woltlab.wcf.conversation.conversation.restore": - window.location.reload(); - break; - - case "com.woltlab.wcf.conversation.conversation.close": - case "com.woltlab.wcf.conversation.conversation.open": - Object.entries(data.returnValues.conversationData).forEach(([conversationId, conversationData]) => { - getConversationEditor(parseInt(conversationId))!.isClosed = conversationData.isClosed; - }); - break; - } -} diff --git a/ts/WoltLabSuite/Core/Conversation/Component/EditorHandler.ts b/ts/WoltLabSuite/Core/Conversation/Component/EditorHandler.ts deleted file mode 100644 index 7e04be9b..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Component/EditorHandler.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Handles editing for conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import { escapeHTML } from "WoltLabSuite/Core/StringUtil"; -import { reload as reloadClipboard } from "WoltLabSuite/Core/Controller/Clipboard"; - -export interface Label { - cssClassName: string; - labelID: number; - label: string; - url?: string; -} - -const _availableLabels: Map = new Map(); -const conversations = new Map(); - -class ConversationEditor { - #conversation: HTMLElement; - #isClosed: boolean; - #canAddParticipants: boolean; - #canCloseConversation: boolean; - #labelIDs: number[]; - #isConversationListItem: boolean; - - constructor(conversation: HTMLElement) { - this.#conversation = conversation; - this.#isConversationListItem = conversation.classList.contains("jsClipboardObject"); - this.#isClosed = conversation.dataset.isClosed === "1"; - this.#canAddParticipants = conversation.dataset.canAddParticipants === "1"; - this.#canCloseConversation = conversation.dataset.canCloseConversation === "1"; - this.#labelIDs = JSON.parse(conversation.dataset.labelIds!); - } - - get isClosed(): boolean { - return this.#isClosed; - } - - set isClosed(isClosed: boolean) { - this.#isClosed = isClosed; - this.#conversation.dataset.isClosed = isClosed ? "1" : "0"; - - if (isClosed) { - this.#getContainer(".statusIcons")?.insertAdjacentHTML( - "beforeend", - `
          • - - - -
          • `, - ); - } else { - this.#getContainer(".statusIcons").querySelector("li .jsIconLock")?.parentElement?.remove(); - } - - reloadClipboard(); - } - - get canAddParticipants(): boolean { - return this.#canAddParticipants; - } - - get canCloseConversation(): boolean { - return this.#canCloseConversation; - } - - get labelIDs(): number[] { - return this.#labelIDs; - } - - set labelIDs(labelIDs: number[]) { - this.#labelIDs = labelIDs; - this.#conversation.dataset.labelIDs = JSON.stringify(labelIDs); - - const labels = labelIDs.map((labelID) => { - return getLabel(labelID)!; - }); - - let labelList = this.#getContainer(".columnSubject > .labelList"); - if (labelIDs.length == 0) { - labelList?.remove(); - } else { - if (!labelList) { - labelList = document.createElement("ul"); - labelList.classList.add("labelList"); - this.#getContainer(".columnSubject")?.insertAdjacentElement("afterbegin", labelList); - } - - // remove old labels - labelList.innerHTML = ""; - - for (const label of labels) { - if (this.#isConversationListItem) { - labelList.insertAdjacentHTML( - "beforeend", - `
          • ${escapeHTML(label.label)}
          • `, - ); - } else { - labelList.insertAdjacentHTML( - "beforeend", - `
          • ${escapeHTML(label.label)}
          • `, - ); - } - } - } - - reloadClipboard(); - } - - #getContainer(selector: string): HTMLElement { - if (this.#isConversationListItem) { - return this.#conversation.querySelector(selector)!; - } else { - return document.querySelector(".contentHeaderTitle > .contentHeaderMetaData")!; - } - } -} - -export function getAvailableLabels(): Label[] { - return Array.from(_availableLabels.values()); -} - -export function addAvailableLabel(label: Label) { - _availableLabels.set(label.labelID, label); -} - -export function getLabel(labelID: number): Label | undefined { - return _availableLabels.get(labelID); -} - -export function getConversationEditor(conversationID: number): ConversationEditor | undefined { - return conversations.get(conversationID); -} - -export function setup(availableLabels: Label[]) { - availableLabels.forEach((label) => { - _availableLabels.set(label.labelID, label); - }); - - wheneverFirstSeen(".conversation", (conversation) => { - conversations.set(parseInt(conversation.dataset.conversationId!), new ConversationEditor(conversation)); - }); -} diff --git a/ts/WoltLabSuite/Core/Conversation/Component/Label/Editor.ts b/ts/WoltLabSuite/Core/Conversation/Component/Label/Editor.ts deleted file mode 100644 index 43491e10..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Component/Label/Editor.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Editor to assign labels to conversations. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; -import { getConversationLabels } from "../../../Api/Conversations/GetConversationLabels"; -import * as FormBuilderManager from "WoltLabSuite/Core/Form/Builder/Manager"; -import { assignConversationLabels } from "../../../Api/Conversations/AssignConversationLabels"; -import { reload as reloadClipboard } from "WoltLabSuite/Core/Controller/Clipboard"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { getConversationEditor } from "../EditorHandler"; - -export async function openDialog(conversationIDs: number[]) { - const response = await getConversationLabels(conversationIDs); - if (!response.ok) { - throw new Error("Failed to load form to assign labels to conversations."); - } - - const dialog = dialogFactory().fromHtml(response.value.template).asPrompt(); - dialog.addEventListener("afterClose", () => { - if (FormBuilderManager.hasForm(response.value.formId)) { - FormBuilderManager.unregisterForm(response.value.formId); - } - }); - dialog.addEventListener("primary", () => { - void FormBuilderManager.getData(response.value.formId).then(async (data) => { - const labelIDs: number[] = []; - for (const labelID of data["conversationLabel_labelIDs"]) { - labelIDs.push(parseInt(labelID)); - } - - await assignConversationLabels(conversationIDs, labelIDs); - - assignLabels(conversationIDs, labelIDs); - reloadClipboard(); - }); - }); - - dialog.show(response.value.title); -} - -function assignLabels(conversationIDs: number[], labelIDs: number[]) { - conversationIDs.forEach((conversationID) => { - getConversationEditor(conversationID)!.labelIDs = labelIDs; - }); - - showDefaultSuccessSnackbar(); -} diff --git a/ts/WoltLabSuite/Core/Conversation/Component/Label/Manager.ts b/ts/WoltLabSuite/Core/Conversation/Component/Label/Manager.ts deleted file mode 100644 index 950fc8da..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Component/Label/Manager.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Managed the labels of a conversation. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { - getConversationLabelManager, - Response as ManagerResponse, -} from "../../../Api/Conversations/GetConversationLabelManager"; -import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; -import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; -import WoltlabCoreDialogElement from "WoltLabSuite/Core/Element/woltlab-core-dialog"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple"; -import { addAvailableLabel } from "../EditorHandler"; - -interface LabelFormResponse { - deleteLabel: boolean; - labelID: number; - label: string; - cssClassName: string; -} - -export class LabelManager { - #conversationListLink: string; - #formLink: string; - #dialog?: WoltlabCoreDialogElement; - #maxLabels = 0; - #labelCount = 0; - - constructor(formLink: string, conversationListLink: string) { - this.#formLink = formLink; - this.#conversationListLink = conversationListLink; - - document.getElementById("manageLabel")?.addEventListener( - "click", - promiseMutex(async () => { - const response = await getConversationLabelManager(); - if (!response.ok) { - throw new Error("Could not fetch conversation label manager"); - } - - this.#openDialog(response.value); - }), - ); - } - - #openDialog(data: ManagerResponse): void { - this.#maxLabels = data.maxLabels; - this.#labelCount = data.labelCount; - - this.#dialog = dialogFactory().fromHtml(data.template).withoutControls(); - this.#updateAddButtonState(); - this.#dialog.show(getPhrase("wcf.conversation.label.management")); - - this.#dialog?.content.querySelectorAll(".conversationLabelList .badge").forEach((badge: HTMLElement) => { - const labelId = parseInt(badge.dataset.labelId!); - badge.addEventListener( - "click", - promiseMutex(async () => { - await this.#openForm(labelId); - }), - ); - }); - - this.#dialog?.content.querySelector(".addLabel")?.addEventListener( - "click", - promiseMutex(async () => { - await this.#openForm(); - }), - ); - } - - async #openForm(labelId?: number) { - const url = new URL(this.#formLink); - if (labelId) { - url.searchParams.set("labelID", labelId.toString()); - } - - const response = await dialogFactory().usingFormBuilder().fromEndpoint(url.toString()); - if (response.ok) { - if (response.result.deleteLabel) { - // check if delete label id is present within URL (causing an IllegalLinkException if reloading) - const regex = new RegExp("(\\?|&)labelID=" + response.result.labelID); - window.location.href = window.location.toString().replace(regex, ""); - return; - } - - if (labelId) { - window.location.reload(); - return; - } - - this.#labelCount++; - - const button = document.createElement("button"); - button.type = "button"; - button.classList.add("badge", "label", response.result.cssClassName || ""); - button.dataset.labelId = response.result.labelID.toString(); - button.dataset.cssClassName = response.result.cssClassName; - button.textContent = response.result.label; - button.addEventListener( - "click", - promiseMutex(() => this.#openForm(response.result.labelID)), - ); - - const li = document.createElement("li"); - li.append(button); - - this.#dialog?.content.querySelector(".conversationLabelList")?.append(li); - this.#insertLabel(response.result); - - this.#updateAddButtonState(); - showDefaultSuccessSnackbar(); - } - } - - #updateAddButtonState(): void { - this.#dialog!.content.querySelector(".addLabel")!.disabled = this.#labelCount >= this.#maxLabels; - } - - #insertLabel(data: LabelFormResponse): void { - const listItem = document.createElement("li"); - const anchor = document.createElement("a"); - - const url = new URL(this.#conversationListLink); - url.searchParams.set("labelID", data.labelID.toString()); - anchor.href = url.toString(); - - const span = document.createElement("span"); - span.className = `badge label${data.cssClassName ? " " + data.cssClassName : ""}`; - span.textContent = data.label; - span.dataset.labelID = data.labelID.toString(); - span.dataset.cssClassName = data.cssClassName; - - anchor.appendChild(span); - listItem.appendChild(anchor); - - UiDropdownSimple.getDropdownMenu("conversationLabelFilter") - ?.querySelector(".scrollableDropdownMenu") - ?.append(listItem); - - addAvailableLabel({ - labelID: data.labelID, - label: data.label, - cssClassName: data.cssClassName, - url: url.toString(), - }); - } -} diff --git a/ts/WoltLabSuite/Core/Conversation/Component/Leave.ts b/ts/WoltLabSuite/Core/Conversation/Component/Leave.ts deleted file mode 100644 index c946089e..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Component/Leave.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Handles the leave conversation action. - * - * @author Olaf Braun - * @copyright 2001-2025 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - */ - -import { getConversationLeaveDialog } from "../../Api/Conversations/GetConversationLeaveDialog"; -import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; -import { getPhrase } from "WoltLabSuite/Core/Language"; -import { leaveConversation, HideConversation } from "../../Api/Conversations/LeaveConversation"; -import DomUtil from "WoltLabSuite/Core/Dom/Util"; - -type Environment = "conversation" | ""; - -export async function openDialog(conversationID: number, environment: Environment) { - const result = await getConversationLeaveDialog(conversationID); - if (result.ok) { - const dialog = dialogFactory().fromHtml(result.value).asConfirmation(); - dialog.addEventListener("validate", (event) => { - const checked = getSelectedValue(dialog) !== undefined; - - event.detail.push(Promise.resolve(checked)); - - DomUtil.innerError(dialog.querySelector("dl")!, checked ? undefined : getPhrase("wcf.global.form.error.empty")); - }); - dialog.addEventListener("primary", () => { - const hideConversation = getSelectedValue(dialog)!; - - void leaveConversation(conversationID, hideConversation).then((leaveResult) => { - if (!leaveResult.ok) { - return; - } - - if (environment === "conversation") { - window.location.href = leaveResult.value; - } else { - window.location.reload(); - } - }); - }); - dialog.show(getPhrase("wcf.conversation.leave.title")); - } -} - -function getSelectedValue(dialog: HTMLElement): HideConversation | undefined { - const selected = dialog.querySelector('input[name="hideConversation"]:checked'); - - return selected ? (parseInt(selected.value) as HideConversation) : undefined; -} diff --git a/ts/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.ts b/ts/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.ts deleted file mode 100644 index 6330cb8f..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Ui/Object/Action/RemoveParticipant.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Reacts to participants being removed from a conversation. - * - * @author Matthias Schmidt - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import UiObjectActionHandler from "WoltLabSuite/Core/Ui/Object/Action/Handler"; -import { ObjectActionData } from "WoltLabSuite/Core/Ui/Object/Data"; - -function removeParticipant(data: ObjectActionData): void { - data.objectElement.querySelector(".userLink")!.classList.add("conversationLeft"); - data.objectElement.querySelector(".jsObjectAction[data-object-action='removeParticipant']")!.remove(); -} - -export function setup(): void { - new UiObjectActionHandler("removeParticipant", [], removeParticipant); -} diff --git a/ts/WoltLabSuite/Core/Conversation/Ui/Participant/Add.ts b/ts/WoltLabSuite/Core/Conversation/Ui/Participant/Add.ts deleted file mode 100644 index 989323a0..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Ui/Participant/Add.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Adds participants to an existing conversation. - * - * @author Alexander Ebert - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import * as Ajax from "WoltLabSuite/Core/Ajax"; -import { AjaxCallbackObject } from "WoltLabSuite/Core/Ajax/Data"; -import { AjaxCallbackSetup, ResponseData } from "WoltLabSuite/Core/Ajax/Data"; -import DomUtil from "WoltLabSuite/Core/Dom/Util"; -import UiDialog from "WoltLabSuite/Core/Ui/Dialog"; -import { showSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { DialogCallbackObject, DialogCallbackSetup } from "WoltLabSuite/Core/Ui/Dialog/Data"; -import * as UiItemListUser from "WoltLabSuite/Core/Ui/ItemList/User"; -import { ItemData } from "WoltLabSuite/Core/Ui/ItemList"; -import * as Language from "WoltLabSuite/Core/Language"; - -interface AjaxResponseData extends ResponseData { - actionName: "addParticipants" | "getAddParticipantsForm"; -} - -interface AjaxAddParticipantsData extends AjaxResponseData { - returnValues: - | { - errorMessage: string; - } - | { - count: number; - successMessage: string; - }; -} - -interface AjaxGetAddParticipantsFormData extends AjaxResponseData { - returnValues: { - canAddGroupParticipants: boolean; - csvPerType: boolean; - excludedSearchValues: string[]; - maxItems: number; - restrictUserGroupIDs: number[]; - template: string; - }; -} - -class UiParticipantAdd implements AjaxCallbackObject, DialogCallbackObject { - protected readonly conversationId: number; - - constructor(conversationId: number) { - this.conversationId = conversationId; - - Ajax.api(this, { - actionName: "getAddParticipantsForm", - }); - } - - _ajaxSetup(): ReturnType { - return { - data: { - className: "wcf\\data\\conversation\\ConversationAction", - objectIDs: [this.conversationId], - }, - }; - } - - _ajaxSuccess(data: AjaxResponseData): void { - switch (data.actionName) { - case "addParticipants": - this.handleResponse(data as AjaxAddParticipantsData); - break; - case "getAddParticipantsForm": - this.render(data as AjaxGetAddParticipantsFormData); - break; - } - } - - /** - * Shows the success message and closes the dialog overlay. - */ - protected handleResponse(data: AjaxAddParticipantsData): void { - if ("errorMessage" in data.returnValues) { - DomUtil.innerError( - document.getElementById("participantsInput")!.closest(".inputItemList")!, - data.returnValues.errorMessage, - ); - return; - } - - if ("count" in data.returnValues) { - showSuccessSnackbar(data.returnValues.successMessage).addEventListener("snackbar:close", () => { - window.location.reload(); - }); - } - - UiDialog.close(this); - } - - /** - * Renders the dialog to add participants. - * @protected - */ - protected render(data: AjaxGetAddParticipantsFormData): void { - UiDialog.open(this, data.returnValues.template); - - const buttonSubmit = document.getElementById("addParticipants") as HTMLButtonElement; - buttonSubmit.disabled = true; - - UiItemListUser.init("participantsInput", { - callbackChange: (elementId: string, values: ItemData[]): void => { - buttonSubmit.disabled = values.length === 0; - }, - excludedSearchValues: data.returnValues.excludedSearchValues, - maxItems: data.returnValues.maxItems, - includeUserGroups: data.returnValues.canAddGroupParticipants && data.returnValues.restrictUserGroupIDs.length > 0, - restrictUserGroupIDs: data.returnValues.restrictUserGroupIDs, - csvPerType: true, - }); - buttonSubmit.addEventListener("click", () => { - this.submit(); - }); - } - - /** - * Sends a request to add participants. - */ - protected submit(): void { - const participants: string[] = []; - const participantsGroupIDs: number[] = []; - UiItemListUser.getValues("participantsInput").forEach((value) => { - if (value.type === "group") { - participantsGroupIDs.push(value.objectId); - } else { - participants.push(value.value); - } - }); - - const parameters = { - participants: participants, - participantsGroupIDs: participantsGroupIDs, - visibility: null as null | string, - }; - const visibility = UiDialog.getDialog(this)!.content.querySelector( - 'input[name="messageVisibility"]:checked, input[name="messageVisibility"][type="hidden"]', - ); - - if (visibility) { - parameters.visibility = visibility.value; - } - - Ajax.api(this, { - actionName: "addParticipants", - parameters: parameters, - }); - } - - _dialogSetup(): ReturnType { - return { - id: "conversationAddParticipants", - options: { - title: Language.get("wcf.conversation.edit.addParticipants"), - }, - source: null, - }; - } -} - -export = UiParticipantAdd; diff --git a/ts/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.ts b/ts/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.ts deleted file mode 100644 index d241c16a..00000000 --- a/ts/WoltLabSuite/Core/Conversation/Ui/Subject/Editor.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Provides the editor for conversation subjects. - * - * @author Alexander Ebert - * @copyright 2001-2021 WoltLab GmbH - * @license GNU Lesser General Public License - */ -import { DialogCallbackObject } from "WoltLabSuite/Core/Ui/Dialog/Data"; -import UiDialog from "WoltLabSuite/Core/Ui/Dialog"; -import DomUtil from "WoltLabSuite/Core/Dom/Util"; -import * as Ajax from "WoltLabSuite/Core/Ajax"; -import * as Language from "WoltLabSuite/Core/Language"; -import { AjaxCallbackObject, ResponseData } from "WoltLabSuite/Core/Ajax/Data"; -import { showDefaultSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; -import { DialogCallbackSetup } from "WoltLabSuite/Core/Ui/Dialog/Data"; -import { AjaxCallbackSetup } from "WoltLabSuite/Core/Ajax/Data"; - -interface AjaxResponseData extends ResponseData { - returnValues: { - subject: string; - }; -} - -class UiSubjectEditor implements AjaxCallbackObject, DialogCallbackObject { - private readonly objectId: number; - private subject: HTMLInputElement; - - constructor(objectId: number) { - this.objectId = objectId; - } - - /** - * Shows the subject editor dialog. - */ - public show(): void { - UiDialog.open(this); - } - - /** - * Validates and saves the new subject. - */ - protected saveEdit(event: Event) { - event.preventDefault(); - - const value = this.subject.value.trim(); - if (value === "") { - DomUtil.innerError(this.subject, Language.get("wcf.global.form.error.empty")); - } else { - DomUtil.innerError(this.subject, ""); - - Ajax.api(this, { - parameters: { - subject: value, - }, - objectIDs: [this.objectId], - }); - } - } - - /** - * Returns the current conversation subject. - */ - protected getCurrentValue(): string { - return Array.from( - document.querySelectorAll( - `.jsConversationSubject[data-conversation-id="${this.objectId}"], .conversationLink[data-object-id="${this.objectId}"]`, - ), - ) - .map((subject: HTMLElement) => subject.textContent!) - .slice(-1)[0]; - } - - _ajaxSuccess(data: AjaxResponseData) { - UiDialog.close(this); - - document - .querySelectorAll( - `.jsConversationSubject[data-conversation-id="${this.objectId}"], .conversationLink[data-object-id="${this.objectId}"]`, - ) - .forEach((subject) => { - subject.textContent = data.returnValues.subject; - }); - - showDefaultSuccessSnackbar(); - } - - _dialogSetup(): ReturnType { - return { - id: "dialogConversationSubjectEditor", - options: { - onSetup: (content) => { - this.subject = document.getElementById("jsConversationSubject") as HTMLInputElement; - this.subject.addEventListener("keyup", (ev) => { - if (ev.key === "Enter") { - this.saveEdit(ev); - } - }); - - content.querySelector(".jsButtonSave")!.addEventListener("click", (ev) => { - this.saveEdit(ev); - }); - }, - onShow: () => { - this.subject.value = this.getCurrentValue(); - }, - title: Language.get("wcf.conversation.edit.subject"), - }, - source: ` -
            -
            - -
            -
            - -
            -
            -
            - -
            - `, - }; - } - - _ajaxSetup(): ReturnType { - return { - data: { - actionName: "editSubject", - className: "wcf\\data\\conversation\\ConversationAction", - }, - }; - } -} - -let editor: UiSubjectEditor; - -export function beginEdit(objectId: number): void { - editor = new UiSubjectEditor(objectId); - - editor.show(); -}