Переглянути джерело

Add moderate functions for topic page

Visman 5 роки тому
батько
коміт
410b8b4e5c

+ 5 - 6
app/Models/Pages/Forum.php

@@ -48,7 +48,7 @@ class Forum extends Page
             $this->c->Lang->load('misc');
             $this->c->Lang->load('misc');
 
 
             $this->enableMod = true;
             $this->enableMod = true;
-            $this->formMod   = $this->formMod($forum->id, $forum->page);
+            $this->formMod   = $this->formMod($forum);
         }
         }
 
 
         return $this;
         return $this;
@@ -57,20 +57,19 @@ class Forum extends Page
     /**
     /**
      * Создает массив данных для формы модерации
      * Создает массив данных для формы модерации
      *
      *
-     * @param int $id
-     * @param int $page
+     * @param ForumModel $forum
      *
      *
      * @return array
      * @return array
      */
      */
-    protected function formMod(int $id, int $page): array
+    protected function formMod(ForumModel $forum): array
     {
     {
         $form = [
         $form = [
             'id'     => 'id-form-mod',
             'id'     => 'id-form-mod',
             'action' => $this->c->Router->link('Moderate'),
             'action' => $this->c->Router->link('Moderate'),
             'hidden' => [
             'hidden' => [
                 'token' => $this->c->Csrf->create('Moderate'),
                 'token' => $this->c->Csrf->create('Moderate'),
-                'forum' => $id,
-                'page'  => $page,
+                'forum' => $forum->id,
+                'page'  => $forum->page,
                 'step'  => 1,
                 'step'  => 1,
             ],
             ],
             'sets'   => [],
             'sets'   => [],

+ 196 - 59
app/Models/Pages/Moderate.php

@@ -7,20 +7,29 @@ use ForkBB\Core\Validator;
 use ForkBB\Models\Page;
 use ForkBB\Models\Page;
 use ForkBB\Models\Forum\Model as Forum;
 use ForkBB\Models\Forum\Model as Forum;
 use ForkBB\Models\Topic\Model as Topic;
 use ForkBB\Models\Topic\Model as Topic;
+use ForkBB\Models\Post\Model as Post;
 
 
 class Moderate extends Page
 class Moderate extends Page
 {
 {
+    const INFORUM = 1; // действие для форума
+    const INTOPIC = 2; // действие для темы
+    const TOTOPIC = 4; // список постов сменить на тему
+    const IFTOTPC = 8; // список постов сменить на тему, если в нем только первый пост темы
+
     /**
     /**
      * Список действий
      * Список действий
      * @var array
      * @var array
      */
      */
     protected $actions = [
     protected $actions = [
-        'open'   => true,
-        'close'  => true,
-        'delete' => true,
-        'move'   => true,
-        'merge'  => true,
-        'cancel' => true,
+        'open'    => self::INFORUM + self::INTOPIC + self::TOTOPIC,
+        'close'   => self::INFORUM + self::INTOPIC + self::TOTOPIC,
+        'delete'  => self::INFORUM + self::INTOPIC + self::IFTOTPC,
+        'move'    => self::INFORUM + self::INTOPIC + self::IFTOTPC,
+        'merge'   => self::INFORUM,
+        'cancel'  => self::INFORUM + self::INTOPIC,
+        'unstick' => self::INTOPIC + self::TOTOPIC,
+        'stick'   => self::INTOPIC + self::TOTOPIC,
+        'split'   => self::INTOPIC,
     ];
     ];
 
 
     /**
     /**
@@ -43,7 +52,7 @@ class Moderate extends Page
     /**
     /**
      * Составление списка категорий/разделов для выбора
      * Составление списка категорий/разделов для выбора
      */
      */
-    protected function calcList(int $curForum): void
+    protected function calcList(int $curForum, bool $noUseCurForum = true): void
     {
     {
         $cid     = null;
         $cid     = null;
         $options = [];
         $options = [];
@@ -58,7 +67,7 @@ class Moderate extends Page
 
 
                 $indent = \str_repeat(\ForkBB\__('Forum indent'), $f->depth);
                 $indent = \str_repeat(\ForkBB\__('Forum indent'), $f->depth);
 
 
-                if ($f->redirect_url || $f->id === $curForum) {
+                if ($f->redirect_url || ($noUseCurForum && $f->id === $curForum)) {
                     $options[] = [$f->id, $indent . \ForkBB\__('Forum prefix') . $f->forum_name, true];
                     $options[] = [$f->id, $indent . \ForkBB\__('Forum prefix') . $f->forum_name, true];
                 } else {
                 } else {
                     $options[] = [$f->id, $indent . \ForkBB\__('Forum prefix') . $f->forum_name];
                     $options[] = [$f->id, $indent . \ForkBB\__('Forum prefix') . $f->forum_name];
@@ -81,28 +90,38 @@ class Moderate extends Page
     public function vActionProcess(Validator $v, $action)
     public function vActionProcess(Validator $v, $action)
     {
     {
         if (empty($v->getErrors())) {
         if (empty($v->getErrors())) {
-            $sum = 0;
+            $type = $v->topic ? self::INTOPIC : self::INFORUM;
+            $sum  = 0;
             foreach ($this->actions as $key => $val) {
             foreach ($this->actions as $key => $val) {
                 if (isset($v->{$key})) {
                 if (isset($v->{$key})) {
                     $action = $key;
                     $action = $key;
                     ++$sum;
                     ++$sum;
                 }
                 }
             }
             }
-            // нажата только одна кнопка из доступных
-            if (1 !== $sum) {
+            // нажата не одна кнопка или недоступная кнопка
+            if (1 !== $sum || ! ($type & $this->actions[$action])) {
                 $v->addError('Action not available');
                 $v->addError('Action not available');
+            // не выбрано ни одного сообщения для действий прямо этого требующих
+            } elseif ($v->topic
+                && 1 === \count($v->ids)
+                && ! ((self::TOTOPIC + self::IFTOTPC) & $this->actions[$action])
+            ) {
+                $v->addError('No object selected');
             }
             }
+
             // объединение тем
             // объединение тем
             if ('merge' === $action && \count($v->ids) < 2) {
             if ('merge' === $action && \count($v->ids) < 2) {
                 $v->addError('Not enough topics selected');
                 $v->addError('Not enough topics selected');
-            // перенос тем
-            } elseif ('move' === $action) {
-                $this->calcList($v->forum);
+            // перенос тем или разделение постов
+            } elseif ('move' === $action || 'split' === $action) {
+                $this->calcList($v->forum, 'move' === $action);
 
 
                 if (empty($this->listOfIndexes)) {
                 if (empty($this->listOfIndexes)) {
                     $v->addError('Nowhere to move');
                     $v->addError('Nowhere to move');
                 } elseif (1 === $v->confirm && ! \in_array($v->destination, $this->listOfIndexes)) {
                 } elseif (1 === $v->confirm && ! \in_array($v->destination, $this->listOfIndexes)) {
                     $v->addError('Invalid destination');
                     $v->addError('Invalid destination');
+                } elseif ('split' === $action && 1 === $v->confirm && '' == $v->subject) {
+                    $v->addError('No subject');
                 }
                 }
             }
             }
         }
         }
@@ -132,6 +151,7 @@ class Moderate extends Page
                 'ids.*'       => 'required|integer|min:1|max:9999999999',
                 'ids.*'       => 'required|integer|min:1|max:9999999999',
                 'confirm'     => 'integer',
                 'confirm'     => 'integer',
                 'redirect'    => 'integer',
                 'redirect'    => 'integer',
+                'subject'     => 'string:trim,spaces|min:1|max:70',
                 'destination' => 'integer',
                 'destination' => 'integer',
                 'open'        => 'string',
                 'open'        => 'string',
                 'close'       => 'string',
                 'close'       => 'string',
@@ -139,6 +159,9 @@ class Moderate extends Page
                 'move'        => 'string',
                 'move'        => 'string',
                 'merge'       => 'string',
                 'merge'       => 'string',
                 'cancel'      => 'string',
                 'cancel'      => 'string',
+                'unstick'     => 'string',
+                'stick'       => 'string',
+                'split'       => 'string',
                 'action'      => 'action_process',
                 'action'      => 'action_process',
             ])->addAliases([
             ])->addAliases([
             ])->addArguments([
             ])->addArguments([
@@ -166,7 +189,27 @@ class Moderate extends Page
             if (! $this->curTopic instanceof Topic || $this->curTopic->parent !== $this->curForum) {
             if (! $this->curTopic instanceof Topic || $this->curTopic->parent !== $this->curForum) {
                 return $this->c->Message->message('Bad request');
                 return $this->c->Message->message('Bad request');
             }
             }
-            // посты
+
+            $objects = null;
+            $curType = $this->actions[$v->action];
+            $ids     = $v->ids;
+            $firstId = $this->curTopic->first_post_id;
+            if (self::TOTOPIC & $curType) {
+                $objects = [$this->curTopic];
+            } elseif (self::IFTOTPC & $curType) {
+                if (1 === \count($ids) && \reset($ids) === $firstId) {
+                    $objects = [$this->curTopic];
+                }
+            }
+            if (null === $objects) {
+                $objects = $this->c->posts->loadByIds(\array_diff($ids, [$firstId]), false);
+                foreach ($objects as $post) {
+                    if (! $post instanceof Post || $post->parent !== $this->curTopic) {
+                        return $this->c->Message->message('Bad request');
+                    }
+                }
+                $this->processAsPosts = true;
+            }
 
 
             $this->backLink = $this->c->Router->link('Topic', [
             $this->backLink = $this->c->Router->link('Topic', [
                 'id'   => $this->curTopic->id,
                 'id'   => $this->curTopic->id,
@@ -180,6 +223,7 @@ class Moderate extends Page
                     return $this->c->Message->message('Bad request');
                     return $this->c->Message->message('Bad request');
                 }
                 }
             }
             }
+
             $this->backLink = $this->c->Router->link('Forum', [
             $this->backLink = $this->c->Router->link('Forum', [
                 'id'   => $this->curForum->id,
                 'id'   => $this->curForum->id,
                 'name' => $this->curForum->forum_name,
                 'name' => $this->curForum->forum_name,
@@ -201,7 +245,7 @@ class Moderate extends Page
             case 1:
             case 1:
                 $this->formTitle   = \ForkBB\__('Open topics');
                 $this->formTitle   = \ForkBB\__('Open topics');
                 $this->buttonValue = \ForkBB\__('Open');
                 $this->buttonValue = \ForkBB\__('Open');
-                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $this->curForum);
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
                 $this->form        = $this->formConfirm($topics, $v);
                 $this->form        = $this->formConfirm($topics, $v);
                 return $this;
                 return $this;
             case 2:
             case 2:
@@ -224,7 +268,7 @@ class Moderate extends Page
             case 1:
             case 1:
                 $this->formTitle   = \ForkBB\__('Close topics');
                 $this->formTitle   = \ForkBB\__('Close topics');
                 $this->buttonValue = \ForkBB\__('Close');
                 $this->buttonValue = \ForkBB\__('Close');
-                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $this->curForum);
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
                 $this->form        = $this->formConfirm($topics, $v);
                 $this->form        = $this->formConfirm($topics, $v);
                 return $this;
                 return $this;
             case 2:
             case 2:
@@ -241,11 +285,13 @@ class Moderate extends Page
         }
         }
     }
     }
 
 
-    protected function actionDelete(array $topics, Validator $v): Page
+    protected function actionDelete(array $objects, Validator $v): Page
     {
     {
         if (! $this->user->isAdmin) { //???? разобраться с правами на удаление
         if (! $this->user->isAdmin) { //???? разобраться с правами на удаление
-            foreach ($topics as $topic) {
-                if (isset($this->c->admins->list[$topic->poster_id])) {
+            foreach ($objects as $object) {
+                if (($object instanceof Topic && isset($this->c->admins->list[$object->poster_id]))
+                    || ($object instanceof Post && ! $object->canDelete)
+                ) {
                     return $this->c->Message->message('No permission', true, 403); //???? причина
                     return $this->c->Message->message('No permission', true, 403); //???? причина
                 }
                 }
             }
             }
@@ -253,18 +299,24 @@ class Moderate extends Page
 
 
         switch ($v->step) {
         switch ($v->step) {
             case 1:
             case 1:
-                $this->formTitle   = \ForkBB\__('Delete topics');
+                $this->formTitle   = \ForkBB\__(true === $this->processAsPosts ? 'Delete posts' : 'Delete topics');
                 $this->buttonValue = \ForkBB\__('Delete');
                 $this->buttonValue = \ForkBB\__('Delete');
-                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $this->curForum);
-                $this->form        = $this->formConfirm($topics, $v);
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
+                $this->form        = $this->formConfirm($objects, $v);
                 return $this;
                 return $this;
             case 2:
             case 2:
                 if (1 === $v->confirm) {
                 if (1 === $v->confirm) {
-                    $this->c->topics->delete(...$topics);
+                    if (true === $this->processAsPosts) {
+                        $this->c->posts->delete(...$objects);
+                        $message = 'Delete posts redirect';
+                    } else {
+                        $this->c->topics->delete(...$objects);
+                        $message = 'Delete topics redirect';
+                    }
 
 
-                    return $this->c->Redirect->url($this->curForum->link)->message('Delete topics redirect');
+                    return $this->c->Redirect->url($this->curForum->link)->message($message);
                 } else {
                 } else {
-                    return $this->actionCancel($topics, $v);
+                    return $this->actionCancel($objects, $v);
                 }
                 }
             default:
             default:
                 return $this->c->Message->message('Bad request');
                 return $this->c->Message->message('Bad request');
@@ -277,7 +329,7 @@ class Moderate extends Page
             case 1:
             case 1:
                 $this->formTitle   = \ForkBB\__('Move topics');
                 $this->formTitle   = \ForkBB\__('Move topics');
                 $this->buttonValue = \ForkBB\__('Move');
                 $this->buttonValue = \ForkBB\__('Move');
-                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $this->curForum);
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
                 $this->chkRedirect = true;
                 $this->chkRedirect = true;
                 $this->form        = $this->formConfirm($topics, $v);
                 $this->form        = $this->formConfirm($topics, $v);
                 return $this;
                 return $this;
@@ -330,6 +382,86 @@ class Moderate extends Page
         }
         }
     }
     }
 
 
+    protected function actionUnstick(array $topics, Validator $v): Page
+    {
+        switch ($v->step) {
+            case 1:
+                $this->formTitle   = \ForkBB\__('Unstick topics');
+                $this->buttonValue = \ForkBB\__('Unstick');
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
+                $this->form        = $this->formConfirm($topics, $v);
+                return $this;
+            case 2:
+                if (1 === $v->confirm) {
+                    foreach ($topics as $topic) {
+                        $topic->sticky = 0;
+                        $this->c->topics->update($topic);
+                    }
+
+                    $message = 1 === \count($topics) ? 'Unstick topic redirect' : 'Unstick topics redirect';
+                    return $this->c->Redirect->url($this->backLink)->message($message);
+                } else {
+                    return $this->actionCancel($topics, $v);
+                }
+            default:
+                return $this->c->Message->message('Bad request');
+        }
+    }
+
+    protected function actionStick(array $topics, Validator $v): Page
+    {
+        switch ($v->step) {
+            case 1:
+                $this->formTitle   = \ForkBB\__('Stick topics');
+                $this->buttonValue = \ForkBB\__('Stick');
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $v->topic ? $this->curTopic : $this->curForum);
+                $this->form        = $this->formConfirm($topics, $v);
+                return $this;
+            case 2:
+                if (1 === $v->confirm) {
+                    foreach ($topics as $topic) {
+                        $topic->sticky = 1;
+                        $this->c->topics->update($topic);
+                    }
+
+                    $message = 1 === \count($topics) ? 'Stick topic redirect' : 'Stick topics redirect';
+                    return $this->c->Redirect->url($this->backLink)->message($message);
+                } else {
+                    return $this->actionCancel($topics, $v);
+                }
+            default:
+                return $this->c->Message->message('Bad request');
+        }
+    }
+
+    protected function actionSplit(array $posts, Validator $v): Page
+    {
+        switch ($v->step) {
+            case 1:
+                $this->formTitle   = \ForkBB\__('Split posts');
+                $this->buttonValue = \ForkBB\__('Split');
+                $this->needSubject = true;
+                $this->crumbs      = $this->crumbs($this->formTitle, \ForkBB\__('Moderate'), $this->curTopic);
+                $this->form        = $this->formConfirm($posts, $v);
+                return $this;
+            case 2:
+                if (1 === $v->confirm) {
+                    $newTopic           = $this->c->topics->create();
+                    $newTopic->subject  = $v->subject;
+                    $newTopic->forum_id = $v->forum;
+                    $this->c->topics->insert($newTopic);
+
+                    $this->c->posts->move(false, $newTopic, ...$posts);
+
+                    return $this->c->Redirect->url($this->curForum->link)->message('Split posts redirect');
+                } else {
+                    return $this->actionCancel($posts, $v);
+                }
+            default:
+                return $this->c->Message->message('Bad request');
+        }
+    }
+
     /**
     /**
      * Подготавливает массив данных для формы подтверждения
      * Подготавливает массив данных для формы подтверждения
      *
      *
@@ -346,25 +478,26 @@ class Moderate extends Page
                 'token'  => $this->c->Csrf->create('Moderate'),
                 'token'  => $this->c->Csrf->create('Moderate'),
                 'step'   => $v->step + 1,
                 'step'   => $v->step + 1,
                 'forum'  => $v->forum,
                 'forum'  => $v->forum,
+                'ids'    => $v->ids,
             ],
             ],
             'sets' => [],
             'sets' => [],
             'btns' => [],
             'btns' => [],
         ];
         ];
+        $autofocus = true;
 
 
         if ($v->topic) {
         if ($v->topic) {
             $form['hidden']['topic'] = $v->topic;
             $form['hidden']['topic'] = $v->topic;
         }
         }
 
 
         $headers = [];
         $headers = [];
-        $ids     = [];
         foreach ($objects as $object) {
         foreach ($objects as $object) {
             if ($object instanceof Topic) {
             if ($object instanceof Topic) {
                 $headers[] = \ForkBB\__('Topic «%s»', \ForkBB\cens(($object->subject)));
                 $headers[] = \ForkBB\__('Topic «%s»', \ForkBB\cens(($object->subject)));
-                $ids[]     = $object->id;
+            } else {
+                $headers[] = \ForkBB\__('Post «%1$s by %2$s»', \ForkBB\dt($object->posted), $object->poster);
             }
             }
         }
         }
 
 
-        $form['hidden']['ids'] = $ids;
         $form['sets']['info'] = [
         $form['sets']['info'] = [
             'info' => [
             'info' => [
                 'info1' => [
                 'info1' => [
@@ -383,44 +516,48 @@ class Moderate extends Page
             ];
             ];
         }
         }
 
 
+        $fields = [];
+
+        if ($this->needSubject) {
+            $fields['subject'] = [
+                'type'      => 'text',
+                'maxlength' => 70,
+                'caption'   => \ForkBB\__('New subject'),
+                'required'  => true,
+                'value'     => '' == $v->subject ? $this->curTopic->subject : $v->subject,
+                'autofocus' => $autofocus,
+            ];
+            $autofocus = null;
+        }
+
         if ($this->listForOptions) {
         if ($this->listForOptions) {
-            $form['sets']['destination'] = [
-                'fields' => [
-                    'destination' => [
-                        'type'     => 'select',
-                        'options'  => $this->listForOptions,
-                        'value'    => null,
-                        'caption'  => \ForkBB\__('Move to'),
-                        'required' => true,
-                    ],
-                ],
+            $fields['destination'] = [
+                'type'      => 'select',
+                'options'   => $this->listForOptions,
+                'value'     => null,
+                'caption'   => \ForkBB\__('Move to'),
+                'required'  => true,
+                'autofocus' => $autofocus,
             ];
             ];
+            $autofocus = null;
         }
         }
 
 
         if (true === $this->chkRedirect) {
         if (true === $this->chkRedirect) {
-            $form['sets']['redirect'] = [
-                'fields' => [
-                    'redirect' => [
-                        'type'    => 'checkbox',
-                        'label'   => \ForkBB\__('Leave redirect'),
-                        'value'   => '1',
-                        'checked' => true,
-                    ],
-                ],
+            $fields['redirect'] = [
+                'type'    => 'checkbox',
+                'label'   => \ForkBB\__('Leave redirect'),
+                'value'   => '1',
+                'checked' => true,
             ];
             ];
         }
         }
 
 
-        $form['sets']['confirm'] = [
-            'fields' => [
-                'confirm' => [
-                    'type'    => 'checkbox',
-                    'label'   => \ForkBB\__('Confirm action'),
-                    'value'   => '1',
-                    'checked' => false,
-                ],
-            ],
+        $fields['confirm'] = [
+            'type'    => 'checkbox',
+            'label'   => \ForkBB\__('Confirm action'),
+            'value'   => '1',
+            'checked' => false,
         ];
         ];
-
+        $form['sets']['moderate']['fields'] = $fields;
         $form['btns'][$v->action] = [
         $form['btns'][$v->action] = [
             'type'      => 'submit',
             'type'      => 'submit',
             'value'     => $this->buttonValue,
             'value'     => $this->buttonValue,

+ 73 - 0
app/Models/Pages/Topic.php

@@ -155,6 +155,13 @@ class Topic extends Page
             $this->form     = $this->messageForm(['id' => $topic->id], $topic, 'NewReply', false, false, true);
             $this->form     = $this->messageForm(['id' => $topic->id], $topic, 'NewReply', false, false, true);
         }
         }
 
 
+        if ($this->user->isAdmin || $this->user->isModerator($topic)) {
+            $this->c->Lang->load('misc');
+
+            $this->enableMod = true;
+            $this->formMod   = $this->formMod($topic);
+        }
+
         if ($topic->showViews) {
         if ($topic->showViews) {
             $topic->incViews();
             $topic->incViews();
         }
         }
@@ -162,4 +169,70 @@ class Topic extends Page
 
 
         return $this;
         return $this;
     }
     }
+
+    /**
+     * Создает массив данных для формы модерации
+     *
+     * @param TopicModel $topic
+     *
+     * @return array
+     */
+    protected function formMod(TopicModel $topic): array
+    {
+        $form = [
+            'id'     => 'id-form-mod',
+            'action' => $this->c->Router->link('Moderate'),
+            'hidden' => [
+                'token' => $this->c->Csrf->create('Moderate'),
+                'forum' => $topic->parent->id,
+                'topic' => $topic->id,
+                'page'  => $topic->page,
+                'ids'   => [$topic->first_post_id => $topic->first_post_id],
+                'step'  => 1,
+            ],
+            'sets'   => [],
+            'btns'   => [],
+        ];
+
+        if ($topic->closed) {
+            $form['btns']['open'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Open topic'),
+            ];
+        } else {
+            $form['btns']['close'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Close topic'),
+            ];
+        }
+
+        if ($topic->sticky) {
+            $form['btns']['unstick'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Unstick topic'),
+            ];
+        } else {
+            $form['btns']['stick'] = [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Stick topic'),
+            ];
+        }
+
+        $form['btns'] += [
+            'move' => [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Move topic'),
+            ],
+            'delete' => [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Delete'),
+            ],
+            'split' => [
+                'type'      => 'submit',
+                'value'     => \ForkBB\__('Split'),
+            ],
+        ];
+
+        return $form;
+    }
 }
 }

+ 48 - 0
app/Models/Post/Move.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace ForkBB\Models\Post;
+
+use ForkBB\Models\Action;
+use ForkBB\Models\Topic\Model as Topic;
+use ForkBB\Models\Post\Model as Post;
+
+class Move extends Action
+{
+    /**
+     * Перенос сообщений
+     *
+     * @param bool $useFrom
+     * @param Topic $toTopic
+     * @param Post ...$posts
+     */
+    public function move(bool $useFrom, Topic $toTopic, Post ...$posts): void
+    {
+        $topics = [
+            $toTopic->id => $toTopic,
+        ];
+
+        foreach ($posts as $post) {
+            $topics[$post->topic_id] = $post->parent;
+
+            if ($useFrom) {
+                $post->message = "[from]{$post->parent->subject}[/from]\n" . $post->message;
+            }
+            $post->topic_id = $toTopic->id;
+            $this->c->posts->update($post);
+        }
+
+        //???? переиндексация поискового индекса? для первого сообщения?
+        //???? перерасчет количества тем у пользователей? или нет?
+
+        $forums = [];
+        foreach ($topics as $topic) {
+            $forums[$topic->forum_id] = $topic->parent;
+
+            $this->c->topics->update($topic->calcStat());
+        }
+
+        foreach ($forums as $forum) {
+            $this->c->forums->update($forum->calcStat());
+        }
+    }
+}

+ 1 - 0
app/config/main.dist.php

@@ -262,6 +262,7 @@ return [
         'PostManagerRebuildIndex' => \ForkBB\Models\Post\RebuildIndex::class,
         'PostManagerRebuildIndex' => \ForkBB\Models\Post\RebuildIndex::class,
         'PostManagerUserInfoFromIP' => \ForkBB\Models\Post\UserInfoFromIP::class,
         'PostManagerUserInfoFromIP' => \ForkBB\Models\Post\UserInfoFromIP::class,
         'PostManagerUserStat'     => \ForkBB\Models\Post\UserStat::class,
         'PostManagerUserStat'     => \ForkBB\Models\Post\UserStat::class,
+        'PostManagerMove'         => \ForkBB\Models\Post\Move::class,
 
 
         'ReportModel'             => \ForkBB\Models\Report\Model::class,
         'ReportModel'             => \ForkBB\Models\Report\Model::class,
         'ReportManagerSave'       => \ForkBB\Models\Report\Save::class,
         'ReportManagerSave'       => \ForkBB\Models\Report\Save::class,

+ 3 - 24
app/lang/en/common.po

@@ -364,30 +364,6 @@ msgstr "Jump to"
 msgid "Go"
 msgid "Go"
 msgstr " Go "
 msgstr " Go "
 
 
-msgid "Moderate topic"
-msgstr "Moderate topic"
-
-msgid "All"
-msgstr "All"
-
-msgid "Move topic"
-msgstr "Move topic"
-
-msgid "Open topic"
-msgstr "Open topic"
-
-msgid "Close topic"
-msgstr "Close topic"
-
-msgid "Unstick topic"
-msgstr "Unstick topic"
-
-msgid "Stick topic"
-msgstr "Stick topic"
-
-msgid "Moderate forum"
-msgstr "Moderate forum"
-
 msgid "Powered by"
 msgid "Powered by"
 msgstr "Powered by <a href=\"https://github.com/forkbb\">ForkBB</a>"
 msgstr "Powered by <a href=\"https://github.com/forkbb\">ForkBB</a>"
 
 
@@ -522,3 +498,6 @@ msgstr "Your passphrase"
 
 
 msgid "Invalid passphrase"
 msgid "Invalid passphrase"
 msgstr "Invalid passphrase"
 msgstr "Invalid passphrase"
+
+msgid "Stick topic"
+msgstr "Stick topic"

+ 47 - 2
app/lang/en/misc.po

@@ -93,8 +93,8 @@ msgstr "Your subscription has been removed. Redirecting …"
 msgid "Moderate"
 msgid "Moderate"
 msgstr "Moderate"
 msgstr "Moderate"
 
 
-msgid "Select"
-msgstr "Select"
+msgid "Select for moderation"
+msgstr "Select for moderation"
 
 
 msgid "Move"
 msgid "Move"
 msgstr "Move"
 msgstr "Move"
@@ -171,9 +171,15 @@ msgstr "You must select at least two topics for merge."
 msgid "Stick topic redirect"
 msgid "Stick topic redirect"
 msgstr "Topic sticked. Redirecting …"
 msgstr "Topic sticked. Redirecting …"
 
 
+msgid "Stick topics redirect"
+msgstr "Topics sticked. Redirecting …"
+
 msgid "Unstick topic redirect"
 msgid "Unstick topic redirect"
 msgstr "Topic unsticked. Redirecting …"
 msgstr "Topic unsticked. Redirecting …"
 
 
+msgid "Unstick topics redirect"
+msgstr "Topics unsticked. Redirecting …"
+
 msgid "Merge topics"
 msgid "Merge topics"
 msgstr "Merge topics"
 msgstr "Merge topics"
 
 
@@ -186,6 +192,9 @@ msgstr "Please confirm merge"
 msgid "New subject"
 msgid "New subject"
 msgstr "New subject"
 msgstr "New subject"
 
 
+msgid "No subject"
+msgstr "No subject for new topic."
+
 msgid "Confirm split legend"
 msgid "Confirm split legend"
 msgstr "Please confirm split of selected posts and select destination of move."
 msgstr "Please confirm split of selected posts and select destination of move."
 
 
@@ -234,6 +243,9 @@ msgstr "This post is moved from topic"
 msgid "Topic «%s»"
 msgid "Topic «%s»"
 msgstr "Topic «%s»"
 msgstr "Topic «%s»"
 
 
+msgid "Post «%1$s by %2$s»"
+msgstr "Post «%1$s by %2$s»"
+
 msgid "All posts will be posted in the «%s» topic"
 msgid "All posts will be posted in the «%s» topic"
 msgstr "All posts will be posted in the «%s» topic."
 msgstr "All posts will be posted in the «%s» topic."
 
 
@@ -263,3 +275,36 @@ msgstr ""
 
 
 msgid "Forum indent"
 msgid "Forum indent"
 msgstr "◦ ◦ "
 msgstr "◦ ◦ "
+
+msgid "Moderate topic"
+msgstr "Moderate topic"
+
+msgid "All"
+msgstr "All"
+
+msgid "Move topic"
+msgstr "Move topic"
+
+msgid "Open topic"
+msgstr "Open topic"
+
+msgid "Close topic"
+msgstr "Close topic"
+
+msgid "Unstick topic"
+msgstr "Unstick topic"
+
+msgid "Unstick topics"
+msgstr "Unstick topics"
+
+msgid "Unstick"
+msgstr "Unstick"
+
+msgid "Stick topics"
+msgstr "Stick topics"
+
+msgid "Stick"
+msgstr "Stick"
+
+msgid "Moderate forum"
+msgstr "Moderate forum"

+ 3 - 24
app/lang/ru/common.po

@@ -366,30 +366,6 @@ msgstr "Перейти"
 msgid "Go"
 msgid "Go"
 msgstr " Иди "
 msgstr " Иди "
 
 
-msgid "Moderate topic"
-msgstr "Модерирование темы"
-
-msgid "All"
-msgstr "All"
-
-msgid "Move topic"
-msgstr "Перенести тему"
-
-msgid "Open topic"
-msgstr "Открыть тему"
-
-msgid "Close topic"
-msgstr "Закрыть тему"
-
-msgid "Unstick topic"
-msgstr "Снять выделение темы"
-
-msgid "Stick topic"
-msgstr "Выделить тему"
-
-msgid "Moderate forum"
-msgstr "Модерирование раздела"
-
 msgid "Powered by"
 msgid "Powered by"
 msgstr "Под управлением <a href=\"https://github.com/forkbb\">ForkBB</a>"
 msgstr "Под управлением <a href=\"https://github.com/forkbb\">ForkBB</a>"
 
 
@@ -524,3 +500,6 @@ msgstr "Ваша кодовая фраза"
 
 
 msgid "Invalid passphrase"
 msgid "Invalid passphrase"
 msgstr "Неверная кодовая фраза"
 msgstr "Неверная кодовая фраза"
+
+msgid "Stick topic"
+msgstr "Выделить тему"

+ 47 - 2
app/lang/ru/misc.po

@@ -93,8 +93,8 @@ msgstr "Подписка удалена. Переадресация &hellip;"
 msgid "Moderate"
 msgid "Moderate"
 msgstr "Модерирование"
 msgstr "Модерирование"
 
 
-msgid "Select"
-msgstr "Выбрать"
+msgid "Select for moderation"
+msgstr "Выбрать для модерирования"
 
 
 msgid "Move"
 msgid "Move"
 msgstr "Перенести"
 msgstr "Перенести"
@@ -171,9 +171,15 @@ msgstr "Вы должны выбрать хотя бы две темы для о
 msgid "Stick topic redirect"
 msgid "Stick topic redirect"
 msgstr "Тема выделена. Переадресация &hellip;"
 msgstr "Тема выделена. Переадресация &hellip;"
 
 
+msgid "Stick topics redirect"
+msgstr "Темы выделены. Переадресация &hellip;"
+
 msgid "Unstick topic redirect"
 msgid "Unstick topic redirect"
 msgstr "Выделение темы снято. Переадресация &hellip;"
 msgstr "Выделение темы снято. Переадресация &hellip;"
 
 
+msgid "Unstick topics redirect"
+msgstr "Выделение тем снято. Переадресация &hellip;"
+
 msgid "Merge topics"
 msgid "Merge topics"
 msgstr "Объединение тем"
 msgstr "Объединение тем"
 
 
@@ -186,6 +192,9 @@ msgstr "Пожалуйста, подтвердите объединение"
 msgid "New subject"
 msgid "New subject"
 msgstr "Название новой темы"
 msgstr "Название новой темы"
 
 
+msgid "No subject"
+msgstr "Не задан заголовок для новой темы."
+
 msgid "Confirm split legend"
 msgid "Confirm split legend"
 msgstr "Пожалуйста, подтвердите разделение выбранных сообщений и укажите место переноса."
 msgstr "Пожалуйста, подтвердите разделение выбранных сообщений и укажите место переноса."
 
 
@@ -234,6 +243,9 @@ msgstr "Это сообщение перенесено из темы"
 msgid "Topic «%s»"
 msgid "Topic «%s»"
 msgstr "Тема «%s»"
 msgstr "Тема «%s»"
 
 
+msgid "Post «%1$s by %2$s»"
+msgstr "Сообщение «%1$s от %2$s»"
+
 msgid "All posts will be posted in the «%s» topic"
 msgid "All posts will be posted in the «%s» topic"
 msgstr "Все сообщения будут размещены в теме «%s»."
 msgstr "Все сообщения будут размещены в теме «%s»."
 
 
@@ -263,3 +275,36 @@ msgstr ""
 
 
 msgid "Forum indent"
 msgid "Forum indent"
 msgstr "◦ ◦ "
 msgstr "◦ ◦ "
+
+msgid "Moderate topic"
+msgstr "Модерирование темы"
+
+msgid "All"
+msgstr "All"
+
+msgid "Move topic"
+msgstr "Перенести тему"
+
+msgid "Open topic"
+msgstr "Открыть тему"
+
+msgid "Close topic"
+msgstr "Закрыть тему"
+
+msgid "Unstick topic"
+msgstr "Снять выделение темы"
+
+msgid "Unstick topics"
+msgstr "Снятие выделение тем"
+
+msgid "Unstick"
+msgstr "Снять выделение"
+
+msgid "Stick topics"
+msgstr "Выделение тем"
+
+msgid "Stick"
+msgstr "Выделить"
+
+msgid "Moderate forum"
+msgstr "Модерирование раздела"

+ 2 - 2
app/templates/forum.forkbb.php

@@ -86,7 +86,7 @@
             <div class="f-cell f-cmain">
             <div class="f-cell f-cmain">
             @if ($p->enableMod)
             @if ($p->enableMod)
               <input id="checkbox-{!! $topic->id !!}" class="f-fch" type="checkbox" name="ids[{!! $topic->id !!}]" value="{!! $topic->id !!}" form="id-form-mod">
               <input id="checkbox-{!! $topic->id !!}" class="f-fch" type="checkbox" name="ids[{!! $topic->id !!}]" value="{!! $topic->id !!}" form="id-form-mod">
-              <label class="f-ficon" for="checkbox-{!! $topic->id !!}"></label>
+              <label class="f-ficon" for="checkbox-{!! $topic->id !!}" title="{{ __('Select for moderation') }}"></label>
             @else
             @else
               <div class="f-ficon"></div>
               <div class="f-ficon"></div>
             @endif
             @endif
@@ -100,7 +100,7 @@
             <div class="f-cell f-cmain">
             <div class="f-cell f-cmain">
             @if ($p->enableMod)
             @if ($p->enableMod)
               <input id="checkbox-{!! $topic->id !!}" class="f-fch" type="checkbox" name="ids[{!! $topic->id !!}]" value="{!! $topic->id !!}" form="id-form-mod">
               <input id="checkbox-{!! $topic->id !!}" class="f-fch" type="checkbox" name="ids[{!! $topic->id !!}]" value="{!! $topic->id !!}" form="id-form-mod">
-              <label class="f-ficon" for="checkbox-{!! $topic->id !!}"></label>
+              <label class="f-ficon" for="checkbox-{!! $topic->id !!}" title="{{ __('Select for moderation') }}"></label>
             @else
             @else
               <div class="f-ficon"></div>
               <div class="f-ficon"></div>
             @endif
             @endif

+ 15 - 0
app/templates/topic.forkbb.php

@@ -60,9 +60,16 @@
         @include ('layouts/iswev')
         @include ('layouts/iswev')
     @else
     @else
       <article id="p{!! $post->id !!}" class="f-post @if (1 == $post->user->gender) f-user-male @elseif (2 == $post->user->gender) f-user-female @endif @if ($post->user->online) f-user-online @endif @if (1 === $post->postNumber) f-post-first @endif">
       <article id="p{!! $post->id !!}" class="f-post @if (1 == $post->user->gender) f-user-male @elseif (2 == $post->user->gender) f-user-female @endif @if ($post->user->online) f-user-online @endif @if (1 === $post->postNumber) f-post-first @endif">
+        @if ($p->enableMod && $post->postNumber > 1)
+        <input id="checkbox-{!! $post->id !!}" class="f-post-checkbox" type="checkbox" name="ids[{!! $post->id !!}]" value="{!! $post->id !!}" form="id-form-mod">
+        @endif
         <header class="f-post-header">
         <header class="f-post-header">
           <h3>@if ($post->postNumber > 1) {!! __('Re') !!} @endif {{ cens($p->model->subject) }}</h3>
           <h3>@if ($post->postNumber > 1) {!! __('Re') !!} @endif {{ cens($p->model->subject) }}</h3>
+        @if ($p->enableMod && $post->postNumber > 1)
+          <label class="f-post-posted" for="checkbox-{!! $post->id !!}" title="{{ __('Select for moderation') }}"><time datetime="{{ utc($post->posted) }}">{{ dt($post->posted) }}</time></label>
+        @else
           <span class="f-post-posted"><time datetime="{{ utc($post->posted) }}">{{ dt($post->posted) }}</time></span>
           <span class="f-post-posted"><time datetime="{{ utc($post->posted) }}">{{ dt($post->posted) }}</time></span>
+        @endif
         @if ($post->edited)
         @if ($post->edited)
           <span class="f-post-edited" title="{!! __('Last edit', $post->edited_by, dt($post->edited)) !!}">{!! __('Edited') !!}</span>
           <span class="f-post-edited" title="{!! __('Last edit', $post->edited_by, dt($post->edited)) !!}">{!! __('Edited') !!}</span>
         @endif
         @endif
@@ -164,3 +171,11 @@
       </div>
       </div>
     </section>
     </section>
 @endif
 @endif
+@if ($p->enableMod && $form = $p->formMod)
+    <section class="f-moderate">
+      <h2>{!! __('Moderate') !!}</h2>
+      <div class="f-fdivm">
+    @include ('layouts/form')
+      </div>
+    </section>
+@endif

+ 30 - 17
public/style/ForkBB/style.css

@@ -774,6 +774,7 @@ body,
 #fork .f-nav-links .f-actions-links {
 #fork .f-nav-links .f-actions-links {
   margin-left: auto;
   margin-left: auto;
   order: 1;
   order: 1;
+  text-align: right;
 }
 }
 
 
 #fork .f-nav-links .f-page {
 #fork .f-nav-links .f-page {
@@ -1176,23 +1177,6 @@ body,
   width: 2rem;
   width: 2rem;
 }
 }
 
 
-#fork .f-ftlist .f-fch {
-  display: none;
-}
-
-#fork .f-ftlist .f-fch + .f-ficon {
-  position: relative;
-  cursor: pointer;
-}
-
-#fork .f-ftlist .f-fch:checked + .f-ficon:after {
-  content: "\2713";
-  color: red;
-  position: absolute;
-  bottom: 0;
-  left: 0;
-}
-
 #fork .f-ftlist .f-finfo {
 #fork .f-ftlist .f-finfo {
   width: calc(100% - 2rem);
   width: calc(100% - 2rem);
 }
 }
@@ -1797,6 +1781,35 @@ body,
   display: inline-block;
   display: inline-block;
 }
 }
 
 
+#fork .f-ftlist .f-fch,
+#fork .f-post-checkbox {
+  display: none;
+}
+
+#fork .f-ftlist label.f-ficon {
+  position: relative;
+  cursor: pointer;
+}
+
+#fork .f-post-checkbox + .f-post-header {
+  position: relative;
+}
+
+#fork label.f-post-posted {
+  cursor: pointer;
+}
+
+#fork .f-ftlist .f-fch:checked + .f-ficon:after,
+#fork .f-post-checkbox:checked + .f-post-header:before {
+  content: "✔";
+  color: red;
+  position: absolute;
+  bottom: 0;
+  left: 0.625rem;
+  font-size: 1rem;
+  font-weight: bold;
+}
+
 /*********************/
 /*********************/
 /* Админка/Категории */
 /* Админка/Категории */
 /*********************/
 /*********************/