From 86504cf65b83145bffb9c2bfc487373049b76722 Mon Sep 17 00:00:00 2001 From: Marcus Eibrink-Lunzenauer Date: Tue, 13 Jun 2023 06:18:55 +0000 Subject: Refactor Blubber using vue, closes #1695 Closes #1695 Merge request studip/studip!1791 --- app/controllers/blubber.php | 150 +++--- app/controllers/course/messenger.php | 47 +- app/views/blubber/dialog.php | 13 +- app/views/blubber/index.php | 25 +- .../JsonApi/JsonApiIntegration/QueryChecker.php | 9 +- lib/classes/JsonApi/RouteMap.php | 11 +- lib/classes/JsonApi/Routes/Blubber/Authority.php | 5 + .../Routes/Blubber/CommentsByThreadIndex.php | 20 +- .../JsonApi/Routes/Blubber/CommentsCreate.php | 30 +- .../JsonApi/Routes/Blubber/Rel/DefaultThread.php | 128 +++++ lib/classes/JsonApi/Routes/Blubber/SortTrait.php | 15 + .../JsonApi/Routes/Blubber/ThreadsUpdate.php | 56 +++ lib/classes/JsonApi/Schemas/BlubberComment.php | 4 + lib/classes/JsonApi/Schemas/BlubberThread.php | 75 ++- lib/classes/JsonApi/Schemas/User.php | 27 +- lib/classes/sidebar/BlubberThreadsWidget.php | 61 --- lib/models/BlubberThread.php | 25 +- resources/assets/javascripts/chunk-loader.js | 4 +- resources/assets/javascripts/entry-base.js | 1 - resources/assets/javascripts/init.js | 3 + resources/assets/javascripts/lib/blubber.js | 266 +++-------- resources/assets/javascripts/lib/jsupdater.js | 6 + resources/assets/stylesheets/scss/blubber.scss | 2 + resources/vue/components/BlubberGlobalstream.vue | 130 ----- resources/vue/components/BlubberPublicComposer.vue | 98 ---- resources/vue/components/BlubberThread.vue | 526 --------------------- resources/vue/components/BlubberThreadWidget.vue | 116 ----- resources/vue/components/SidebarWidget.vue | 25 +- resources/vue/components/blubber/Comment.vue | 118 +++++ resources/vue/components/blubber/CommunityPage.vue | 89 ++++ resources/vue/components/blubber/Composer.vue | 126 +++++ resources/vue/components/blubber/DialogPanel.vue | 36 ++ resources/vue/components/blubber/Panel.vue | 172 +++++++ resources/vue/components/blubber/SearchWidget.vue | 62 +++ resources/vue/components/blubber/SideInfo.vue | 16 + resources/vue/components/blubber/Thread.vue | 252 ++++++++++ .../vue/components/blubber/ThreadSubscriber.vue | 32 ++ resources/vue/components/blubber/ThreadsWidget.vue | 109 +++++ resources/vue/components/blubber/components.js | 10 + resources/vue/plugins/blubber.js | 63 +++ resources/vue/store/blubber.js | 364 ++++++++++++++ templates/blubber/threads-overview.php | 16 - tests/jsonapi/BlubberThreadsCreateTest.php | 6 + tests/jsonapi/BlubberThreadsIndexTest.php | 6 + tests/jsonapi/BlubberThreadsShowTest.php | 6 + tests/jsonapi/_bootstrap.php | 3 + 46 files changed, 2042 insertions(+), 1322 deletions(-) create mode 100644 lib/classes/JsonApi/Routes/Blubber/Rel/DefaultThread.php create mode 100644 lib/classes/JsonApi/Routes/Blubber/SortTrait.php create mode 100644 lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php delete mode 100644 lib/classes/sidebar/BlubberThreadsWidget.php delete mode 100644 resources/vue/components/BlubberGlobalstream.vue delete mode 100644 resources/vue/components/BlubberPublicComposer.vue delete mode 100644 resources/vue/components/BlubberThread.vue delete mode 100644 resources/vue/components/BlubberThreadWidget.vue create mode 100644 resources/vue/components/blubber/Comment.vue create mode 100644 resources/vue/components/blubber/CommunityPage.vue create mode 100644 resources/vue/components/blubber/Composer.vue create mode 100644 resources/vue/components/blubber/DialogPanel.vue create mode 100644 resources/vue/components/blubber/Panel.vue create mode 100644 resources/vue/components/blubber/SearchWidget.vue create mode 100644 resources/vue/components/blubber/SideInfo.vue create mode 100644 resources/vue/components/blubber/Thread.vue create mode 100644 resources/vue/components/blubber/ThreadSubscriber.vue create mode 100644 resources/vue/components/blubber/ThreadsWidget.vue create mode 100644 resources/vue/components/blubber/components.js create mode 100644 resources/vue/plugins/blubber.js create mode 100644 resources/vue/store/blubber.js delete mode 100644 templates/blubber/threads-overview.php diff --git a/app/controllers/blubber.php b/app/controllers/blubber.php index d89916c..71d89f3 100644 --- a/app/controllers/blubber.php +++ b/app/controllers/blubber.php @@ -15,20 +15,16 @@ class BlubberController extends AuthenticatedController public function index_action($thread_id = null) { - Navigation::activateItem('/community/blubber'); - - $this->threads = BlubberThread::findMyGlobalThreads( - 51, - null, - null, - null, - Request::get("search") - ); + if (Navigation::hasItem('/community/blubber')) { + Navigation::activateItem('/community/blubber'); + } + + $this->search = Request::get('search'); + $this->threads = BlubberThread::findMyGlobalThreads(21, null, null, null, $this->search); if (count($this->threads) > 20) { array_pop($this->threads); $this->threads_more_down = 1; } - if ($thread_id) { $GLOBALS['user']->cfg->store('BLUBBER_DEFAULT_THREAD', $thread_id); } else { @@ -47,14 +43,8 @@ class BlubberController extends AuthenticatedController $this->thread = array_pop($threads); } - $this->thread_data = []; if ($this->thread) { $this->thread->markAsRead(); - $this->thread_data = $this->thread->getJSONData( - 50, - null, - Request::get("search") - ); } if ( @@ -62,19 +52,20 @@ class BlubberController extends AuthenticatedController && !Avatar::getAvatar($GLOBALS['user']->id)->is_customized() ) { $_SESSION['already_asked_for_avatar'] = true; - PageLayout::postInfo(sprintf( - _('Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s.'), - 'id) . '" data-dialog>', - '' - )); - } - - if (Request::isDialog()) { - PageLayout::setTitle($this->thread->getName()); + PageLayout::postInfo( + sprintf( + _('Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s.'), + '', + '' + ) + ); } $this->buildSidebar(); if (Request::isDialog()) { + PageLayout::setTitle($this->thread->getName()); $this->render_template('blubber/dialog'); } } @@ -111,7 +102,7 @@ class BlubberController extends AuthenticatedController $statement = DBManager::get()->prepare($query); $statement->execute([ 'me' => $GLOBALS['user']->id, - 'friend' => $user_ids[0] + 'friend' => $user_ids[0], ]); $thread_id = $statement->fetchColumn(); if ($thread_id) { @@ -141,16 +132,19 @@ class BlubberController extends AuthenticatedController foreach ($user_ids as $user_id) { $insert->execute([ 'thread_id' => $blubber->getId(), - 'user_id' => $user_id, + 'user_id' => $user_id, ]); } $this->redirect("blubber/index/{$blubber->getId()}"); return; } - $this->contacts = Contact::findBySQL("JOIN auth_user_md5 USING (user_id) WHERE owner_id = ? ORDER BY auth_user_md5.Nachname ASC, auth_user_md5.Vorname ASC", [ - $GLOBALS['user']->id - ]); + $this->contacts = Contact::findBySQL( + "JOIN auth_user_md5 USING (user_id) + WHERE owner_id = ? + ORDER BY auth_user_md5.Nachname, auth_user_md5.Vorname", + [$GLOBALS['user']->id] + ); } public function delete_action($thread_id) @@ -164,7 +158,7 @@ class BlubberController extends AuthenticatedController $this->thread->delete(); PageLayout::postSuccess(_('Der Blubber wurde gelöscht.')); } - $this->redirect("blubber/index"); + $this->redirect('blubber/index'); return; } @@ -194,7 +188,7 @@ class BlubberController extends AuthenticatedController LIMIT 1"; $statement = DBManager::get()->prepare($query); $statement->execute([ - 'me' => $GLOBALS['user']->id, + 'me' => $GLOBALS['user']->id, 'friend' => $user_ids[0], ]); $thread_id = $statement->fetchColumn(); @@ -225,7 +219,7 @@ class BlubberController extends AuthenticatedController foreach ($user_ids as $user_id) { $insert->execute([ 'thread_id' => $blubber->getId(), - 'user_id' => $user_id, + 'user_id' => $user_id, ]); } $this->redirect("blubber/index/{$blubber->getId()}"); @@ -265,8 +259,12 @@ class BlubberController extends AuthenticatedController { $context = Request::get('context', $GLOBALS['user']->id); $context_type = Request::option('context_type'); - if (!Request::isPost() - || ($context_type === 'course' && !$GLOBALS['perm']->have_studip_perm('autor', $context)) + if ( + !Request::isPost() + || ( + $context_type === 'course' + && !$GLOBALS['perm']->have_studip_perm('autor', $context) + ) ) { throw new AccessDeniedException(); } @@ -276,7 +274,6 @@ class BlubberController extends AuthenticatedController $newfile = null; //is filled below $file_ref = null; //is also filled below - if ($file['size']) { $document['user_id'] = $GLOBALS['user']->id; $document['filesize'] = $file['size']; @@ -290,11 +287,10 @@ class BlubberController extends AuthenticatedController AND data_content = :content", [ 'parent_id' => $root_dir->getId(), - 'content' => json_encode(['Blubber']), + 'content' => json_encode(['Blubber']), ] ); - if ($blubber_directory) { $blubber_directory = $blubber_directory->getTypedFolder(); } else { @@ -321,10 +317,10 @@ class BlubberController extends AuthenticatedController $uploaded = FileManager::handleFileUpload( [ 'tmp_name' => [$file['tmp_name']], - 'name' => [$file['name']], - 'size' => [$file['size']], - 'type' => [$file['type']], - 'error' => [$file['error']] + 'name' => [$file['name']], + 'size' => [$file['size']], + 'type' => [$file['type']], + 'error' => [$file['error']], ], $blubber_directory, $GLOBALS['user']->id @@ -332,7 +328,7 @@ class BlubberController extends AuthenticatedController if ($uploaded['error']) { throw new Exception(implode("\n", $uploaded['error'])); - } elseif($uploaded['files'][0]) { + } elseif ($uploaded['files'][0]) { $oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']); $url = $uploaded['files'][0]->getDownloadURL(); URLHelper::setBaseURL($oldbase); @@ -340,14 +336,12 @@ class BlubberController extends AuthenticatedController } else { throw new Exception('File cannot be created!'); } - } } catch (Exception $e) { $output['errors'][] = $e->getMessage(); $success = false; } - if ($success) { $type = null; @@ -387,12 +381,9 @@ class BlubberController extends AuthenticatedController $statement = DBManager::get()->prepare($query); $statement->execute([ 'thread_id' => $thread_id, - 'user_id' => Request::option('user_id'), + 'user_id' => Request::option('user_id'), ]); - $this->response->add_header( - 'X-Dialog-Execute', - 'STUDIP.Blubber.refreshThread' - ); + $this->response->add_header('X-Dialog-Execute', 'STUDIP.Blubber.refreshThread'); $this->response->add_header('X-Dialog-Close', '1'); $this->render_json([ 'thread_id' => $thread_id, @@ -405,7 +396,7 @@ class BlubberController extends AuthenticatedController if ($this->thread['context_type'] !== 'private' || !$this->thread->isReadable()) { throw new AccessDeniedException(); } - PageLayout::setTitle(_("Studiengruppe aus Konversation erstellen")); + PageLayout::setTitle(_('Studiengruppe aus Konversation erstellen')); if (Request::isPost() && count(studygroup_sem_types())) { $studgroup_sem_types = studygroup_sem_types(); $course = new Course(); @@ -436,7 +427,9 @@ class BlubberController extends AuthenticatedController $this->thread->store(); PluginManager::getInstance()->setPluginActivated( - PluginManager::getInstance()->getPlugin('Blubber')->getPluginId(), + PluginManager::getInstance() + ->getPlugin('Blubber') + ->getPluginId(), $course->getId(), true ); @@ -451,62 +444,47 @@ class BlubberController extends AuthenticatedController if ($this->thread['context_type'] !== 'private' || !$this->thread->isReadable()) { throw new AccessDeniedException(); } - PageLayout::setTitle(_("Private Konversation verlassen")); + PageLayout::setTitle(_('Private Konversation verlassen')); if (Request::isPost()) { BlubberMention::deleteBySQL("user_id = :me AND external_contact = '0' AND thread_id = :thread_id", [ 'thread_id' => $this->thread->getId(), - 'me' => $GLOBALS['user']->id + 'me' => $GLOBALS['user']->id, ]); - if (Request::get("delete_comments")) { + if (Request::get('delete_comments')) { BlubberComment::deleteBySQL("thread_id = :thread_id AND user_id = :me AND external_contact = '0'", [ 'thread_id' => $this->thread->getId(), - 'me' => $GLOBALS['user']->id + 'me' => $GLOBALS['user']->id, ]); } if ($this->thread['user_id'] === $GLOBALS['user']->id) { - $this->thread['content'] = ""; + $this->thread['content'] = ''; $this->thread->store(); } - $count_departed = BlubberMention::countBySQL("INNER JOIN auth_user_md5 USING (user_id) WHERE external_contact = '0' AND thread_id = :thread_id", [ - 'thread_id' => $this->thread->getId() - ]); + $count_departed = BlubberMention::countBySQL( + "JOIN auth_user_md5 USING (user_id) + WHERE external_contact = 0 AND thread_id = :thread_id", + [ + 'thread_id' => $this->thread->getId(), + ] + ); $count_comments = BlubberComment::countBySQL("thread_id = :thread_id AND external_contact = '0'", [ - 'thread_id' => $this->thread->getId() + 'thread_id' => $this->thread->getId(), ]); if (!$count_departed || (!$count_comments && !$this->thread['content'])) { //ich mache das Licht aus: $this->thread->delete(); - PageLayout::postSuccess(_("Private Konversation gelöscht.")); + PageLayout::postSuccess(_('Private Konversation gelöscht.')); } else { - PageLayout::postSuccess(_("Private Konversation verlassen.")); + PageLayout::postSuccess(_('Private Konversation verlassen.')); } - $this->redirect("blubber/index"); + $this->redirect('blubber/index'); } } protected function buildSidebar() { - $search = new SearchWidget("#"); - $search->addNeedle( - _("Suche nach ..."), - "search", - true - ); - - Sidebar::Get()->addWidget($search, "blubbersearch"); - - $threads_widget = Sidebar::Get()->addWidget( - new BlubberThreadsWidget(), - 'threads' - ); - foreach ($this->threads as $thread) { - $threads_widget->addThread($thread); - } - - if ($this->thread) { - $threads_widget->setActive($this->thread->getId()); - } - - $threads_widget->withComposer(); + $sidebar = Sidebar::Get(); + $sidebar->addWidget(new VueWidget('blubber-search-widget')); + $sidebar->addWidget(new VueWidget('blubber-threads-widget')); } } diff --git a/app/controllers/course/messenger.php b/app/controllers/course/messenger.php index 79db849..8acd24b 100644 --- a/app/controllers/course/messenger.php +++ b/app/controllers/course/messenger.php @@ -6,7 +6,8 @@ class Course_MessengerController extends AuthenticatedController parent::before_filter($action, $args); PageLayout::setBodyElementId('blubber-index'); - PageLayout::setHelpKeyword("Basis/InteraktionBlubber"); + PageLayout::setHelpKeyword('Basis/InteraktionBlubber'); + PageLayout::setTitle(_('Blubber')); } public function course_action($thread_id = null) @@ -17,6 +18,7 @@ class Course_MessengerController extends AuthenticatedController Navigation::activateItem('/course/blubber'); } + $this->search = ''; $this->threads = BlubberThread::findByContext(Context::get()->id, true, Context::getType()); $this->thread = null; $this->threads_more_down = 0; @@ -32,17 +34,26 @@ class Course_MessengerController extends AuthenticatedController } } } - if (!$this->thread || Request::get("thread") === "new") { + if (!$this->thread || Request::get('thread') === 'new') { $threads = array_reverse($this->threads); $this->thread = array_pop($threads); } - $this->thread->markAsRead(); - $this->thread_data = $this->thread->getJSONData(); - $_SESSION['already_asked_for_avatar'] = false; - if (!Avatar::getAvatar($GLOBALS['user']->id)->is_customized() && !$_SESSION['already_asked_for_avatar']) { + if ($this->thread) { + $this->thread->markAsRead(); + } + + if (!Avatar::getAvatar($GLOBALS['user']->id)->is_customized()) { $_SESSION['already_asked_for_avatar'] = true; - PageLayout::postInfo(sprintf(_("Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s."), 'id).'" data-dialog>', '')); + PageLayout::postInfo( + sprintf( + _('Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s.'), + '', + '' + ) + ); } $this->buildSidebar(); @@ -57,25 +68,7 @@ class Course_MessengerController extends AuthenticatedController protected function buildSidebar() { $sidebar = Sidebar::Get(); - $search = new SearchWidget("#"); - $search->addNeedle( - _("Suche nach ..."), - "search", - true, - null, - null, - null, - [] - ); - $sidebar->addWidget($search, "blubbersearch"); - - $threads_widget = new BlubberThreadsWidget(); - foreach ($this->threads as $thread) { - $threads_widget->addThread($thread); - } - if ($this->thread) { - $threads_widget->setActive($this->thread->getId()); - } - $sidebar->addWidget($threads_widget, "threads"); + $sidebar->addWidget(new VueWidget('blubber-search-widget')); + $sidebar->addWidget(new VueWidget('blubber-threads-widget')); } } diff --git a/app/views/blubber/dialog.php b/app/views/blubber/dialog.php index 6be9bfd..f790a5d 100644 --- a/app/views/blubber/dialog.php +++ b/app/views/blubber/dialog.php @@ -1,12 +1,5 @@ -
- -
- -
-
+render_partial('blubber/index') ?>
- getURL()) ?> -
\ No newline at end of file + getURL()) ?> + diff --git a/app/views/blubber/index.php b/app/views/blubber/index.php index 0055f97..e168f4a 100644 --- a/app/views/blubber/index.php +++ b/app/views/blubber/index.php @@ -1,21 +1,6 @@
- -
- -
- -
-
-
- -
{{ thread_data.thread_posting.user_name }}
-
-
-
-
-
-
+ !empty($thread) ? $thread->getId() : '', + 'data-search' => $search, + ]) ?> +> diff --git a/lib/classes/JsonApi/JsonApiIntegration/QueryChecker.php b/lib/classes/JsonApi/JsonApiIntegration/QueryChecker.php index ff2a03d..045598b 100644 --- a/lib/classes/JsonApi/JsonApiIntegration/QueryChecker.php +++ b/lib/classes/JsonApi/JsonApiIntegration/QueryChecker.php @@ -104,12 +104,13 @@ class QueryChecker protected function checkSorting(ErrorCollection $errors, QueryParserInterface $queryParser): void { - if (null !== $queryParser->getSorts() && null !== $this->sortParameters) { - foreach ($queryParser->getSorts() as $sortParameter) { - if (!array_key_exists($sortParameter->getField(), $this->sortParameters)) { + $sorts = iterator_to_array($queryParser->getSorts()); + if (null !== $sorts && null !== $this->sortParameters) { + foreach (array_keys($sorts) as $sortParameter) { + if (!array_key_exists($sortParameter, $this->sortParameters)) { $errors->addQueryParameterError( QueryParser::PARAM_SORT, - sprintf('Sort parameter %s is not allowed.', $sortParameter->getField()) + sprintf('Sort parameter %s is not allowed.', $sortParameter) ); } } diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index bffa13a..bef6327 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -173,21 +173,30 @@ class RouteMap $group->get('/users/{id}/blubber-threads', Routes\Blubber\ThreadsIndex::class)->setArgument('type', 'private'); $group->get('/blubber-threads', Routes\Blubber\ThreadsIndex::class)->setArgument('type', 'all'); $group->get('/blubber-threads/{id}', Routes\Blubber\ThreadsShow::class); + $group->patch('/blubber-threads/{id}', Routes\Blubber\ThreadsUpdate::class); // create, read, update and delete BlubberComments $group->get('/blubber-threads/{id}/comments', Routes\Blubber\CommentsByThreadIndex::class); $group->post('/blubber-threads/{id}/comments', Routes\Blubber\CommentsCreate::class); $group->get('/blubber-comments', Routes\Blubber\CommentsIndex::class); $group->get('/blubber-comments/{id}', Routes\Blubber\CommentsShow::class); + $group->post('/blubber-comments', Routes\Blubber\CommentsCreate::class); $group->patch('/blubber-comments/{id}', Routes\Blubber\CommentsUpdate::class); $group->delete('/blubber-comments/{id}', Routes\Blubber\CommentsDelete::class); - // REL mentions + // REL blubber-threads > mentions $this->addRelationship( $group, '/blubber-threads/{id}/relationships/mentions', Routes\Blubber\Rel\Mentions::class ); + + // REL users > blubber-default-thread + $this->addRelationship( + $group, + '/users/{id}/relationships/blubber-default-thread', + Routes\Blubber\Rel\DefaultThread::class + ); } private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void diff --git a/lib/classes/JsonApi/Routes/Blubber/Authority.php b/lib/classes/JsonApi/Routes/Blubber/Authority.php index 8ab431e..9d4cf67 100644 --- a/lib/classes/JsonApi/Routes/Blubber/Authority.php +++ b/lib/classes/JsonApi/Routes/Blubber/Authority.php @@ -14,6 +14,11 @@ class Authority return self::userIsAuthor($user) && $resource->isReadable($user->id); } + public static function canEditBlubberThread(User $user, BlubberThread $resource): bool + { + return self::canShowBlubberThread($user, $resource); + } + public static function canCreatePrivateBlubberThread(User $user) { return self::userIsAuthor($user); diff --git a/lib/classes/JsonApi/Routes/Blubber/CommentsByThreadIndex.php b/lib/classes/JsonApi/Routes/Blubber/CommentsByThreadIndex.php index f3222ef..fb8b50d 100644 --- a/lib/classes/JsonApi/Routes/Blubber/CommentsByThreadIndex.php +++ b/lib/classes/JsonApi/Routes/Blubber/CommentsByThreadIndex.php @@ -14,11 +14,14 @@ use Psr\Http\Message\ServerRequestInterface as Request; */ class CommentsByThreadIndex extends JsonApiController { - use TimestampTrait, FilterTrait; + use FilterTrait; + use SortTrait; + use TimestampTrait; protected $allowedFilteringParameters = ['since', 'before', 'search']; - protected $allowedIncludePaths = ['author', 'mentions', 'thread']; + protected $allowedIncludePaths = ['author', 'mentions', 'thread', 'thread.author']; protected $allowedPagingParameters = ['offset', 'limit']; + protected $allowedSortFields = ['mkdate']; /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -49,12 +52,12 @@ class CommentsByThreadIndex extends JsonApiController $params = ['thread_id' => $thread->id]; if (isset($filters['before'])) { - $query .= ' AND mkdate <= :before'; + $query .= ' AND mkdate < :before'; $params['before'] = $filters['before']; } if (isset($filters['since'])) { - $query .= ' AND mkdate >= :since'; + $query .= ' AND mkdate > :since'; $params['since'] = $filters['since']; } @@ -63,7 +66,14 @@ class CommentsByThreadIndex extends JsonApiController $params['search'] = '%' . $filters['search'] . '%'; } - $query .= ' ORDER BY mkdate ASC LIMIT :limit OFFSET :offset'; + $sortParameters = $this->getSortParameters(); + if (empty($sortParameters)) { + $query .= ' ORDER BY mkdate'; + } elseif (array_key_exists('mkdate', $sortParameters)) { + $query .= ' ORDER BY mkdate ' . ($sortParameters['mkdate'] ? 'ASC' : 'DESC'); + } + + $query .= ' LIMIT :limit OFFSET :offset'; $params['limit'] = $limit + 1; $params['offset'] = $offset; diff --git a/lib/classes/JsonApi/Routes/Blubber/CommentsCreate.php b/lib/classes/JsonApi/Routes/Blubber/CommentsCreate.php index 3548645..6d95020 100644 --- a/lib/classes/JsonApi/Routes/Blubber/CommentsCreate.php +++ b/lib/classes/JsonApi/Routes/Blubber/CommentsCreate.php @@ -9,7 +9,7 @@ use JsonApi\Errors\BadRequestException; use JsonApi\Errors\InternalServerError; use JsonApi\Errors\RecordNotFoundException; use JsonApi\JsonApiController; - +use JsonApi\Schemas\BlubberThread as ThreadSchema; use JsonApi\Routes\ValidationTrait; /** @@ -24,9 +24,15 @@ class CommentsCreate extends JsonApiController */ public function __invoke(Request $request, Response $response, $args) { - $json = $this->validate($request); + if (isset($args['id'])) { + $json = $this->validate($request, $args['id']); + $thread = \BlubberThread::find($args['id']); + } else { + $json = $this->validate($request, null); + $thread = $this->getThreadFromJson($json); + } - if (!($thread = \BlubberThread::find($args['id']))) { + if (!$thread) { throw new RecordNotFoundException(); } @@ -40,16 +46,30 @@ class CommentsCreate extends JsonApiController 'thread_id' => $thread->id, 'content' => $content, 'user_id' => $user->id, - 'external_contact' => 0 + 'external_contact' => 0, ]); return $this->getCreatedResponse($comment); } - protected function validateResourceDocument($json, $data) + protected function validateResourceDocument($json, $id = null) { if (empty(self::arrayGet($json, 'data.attributes.content'))) { return 'Comment should not be empty.'; } + if (!$id && !$this->getThreadFromJson($json)) { + return 'Invalid `block` relationship.'; + } + } + + private function getThreadFromJson($json) + { + $relationship = 'thread'; + if (!$this->validateResourceObject($json, 'data.relationships.' . $relationship, ThreadSchema::TYPE)) { + return null; + } + $resourceId = self::arrayGet($json, 'data.relationships.' . $relationship . '.data.id'); + + return \BlubberThread::find($resourceId); } } diff --git a/lib/classes/JsonApi/Routes/Blubber/Rel/DefaultThread.php b/lib/classes/JsonApi/Routes/Blubber/Rel/DefaultThread.php new file mode 100644 index 0000000..91eb3b8 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Blubber/Rel/DefaultThread.php @@ -0,0 +1,128 @@ +getConfiguration()->getValue('BLUBBER_DEFAULT_THREAD'); + $thread = \BlubberThread::find($threadId); + + return $this->getIdentifiersResponse($thread); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function replaceRelationship(Request $request, $related) + { + $json = $this->validate($request); + $thread = isset($json['data']) ? $this->validateBlubberThread($related, $json) : null; + $this->replaceBlubberDefaultThread($related, $thread); + + return $this->getCodeResponse(204); + } + + private function replaceBlubberDefaultThread(\User $related, $threadOrNull) + { + $related->getConfiguration()->store('BLUBBER_DEFAULT_THREAD', $threadOrNull ? $threadOrNull->id : null); + } + + protected function findRelated(array $args) + { + $user = \User::find($args['id']); + if (!$user) { + throw new RecordNotFoundException(); + } + + return $user; + } + + /** + * @param \User $resource + */ + protected function authorize(Request $request, $resource) + { + switch ($request->getMethod()) { + case 'GET': + case 'PATCH': + return UsersAuthority::canEditUser($this->getUser($request), $resource); + + default: + return false; + } + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function validateResourceDocument($json, $data) + { + if (!self::arrayHas($json, 'data')) { + return 'Missing `data` member at document´s top level.'; + } + + $item = self::arrayGet($json, 'data'); + + if ($item !== null) { + if (\JsonApi\Schemas\BlubberThread::TYPE !== self::arrayGet($item, 'type')) { + return 'Wrong `type` in document´s `data`.'; + } + + if (!self::arrayGet($item, 'id')) { + return 'Missing `id` of document´s `data`.'; + } + + if (self::arrayHas($item, 'attributes')) { + return 'Document must not have `attributes`.'; + } + } + } + + private function validateBlubberThread(\User $user, $json) + { + $resourceIdentifier = self::arrayGet($json, 'data'); + $thread = \BlubberThread::find($resourceIdentifier['id']); + + if (!$thread) { + throw new RecordNotFoundException(); + } + + if (!BlubberAuthority::canShowBlubberThread($user, $thread)) { + throw new BadRequestException('User is not able to access given thread.'); + } + + return $thread; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param \User $resource + */ + protected function getRelationshipSelfLink($resource, $schema, $userData) + { + return $schema->getRelationshipSelfLink($resource, UserSchema::REL_BLUBBER_DEFAULT_THREAD); + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param \User $resource + */ + protected function getRelationshipRelatedLink($resource, $schema, $userData) + { + return $schema->getRelationshipRelatedLink($resource, UserSchema::REL_BLUBBER_DEFAULT_THREAD); + } +} diff --git a/lib/classes/JsonApi/Routes/Blubber/SortTrait.php b/lib/classes/JsonApi/Routes/Blubber/SortTrait.php new file mode 100644 index 0000000..ee577ce --- /dev/null +++ b/lib/classes/JsonApi/Routes/Blubber/SortTrait.php @@ -0,0 +1,15 @@ +getQueryParameters()->getSorts()) ?? []; + + return $sortParameters; + } +} diff --git a/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php b/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php new file mode 100644 index 0000000..a85db30 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php @@ -0,0 +1,56 @@ +validate($request); + + $thread = \BlubberThread::find($args['id']); + if (!$thread) { + throw new RecordNotFoundException(); + } + + $user = $this->getUser($request); + if (!Authority::canEditBlubberThread($user, $thread)) { + throw new AuthorizationFailedException(); + } + + $visitedAt = self::arrayGet($json, 'data.attributes.visited-at'); + if ($visitedAt) { + $visitedDate = self::fromISO8601($visitedAt)->getTimestamp(); + $GLOBALS['user']->cfg->store('BLUBBERTHREAD_VISITED_' . $thread->getId(), $visitedDate); + } + + return $this->getContentResponse($thread); + } + + protected function validateResourceDocument($json) + { + if (self::arrayHas($json, 'data.attributes.visited-at')) { + $visitedAt = self::arrayGet($json, 'data.attributes.visited-at'); + if (!self::isValidTimestamp($visitedAt)) { + return '`visited-at` is not an ISO 8601 timestamp.'; + } + } + } +} diff --git a/lib/classes/JsonApi/Schemas/BlubberComment.php b/lib/classes/JsonApi/Schemas/BlubberComment.php index 0077023..6c0ffe3 100644 --- a/lib/classes/JsonApi/Schemas/BlubberComment.php +++ b/lib/classes/JsonApi/Schemas/BlubberComment.php @@ -19,11 +19,15 @@ class BlubberComment extends SchemaProvider public function getAttributes($resource, ContextInterface $context): iterable { + $userId = $this->currentUser->id; + $attributes = [ # `network` VARCHAR(64) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, 'content' => $resource['content'], 'content-html' => blubberReady($resource['content']), + 'is-writable' => $resource->isWritable($userId), + 'mkdate' => date('c', $resource['mkdate']), 'chdate' => date('c', $resource['chdate']), ]; diff --git a/lib/classes/JsonApi/Schemas/BlubberThread.php b/lib/classes/JsonApi/Schemas/BlubberThread.php index 2a7f660..e959c05 100644 --- a/lib/classes/JsonApi/Schemas/BlubberThread.php +++ b/lib/classes/JsonApi/Schemas/BlubberThread.php @@ -14,8 +14,6 @@ class BlubberThread extends SchemaProvider const REL_CONTEXT = 'context'; const REL_MENTIONS = 'mentions'; - - public function getId($resource): ?string { return $resource->id; @@ -25,8 +23,18 @@ class BlubberThread extends SchemaProvider { $userId = $this->currentUser->id; + $contextInfo = null; + $contextTemplate = $resource->getContextTemplate(); + if ($contextTemplate) { + $contextInfo = $contextTemplate->render(); + } + $attributes = [ + 'name' => $resource->getName(), + 'context-type' => $resource['context_type'], + 'context-info' => $contextInfo, + 'content' => $resource['content'], 'content-html' => formatReady($resource['content']), @@ -35,7 +43,11 @@ class BlubberThread extends SchemaProvider 'is-writable' => (bool) $resource->isWritable($userId), 'is-visible-in-stream' => (bool) $resource->isVisibleInStream(), + 'is-followed' => (bool) $resource->isFollowedByUser($userId), + 'may-disable-notifications' => (bool) $resource->mayDisableNotifications($userId), + 'latest-activity' => date('c', $resource->getLatestActivity()), + 'visited-at' => date('c', $resource->getLastVisit($userId)), 'mkdate' => date('c', $resource['mkdate']), 'chdate' => date('c', $resource['chdate']), ]; @@ -51,16 +63,32 @@ class BlubberThread extends SchemaProvider public function getRelationships($resource, ContextInterface $context): iterable { $relationships = []; - $relationships = $this->getAuthorRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_AUTHOR)); + $relationships = $this->getAuthorRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_AUTHOR) + ); $isPrimary = $context->getPosition()->getLevel() === 0; if (!$isPrimary) { return $relationships; } - $relationships = $this->getCommentsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_COMMENTS)); - $relationships = $this->getContextRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_CONTEXT)); - $relationships = $this->getMentionsRelationship($relationships, $resource, $this->shouldInclude($context, self::REL_MENTIONS)); + $relationships = $this->getCommentsRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_COMMENTS) + ); + $relationships = $this->getContextRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_CONTEXT) + ); + $relationships = $this->getMentionsRelationship( + $relationships, + $resource, + $this->shouldInclude($context, self::REL_MENTIONS) + ); return $relationships; } @@ -78,6 +106,10 @@ class BlubberThread extends SchemaProvider ], self::RELATIONSHIP_DATA => $related, ]; + } else { + $relationships[self::REL_AUTHOR] = [ + self::RELATIONSHIP_DATA => null, + ]; } return $relationships; @@ -107,7 +139,12 @@ class BlubberThread extends SchemaProvider { $relationship = [ self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($resource, self::REL_COMMENTS), + Link::RELATED => $this->getFactory()->createLink( + true, + $this->getSelfSubUrl($resource) . '/' . self::REL_COMMENTS, + true, + ['unseen-comments' => $resource->countUnseenComments($this->currentUser->id)] + ), ], ]; @@ -128,7 +165,8 @@ class BlubberThread extends SchemaProvider $related = $data = null; if ('course' === $resource['context_type']) { - if (!$course = \Course::find($resource['context_id'])) { + $course = \Course::find($resource['context_id']); + if (!$course) { throw new InternalServerError('Inconsistent data in BlubberThread.'); } @@ -137,7 +175,8 @@ class BlubberThread extends SchemaProvider } if ('institute' === $resource['context_type']) { - if (!$institute = \Institute::find($resource['context_id'])) { + $institute = \Institute::find($resource['context_id']); + if (!$institute) { throw new InternalServerError('Inconsistent data in BlubberThread.'); } @@ -157,4 +196,22 @@ class BlubberThread extends SchemaProvider return $relationships; } + + /** + * @inheritdoc + */ + public function hasResourceMeta($resource): bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getResourceMeta($resource) + { + return [ + 'avatar' => $resource->getAvatar(), + ]; + } } diff --git a/lib/classes/JsonApi/Schemas/User.php b/lib/classes/JsonApi/Schemas/User.php index 3e32665..beaeecb 100644 --- a/lib/classes/JsonApi/Schemas/User.php +++ b/lib/classes/JsonApi/Schemas/User.php @@ -2,6 +2,7 @@ namespace JsonApi\Schemas; +use JsonApi\Routes\Users\Authority as UsersAuthority; use Neomerx\JsonApi\Contracts\Factories\FactoryInterface; use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Schema\Link; @@ -12,6 +13,7 @@ class User extends SchemaProvider const REL_ACTIVITYSTREAM = 'activitystream'; const REL_BLUBBER = 'blubber-threads'; + const REL_BLUBBER_DEFAULT_THREAD = 'blubber-default-thread'; const REL_CONFIG_VALUES = 'config-values'; const REL_CONTACTS = 'contacts'; const REL_COURSES = 'courses'; @@ -191,11 +193,26 @@ class User extends SchemaProvider */ private function getBlubberRelationship(array $relationships, \User $user, $includeData) { - $relationships[self::REL_BLUBBER] = [ - self::RELATIONSHIP_LINKS => [ - Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_BLUBBER), - ], - ]; + if (\Config::get()->BLUBBER_GLOBAL_MESSENGER_ACTIVATE) { + $relationships[self::REL_BLUBBER] = [ + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->getRelationshipRelatedLink($user, self::REL_BLUBBER), + ], + ]; + + if (UsersAuthority::canEditUser($this->currentUser, $user)) { + $threadId = $user->getConfiguration()->getValue('BLUBBER_DEFAULT_THREAD'); + $thread = $includeData + ? \BlubberThread::find($threadId) + : \BlubberThread::build(['id' => $threadId], false); + $relationships[self::REL_BLUBBER_DEFAULT_THREAD] = [ + self::RELATIONSHIP_LINKS_SELF => true, + self::RELATIONSHIP_LINKS => [ + Link::RELATED => $this->createLinkToResource($thread), + ], + ]; + } + } return $relationships; } diff --git a/lib/classes/sidebar/BlubberThreadsWidget.php b/lib/classes/sidebar/BlubberThreadsWidget.php deleted file mode 100644 index db80023..0000000 --- a/lib/classes/sidebar/BlubberThreadsWidget.php +++ /dev/null @@ -1,61 +0,0 @@ -elements[] = $thread; - } - - public function setActive($thread_id) - { - $this->active_thread = $thread_id; - } - - public function withComposer($with = true) - { - $this->with_composer = $with; - } - - public function render($variables = []) - { - $template = $GLOBALS['template_factory']->open('blubber/threads-overview.php'); - if (count($this->elements) > 30) { - array_pop($this->elements); - $template->more_down = true; - } - - $json = []; - foreach ($this->elements as $thread) { - $unseen_comments = BlubberComment::countBySQL("thread_id = ? AND mkdate >= ?", [ - $thread->getId(), - $thread->getLastVisit() - ]); - - $json[] = [ - 'thread_id' => $thread->getId(), - 'avatar' => $thread->getAvatar(), - 'name' => $thread->getName(), - 'timestamp' => (int) $thread->getLatestActivity(), - 'mkdate' => (int) $thread->mkdate, - 'unseen_comments' => $unseen_comments, - 'notifications' => $thread->id === 'global' || ($thread->context_type === 'course' && !$GLOBALS['perm']->have_perm('admin')), - 'followed' => $thread->isFollowedByUser(), - ]; - } - - $template->threads = $this->elements; - $template->with_composer = $this->with_composer; - $template->json = $json; - return $template->render(); - } -} diff --git a/lib/models/BlubberThread.php b/lib/models/BlubberThread.php index d0f2f89..52465d2 100644 --- a/lib/models/BlubberThread.php +++ b/lib/models/BlubberThread.php @@ -1085,9 +1085,11 @@ class BlubberThread extends SimpleORMap implements PrivacyObject /** * Returns whether the notifications for this thread may be disabled. * + * @param string $user_id optional; use this ID instead of $GLOBALS['user']->id + * * @return bool */ - public function mayDisableNotifications(): bool + public function mayDisableNotifications(string $user_id = null): bool { // Notifications may always be disabled for global blubber stream if ($this->id === 'global') { @@ -1101,6 +1103,25 @@ class BlubberThread extends SimpleORMap implements PrivacyObject } // Only users with permission below admin may disable the notifications. - return !$GLOBALS['perm']->have_perm('admin'); + $user_id = $user_id ?? $GLOBALS['user']->id; + + return !$GLOBALS['perm']->have_perm('admin', $user_id); + } + + /** + * Count all unseen comments of this thread. + * + * @param string $user_id optional; use this ID instead of $GLOBALS['user']->id + * + */ + public function countUnseenComments(string $user_id = null): int + { + return \BlubberComment::countBySQL( + 'thread_id = ? AND mkdate >= ?', + [ + $this->getId(), + $this->getLastVisit($user_id ?? $GLOBALS['user']->id) ?: object_get_visit_threshold(), + ] + ); } } diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js index 59c0527..db613fd 100644 --- a/resources/assets/javascripts/chunk-loader.js +++ b/resources/assets/javascripts/chunk-loader.js @@ -1,4 +1,4 @@ -STUDIP.loadScript = function (script_name) { +export const loadScript = function (script_name) { return new Promise(function (resolve, reject) { let script = document.createElement('script'); script.src = `${STUDIP.ASSETS_URL}${script_name}`; @@ -8,7 +8,7 @@ STUDIP.loadScript = function (script_name) { }); }; -STUDIP.loadChunk = (function () { +export const loadChunk = (function () { var mathjax_promise = null; return function (chunk) { diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js index 70b78e7..5de07aa 100644 --- a/resources/assets/javascripts/entry-base.js +++ b/resources/assets/javascripts/entry-base.js @@ -15,7 +15,6 @@ import "./jquery-bundle.js" import "./init.js" import "./bootstrap/responsive.js" -import "./chunk-loader.js" import "./bootstrap/vue.js" import "./bootstrap/my-courses.js"; diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js index a37a466..a7d6f05 100644 --- a/resources/assets/javascripts/init.js +++ b/resources/assets/javascripts/init.js @@ -1,3 +1,4 @@ +import { loadChunk, loadScript, } from './chunk-loader.js'; import Vue from './lib/studip-vue.js'; import ActionMenu from './lib/actionmenu.js'; @@ -125,6 +126,8 @@ window.STUDIP = _.assign(window.STUDIP || {}, { JSONAPI, JSUpdater, Lightbox, + loadChunk, + loadScript, Markup, Members, Messages, diff --git a/resources/assets/javascripts/lib/blubber.js b/resources/assets/javascripts/lib/blubber.js index d169163..44d91bd 100644 --- a/resources/assets/javascripts/lib/blubber.js +++ b/resources/assets/javascripts/lib/blubber.js @@ -1,171 +1,40 @@ -import { $gettext } from './gettext'; - - const Blubber = { - App: null, //This app is not always available. The app is blubber with a widget and the threads next to it. - threads: [], - components: { - BlubberGlobalstream: () => import('../../../vue/components/BlubberGlobalstream.vue'), - BlubberPublicComposer: () => import('../../../vue/components/BlubberPublicComposer.vue'), - BlubberThread: () => import('../../../vue/components/BlubberThread.vue'), - BlubberThreadWidget: () => import('../../../vue/components/BlubberThreadWidget.vue'), - }, - init () { - let components = STUDIP.Blubber.components; - if ($('#blubber-index, #messenger-course, .blubber_panel.vueinstance').length) { - STUDIP.JSUpdater.register('blubber', Blubber.updateState, Blubber.getParamsForPolling); - - let panel_data = $('.blubber_panel').data(); - STUDIP.Vue.load().then(({createApp}) => { - STUDIP.Blubber.App = createApp({ - el: '#content-wrapper', - data() { - return { - threads: $('.blubber_threads_widget').data('threads_data'), - thread_data: panel_data.thread_data, - active_thread: panel_data.active_thread, - threads_more_down: panel_data.threads_more_down, - waiting: false, - display_context_posting: 0 - }; - }, - methods: { - changeActiveThread: function (thread_id) { - this.waiting = true; - let search = jQuery("form.sidebar-search input[name=search]").val(); - let parameters = search ? {data: {"search": search}} : {}; - STUDIP.api.GET(`blubber/threads/${thread_id}`, parameters).done((data) => { - this.active_thread = thread_id; - this.thread_data = data; - }).always(() => { - this.waiting = false; - }).fail(() => { - window.alert($gettext("Konnte die Konversation nicht laden. Probieren Sie es nachher erneut.")); - }); - for (let i in this.threads) { - if (this.threads[i].thread_id === thread_id) { - this.threads[i].unseen_comments = 0; - } - } - } - }, - components, - }); - }); - - $(document).on('submit', 'form.sidebar-search', function (event) { - this.waiting = true; - let search = jQuery("form.sidebar-search input[name=search]").val(); - if ($('#messenger-course').length === 0) { - STUDIP.api.GET(`blubber/threads`, {data: {"search": search}}).done((data) => { - STUDIP.Blubber.App.threads = data.threads; - STUDIP.Blubber.App.threads_more_down = data.more_down; - $('.blubber_thread_widget')[0].__vue__.display_more_down = data.more_down; - }).always(() => { - this.waiting = false; - }).fail(() => { - window.alert($gettext("Konnte die Suche nicht ausführen. Probieren Sie es nachher erneut.")); - }); - } - let parameters = search ? {"search": search} : {"modifier": "olderthan"}; - STUDIP.api.GET(`blubber/threads/` + STUDIP.Blubber.App.active_thread + `/comments`, {data: parameters}).done((data) => { - STUDIP.Blubber.App.thread_data.comments = data.comments; - STUDIP.Blubber.App.thread_data.more_up = data.more_up; - STUDIP.Blubber.App.thread_data.more_down = data.more_down; - $('.blubber_thread')[0].__vue__.scrollDown(); - }).always(() => { - this.waiting = false; - }).fail(() => { - window.alert($gettext("Konnte die Suche nicht ausführen. Probieren Sie es nachher erneut.")); - }); - event.preventDefault(); - return false; - }); - jQuery('#blubber-index, #messenger-course').on("click", 'a.blubber_hashtag', function (event) { - let tag = jQuery(this).closest("a").data("tag"); - jQuery("form.sidebar-search input[name=search]").val("#" + tag); - jQuery("form.sidebar-search").trigger("submit"); - event.preventDefault(); - return false; - }); - } - - $(document).on('dialog-open', function() { - $('.studip-dialog .blubber_panel').each(function () { - STUDIP.JSUpdater.register('blubber', Blubber.updateState, Blubber.getParamsForPolling); - - let panel_data = $(this).data(); - STUDIP.Vue.load().then(({createApp}) => { - createApp({ - el: this, - data () { - return { - threads: panel_data.threads_data, - thread_data: panel_data.thread_data, - active_thread: panel_data.active_thread, - threads_more_down: panel_data.threads_more_down, - waiting: false, - display_context_posting: 0 - }; - }, - components, - }); - }); - }); - }); - }, - updateState(datagram) { - for (const [method, data] of Object.entries(datagram)) { - if (method in Blubber) { - Blubber[method](data); + init() { + const blubberPage = document.querySelector('#blubber-index, #messenger-course, .blubber_panel.vueinstance'); + if (blubberPage !== null) { + const blubberPanel = document.querySelector('.blubber_panel'); + if (blubberPanel !== null) { + connectBlubber(blubberPanel, 'BlubberCommunityPage'); } } - }, - getParamsForPolling () { - const data = { - threads: [], - }; - $('.blubber_thread').each(function () { - data.threads.push(this.__vue__._props.thread_data.thread_posting.thread_id); - }); - return data; - }, - addNewComments (blubberdata) { - $('.blubber_thread').each(function () { - for (let thread_id in blubberdata) { - if (this.__vue__._props.thread_data.thread_posting.thread_id === thread_id) { - this.__vue__.addComments(blubberdata[thread_id], true); - this.__vue__.scrollDown(); - } + $(document).on('dialog-open', function (event, { dialog }) { + const blubberPanel = dialog.querySelector('.blubber_panel'); + if (blubberPanel !== null) { + connectBlubber(blubberPanel, 'BlubberDialogPanel'); } }); - }, - removeDeletedComments: function (comment_ids) { - $('.blubber_thread').each(function () { - this.__vue__.removeDeletedComments(comment_ids); - }); - }, - updateThreadWidget (threaddata) { - for (let i in threaddata) { - let exists = false; - for (let k in STUDIP.Blubber.App.threads) { - if (STUDIP.Blubber.App.threads[k].thread_id == threaddata[i].thread_id) { - exists = true; - STUDIP.Blubber.App.threads[k].name = threaddata[i].name; - STUDIP.Blubber.App.threads[k].timestamp = threaddata[i].timestamp; - STUDIP.Blubber.App.threads[k].avatar = threaddata[i].avatar; + + function connectBlubber(blubberPanel, componentName) { + return Promise.all([window.STUDIP.Vue.load(), Blubber.plugin()]).then( + ([{ Vue, createApp, store }, BlubberPlugin]) => { + Vue.use(BlubberPlugin, { store }); + const { initialThreadId, search } = blubberPanel.dataset; + return createApp({ + el: blubberPanel, + render: (h) => h(Vue.component(componentName), { props: { initialThreadId, search } }), + }); } - } - if (!exists) { - STUDIP.Blubber.App.threads.push(threaddata[i]); - } + ); } }, - refreshThread (data) { - STUDIP.Blubber.App.changeActiveThread(data.thread_id); + plugin() { + return import('@/vue/plugins/blubber.js').then(({ BlubberPlugin }) => BlubberPlugin); + }, + refreshThread(data) { + STUDIP.eventBus.emit('studip:select-blubber-thread', data.thread_id); }, - followunfollow (thread_id, follow) { + followunfollow(thread_id, follow) { const elements = $(`.blubber_panel .followunfollow[data-thread_id="${thread_id}"]`); if (follow === undefined) { follow = elements.hasClass('unfollowed'); @@ -176,51 +45,56 @@ const Blubber = { ? STUDIP.api.POST(`blubber/threads/${thread_id}/follow`) : STUDIP.api.DELETE(`blubber/threads/${thread_id}/follow`); - return promise.then(() => { - elements.toggleClass('unfollowed', !follow); - return follow; - }).always(() => { - elements.removeClass('loading'); - }).promise(); + return promise + .then(() => { + elements.toggleClass('unfollowed', !follow); + return follow; + }) + .always(() => { + elements.removeClass('loading'); + }) + .promise(); }, Composer: { vue: null, - init () { - STUDIP.Vue.load().then(({createApp}) => { - let components = STUDIP.Blubber.components; - return createApp({ - el: '#blubber_contact_ids', - data () { - return { - users: [] - }; - }, - methods: { - addUser: function (user_id, name) { - this.users.push({ - user_id: user_id, - name: name - }); + init() { + STUDIP.Vue.load() + .then(({ createApp }) => { + let components = STUDIP.Blubber.components; + return createApp({ + el: '#blubber_contact_ids', + data() { + return { + users: [], + }; }, - removeUser: function (event) { - let user_id = $(event.target).closest('li').find('input').val(); - for (let i in this.users) { - if (this.users[i].user_id === user_id) { - this.$delete(this.users, i); + methods: { + addUser: function (user_id, name) { + this.users.push({ + user_id: user_id, + name: name, + }); + }, + removeUser: function (event) { + let user_id = $(event.target).closest('li').find('input').val(); + for (let i in this.users) { + if (this.users[i].user_id === user_id) { + this.$delete(this.users, i); + } } - } + }, + clearUsers: function () { + this.users = []; + }, }, - clearUsers: function () { - this.users = []; - } - }, - components, + components, + }); + }) + .then((app) => { + STUDIP.Blubber.Composer.vue = app; }); - }).then((app) => { - STUDIP.Blubber.Composer.vue = app; - }); - } - } + }, + }, }; export default Blubber; diff --git a/resources/assets/javascripts/lib/jsupdater.js b/resources/assets/javascripts/lib/jsupdater.js index fad92e2..7888f29 100644 --- a/resources/assets/javascripts/lib/jsupdater.js +++ b/resources/assets/javascripts/lib/jsupdater.js @@ -219,6 +219,12 @@ const JSUpdater = { active = false; }, + // Returns true if there is already a registered handler for this index, + // false otherwise + isRegistered(index) { + return index in registeredHandlers; + }, + // Registers a new handler by an index, a callback and an optional data // object or function register(index, callback, data = null, interval = 0) { diff --git a/resources/assets/stylesheets/scss/blubber.scss b/resources/assets/stylesheets/scss/blubber.scss index 1b0e9e7..6c9e7ff 100644 --- a/resources/assets/stylesheets/scss/blubber.scss +++ b/resources/assets/stylesheets/scss/blubber.scss @@ -286,6 +286,8 @@ justify-content: space-around; align-items: center; + transition: all 0.5s ease-out; + > textarea { border: 1px solid $content-color-40; background-color: $white; diff --git a/resources/vue/components/BlubberGlobalstream.vue b/resources/vue/components/BlubberGlobalstream.vue deleted file mode 100644 index 2236e1f..0000000 --- a/resources/vue/components/BlubberGlobalstream.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - diff --git a/resources/vue/components/BlubberPublicComposer.vue b/resources/vue/components/BlubberPublicComposer.vue deleted file mode 100644 index 15fce7c..0000000 --- a/resources/vue/components/BlubberPublicComposer.vue +++ /dev/null @@ -1,98 +0,0 @@ - - diff --git a/resources/vue/components/BlubberThread.vue b/resources/vue/components/BlubberThread.vue deleted file mode 100644 index dab56cf..0000000 --- a/resources/vue/components/BlubberThread.vue +++ /dev/null @@ -1,526 +0,0 @@ - - - diff --git a/resources/vue/components/BlubberThreadWidget.vue b/resources/vue/components/BlubberThreadWidget.vue deleted file mode 100644 index c62bf3c..0000000 --- a/resources/vue/components/BlubberThreadWidget.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - diff --git a/resources/vue/components/SidebarWidget.vue b/resources/vue/components/SidebarWidget.vue index 34d935d..88c2834 100644 --- a/resources/vue/components/SidebarWidget.vue +++ b/resources/vue/components/SidebarWidget.vue @@ -2,8 +2,11 @@