- */
-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 @@
conversationListUserPanel
conversationLabelAssignment
+ conversationLeave
+ conversationLabelManagement
+ conversationAddParticipants
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 @@
+
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}
-
-
-
- {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'}