diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 989a67ba9867179932dd008fa5c3b85fc7d2ae8f..95a7d5973496b6d3207902b4381ebc5abdc3a2be 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -193,6 +193,8 @@ class RouteMap
         $this->app->get('/blubber-threads', Routes\Blubber\ThreadsIndex::class)
                   ->setArgument('type', 'all');
         $this->app->get('/blubber-threads/{id}', Routes\Blubber\ThreadsShow::class);
+        $this->app->post('/blubber-threads', Routes\Blubber\ThreadsCreate::class);
+        $this->app->patch('/blubber-threads/{id}', Routes\Blubber\ThreadsUpdate::class);
 
         // create, read, update and delete BlubberComments
         $this->app->get('/blubber-threads/{id}/comments', Routes\Blubber\CommentsByThreadIndex::class);
diff --git a/lib/classes/JsonApi/Routes/Blubber/Authority.php b/lib/classes/JsonApi/Routes/Blubber/Authority.php
index 8ab431e372060f40f576fd29c3c7837a2abd81d0..57ae871627fe24f5a85099b99ade2d58b77fd29f 100644
--- a/lib/classes/JsonApi/Routes/Blubber/Authority.php
+++ b/lib/classes/JsonApi/Routes/Blubber/Authority.php
@@ -19,6 +19,16 @@ class Authority
         return self::userIsAuthor($user);
     }
 
+    public static function canCreateCourseBlubberThread(User $user)
+    {
+        return self::userIsTeacher($user);
+    }
+
+    public static function canEditCourseBlubberThread(User $user)
+    {
+        return self::userIsTeacher($user);
+    }
+
     public static function canCreateComment(User $user, BlubberThread $resource)
     {
         return self::userIsAuthor($user) && $resource->isCommentable($user->id);
@@ -53,4 +63,13 @@ class Authority
     {
         return $GLOBALS['perm']->have_perm('autor', $user->id);
     }
+
+
+    /**
+     * @SuppressWarnings(PHPMD.Superglobals)
+     */
+    private static function userIsTeacher(User $user)
+    {
+        return $GLOBALS['perm']->have_perm('tutor', $user->id);
+    }
 }
diff --git a/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php b/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
index d503157b93516b55e4c7d93ba3c018e0e12bb2d1..98dfa695d812ca1d7bc6b7c209c66b1f1656ae34 100644
--- a/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
+++ b/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
@@ -24,29 +24,41 @@ class ThreadsCreate extends JsonApiController
     {
         $json = $this->validate($request);
 
-        if (!Authority::canCreatePrivateBlubberThread($user = $this->getUser($request))) {
-            throw new AuthorizationFailedException();
+        $contextType = self::arrayGet($json, 'data.attributes.context-type', '');
+        if (!in_array($contextType, ['private', 'course'])) {
+            throw new BadRequestException('Only blubber threads of context-type private or course can be created.');
         }
 
-        $contextType = self::arrayGet($json, 'data.attributes.context-type', '');
-        if ('private' !== $contextType) {
-            throw new BadRequestException('Only blubber threads of context-type=private can be created.');
+        if ($contextType === 'private') {
+            if (!Authority::canCreatePrivateBlubberThread($user = $this->getUser($request))) {
+                throw new AuthorizationFailedException();
+            }
+            $contextId = 'global';
+        } else {
+            if (!Authority::canCreateCourseBlubberThread($user = $this->getUser($request))) {
+                throw new AuthorizationFailedException();
+            }
+            $contextId = self::arrayGet($json, 'data.attributes.context-id', '');
         }
 
+        $content = self::arrayGet($json, 'data.attributes.content', '');
+
         $thread = \BlubberThread::create(
             [
-                'context_type' => 'private',
-                'context_id' => 'global',
+                'context_type' => $contextType,
+                'context_id' => $contextId,
                 'user_id' => $user->id,
                 'external_contact' => 0,
                 'display_class' => null,
                 'visible_in_stream' => 1,
                 'commentable' => 1,
-                'content' => '',
+                'content' => $content,
             ]
         );
 
-        \BlubberMention::create(['thread_id' => $thread->id, 'user_id' => $user->id]);
+        if ($contextType === 'private') {
+            \BlubberMention::create(['thread_id' => $thread->id, 'user_id' => $user->id]);
+        }
 
         return $this->getCreatedResponse($thread);
     }
diff --git a/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php b/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php
new file mode 100644
index 0000000000000000000000000000000000000000..144f811d65a6427bff41b9eaf4bf4e8b78deb2e0
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace JsonApi\Routes\Blubber;
+
+use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
+use JsonApi\JsonApiController;
+use JsonApi\Routes\ValidationTrait;
+use JsonApi\Errors\RecordNotFoundException;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+
+/**
+ * Create a new private blubber thread.
+ */
+class ThreadsUpdate extends JsonApiController
+{
+    use ValidationTrait;
+        /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function __invoke(Request $request, Response $response, $args)
+    {
+        $json = $this->validate($request);
+
+        if (!($thread = \BlubberThread::find($args['id']))) {
+            throw new RecordNotFoundException();
+        }
+
+        if ($thread['context_type'] !== 'course') {
+            throw new BadRequestException('Only blubber threads of context-type course can be edited.');
+        }
+
+        if (!Authority::canEditCourseBlubberThread($this->getUser($request))) {
+            throw new AuthorizationFailedException();
+        }
+
+        $thread['content'] = self::arrayGet($json, 'data.attributes.content');
+        $thread->store();
+
+        return $this->getCodeResponse(204);
+    }
+
+    protected function validateResourceDocument($json)
+    {
+        if (empty(self::arrayGet($json, 'data.attributes.content'))) {
+            return 'Thread content should not be empty.';
+        }
+    }
+}
\ No newline at end of file
diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php
index f4ca5b50d5d90aa68272477a0f72cb07b1d2356d..72dfd31bb84d6f4785ba81084569232c0792a8e7 100755
--- a/lib/models/Courseware/BlockTypes/BlockType.php
+++ b/lib/models/Courseware/BlockTypes/BlockType.php
@@ -88,6 +88,7 @@ abstract class BlockType
         $blockTypes = [
             Audio::class,
             BeforeAfter::class,
+            Blubber::class,
             Canvas::class,
             Chart::class,
             Code::class,
diff --git a/lib/models/Courseware/BlockTypes/Blubber.json b/lib/models/Courseware/BlockTypes/Blubber.json
new file mode 100644
index 0000000000000000000000000000000000000000..2d115647e04efa4e5788b1eab426f63dba19b27d
--- /dev/null
+++ b/lib/models/Courseware/BlockTypes/Blubber.json
@@ -0,0 +1,12 @@
+{
+    "title": "Payload schema of Courseware\\BlockType\\Blubber",
+    "type": "object",
+    "properties": {
+        "thread_id": {
+            "type": "string"
+        }
+    },
+    "required": [
+    ],
+    "additionalProperties": false
+}
diff --git a/lib/models/Courseware/BlockTypes/Blubber.php b/lib/models/Courseware/BlockTypes/Blubber.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d5df1489bd9c968e1822496058ba4ebab3d8bd3
--- /dev/null
+++ b/lib/models/Courseware/BlockTypes/Blubber.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Courseware\BlockTypes;
+
+use BlubberThread;
+use Opis\JsonSchema\Schema;
+
+/**
+ * This class represents the content of a Courseware blubber block.
+ *
+ * @author  Ron Lucke <lucke@elan-ev.de>
+ * @license GPL2 or any later version
+ *
+ * @since   Stud.IP 5.0
+ */
+class Blubber extends BlockType
+{
+    public static function getType(): string
+    {
+        return 'blubber';
+    }
+
+    public static function getTitle(): string
+    {
+        return _('Blubber');
+    }
+
+    public static function getDescription(): string
+    {
+        return _('Lehrende können eine Konversation starten oder eine bestehende Konversation einbinden.');
+    }
+
+    public function initialPayload(): array
+    {
+        return [
+            'thread_id' => '',
+        ];
+    }
+
+    public static function getJsonSchema(): Schema
+    {
+        $schemaFile = __DIR__.'/Blubber.json';
+
+        return Schema::fromJsonString(file_get_contents($schemaFile));
+    }
+
+    public static function getCategories(): array
+    {
+        return ['interaction'];
+    }
+
+    public static function getContentTypes(): array
+    {
+        return ['text'];
+    }
+
+    public static function getFileTypes(): array
+    {
+        return [];
+    }
+
+    public function copyPayload(string $rangeId = ''): array
+    {
+        $payload = $this->getPayload();
+        $threadId = $payload['thread_id'];
+
+        if ( $threadId === '') {
+            return $payload;
+        }
+
+        $remoteBlubberThread = \BlubberThread::find($threadId);
+
+        $threadTitle = $remoteBlubberThread['content'];
+
+        $presentBlubberThread = \BlubberThread::findOneBySQL('content = ? AND context_id = ?', array($threadTitle, $rangeId));
+
+        if ($presentBlubberThread !== null) {
+            $payload['thread_id'] = $presentBlubberThread['thread_id'];
+        } else {
+            $user = \User::findCurrent();
+            $newBlubberThread = \BlubberThread::create(
+                [
+                    'context_type' => 'course',
+                    'context_id' => $rangeId,
+                    'user_id' => $user->id,
+                    'external_contact' => 0,
+                    'display_class' => null,
+                    'visible_in_stream' => 1,
+                    'commentable' => 1,
+                    'content' => $threadTitle,
+                ]
+            );
+            $payload['thread_id'] = $newBlubberThread['thread_id'];
+        }
+
+        return $payload;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index f9194ec4c9fd4df656212f79131c71b9cc493a4f..2464783ad111ae15c47d686a7425751b8c97db8d 100755
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -39,6 +39,7 @@ $icon-colors:(
 
 $blockadder-items: (
     before-after: block-comparison,
+    blubber: blubber,
     canvas: block-canvas,
     gallery: block-gallery,
     image-map: block-imagemap,
@@ -4194,6 +4195,99 @@ text block
 text block end
 */
 
+/* blubber block */
+
+.cw-block-blubber-content {
+    border: solid thin $content-color-40;
+    border-top: none;
+    padding-top: 10px;
+
+    .cw-blubber-comments {
+        padding-left: 0;
+        max-height: 400px;
+        overflow-y: scroll;
+        overflow-x: hidden;
+        scrollbar-width: thin;
+        scrollbar-color: $base-color #f5f5f5;
+
+        .cw-talk-bubble {
+            margin: 0 0 10px 28px;
+            float: unset;
+
+            &.cw-talk-bubble-own-post {
+                margin-left: auto;
+                margin-right: 28px;
+            }
+
+            .cw-loading-indicator-blubber-comment {
+                padding-top: 10px;
+            }
+
+            .cw-talk-bubble-talktext {
+                .edit-button {
+                    @include background-icon(edit, clickable, 16);
+                }
+                .delete-button {
+                    @include background-icon(trash, clickable, 16);
+                }
+                .cw-talk-bubble-button {
+                    background-color: transparent;
+                    height: 16px;
+                    width: 16px;
+                    border: none;
+                    vertical-align: top;
+                    cursor: pointer;
+                }
+                .cw-talk-bubble-talktext-time {
+                    .time {
+                        margin-right: 5px;
+                    }
+                }
+            }
+
+            .cw-talk-bubble-talktext-edit {
+                textarea {
+                    border-radius: 0;
+                    border: solid thin $base-color-60;
+                    width: calc(100% - 10px);
+                    resize: none;
+                    max-height: 240px;
+                    scrollbar-width: thin;
+                    scrollbar-color: $base-color #f5f5f5;
+                }
+                button.accept,
+                button.cancel {
+                    min-width: unset;
+                    width: unset;
+                    padding: 5px;
+                    margin: 0;
+                    &::before {
+                        margin: unset;
+                    }
+                }
+            }
+        }
+    }
+    .cw-blubber-thread-add-comment {
+        border-top: solid thin $content-color-40;
+        padding-top: 1em;
+        margin: 10px;
+        textarea {
+            width: calc(100% - 6px);
+            resize: none;
+            border: solid thin $content-color-40;
+            &:active {
+                border: solid thin $content-color-80;
+            }
+        }
+    }
+    .cw-blubber-thread-empty {
+        margin: 0 10px 10px 10px;
+    }
+}
+
+/* blubber block end*/
+
 /*
 cw tiles
 */
diff --git a/resources/vue/components/courseware/CoursewareBlubberBlock.vue b/resources/vue/components/courseware/CoursewareBlubberBlock.vue
new file mode 100644
index 0000000000000000000000000000000000000000..cd7b137fb7ae1cbc5d011e8b1721a67a9c2ff9f8
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareBlubberBlock.vue
@@ -0,0 +1,181 @@
+<template>
+    <div class="cw-block cw-blubber-chart">
+        <courseware-default-block
+            :block="block"
+            :canEdit="canEdit"
+            :isTeacher="isTeacher"
+            :preview="true"
+            @storeEdit="storeBlock"
+            @closeEdit="initCurrentData"
+        >
+            <template #content>
+                <div v-if="currentTitle" class="cw-block-title">
+                    {{ currentTitle }}
+                </div>
+                <div v-if="currentThreadId" class="cw-block-blubber-content" >
+                    <courseware-blubber-thread
+                        :thread-id="currentThreadId"
+                        @threadContent="setTitle"
+                    />
+                </div>
+                <courseware-companion-box
+                    v-else
+                    :msgCompanion="$gettext('Es wurde noch kein Blubber-Konversation angelegt.')"
+                    mood="unsure"
+                />
+            </template>
+            <template v-if="canEdit" #edit>
+                <form v-if="isTeacher && context.type === 'courses'" class="default" @submit.prevent="">
+                    <label>
+                        <translate>Blubber Konversation</translate>
+                        <select v-model="currentThreadId">
+                            <option value="">
+                                <translate>neue Konversation</translate>
+                            </option>
+                            <option
+                                v-for="thread in availableThreads"
+                                :key="thread.id"
+                                :value="thread.id"
+                            >
+                            {{ thread.attributes.content }}
+                            </option>
+                        </select>
+                    </label>
+                    <label>
+                        <translate>Titel</translate>
+                        <input type="text" v-model="currentTitle" />
+                    </label>
+                </form>
+                <courseware-companion-box
+                    v-if="!isTeacher"
+                    :msgCompanion="onlyTeachersInfo"
+                    mood="pointing"
+                />
+                <courseware-companion-box
+                    v-if="context.type !== 'courses'"
+                    :msgCompanion="notInCourseInfo"
+                    mood="pointing"
+                />
+            </template>
+            <template #info>
+                <p><translate>Informationen zum Blubber-Block</translate></p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import CoursewareBlubberThread from './CoursewareBlubberThread.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blubber-block',
+    components: {
+        CoursewareBlubberThread,
+        CoursewareCompanionBox,
+        CoursewareDefaultBlock,
+    },
+    props: {
+        block: Object,
+        canEdit: Boolean,
+        isTeacher: Boolean,
+    },
+    data() {
+        return {
+            currentTitle: '',
+            currentThreadId: '',
+            availableThreads: [],
+        }
+    },
+    computed: {
+        ...mapGetters({
+            context: 'context',
+        }),
+        notInCourseInfo() {
+            return this.$gettext('Blubber-Konversationen für Courseware Blöcke können nur in Veranstaltungen anlegen werden.');
+        },
+        onlyTeachersInfo() {
+            return this.$gettext('Nur Lehrende dürfen Blubber-Konversationen anlegen und ändern.')
+        }
+    },
+    methods:{
+        ...mapActions({
+            updateBlock: 'updateBlockInContainer',
+            loadCourseBlubberThreads: 'loadCourseBlubberThreads',
+            createBlubberThread: 'createBlubberThread',
+            updateBlubberThread: 'updateBlubberThread',
+            companionWarning: 'companionWarning'
+        }),
+        async initCurrentData() {
+            this.currentThreadId = this.block?.attributes?.payload?.thread_id;
+            this.availableThreads = await this.loadCourseBlubberThreads({cid: this.context.id});
+            this.availableThreads = this.availableThreads.filter(thread => thread.attributes.content !== null && thread.attributes.content !== '');
+        },
+        setTitle(e) {
+            this.currentTitle = e;
+        },
+        async storeBlock() {
+            if (this.context.type !== 'courses') {
+                this.companionWarning({
+                    info: this.notInCourseInfo
+                });
+                return;
+            }
+            if (!this.isTeacher) {
+                this.companionWarning({
+                    info: onlyTeachersInfo
+                });
+                return;
+            }
+            let attributes = {};
+            attributes.payload = {};
+
+            if (this.currentThreadId !== '' && this.currentTitle !== '') {
+                await this.updateBlubberThread({ 
+                    content: this.currentTitle,
+                    threadId: this.currentThreadId
+                });
+            }
+
+            if (this.currentThreadId !== '' && this.currentTitle === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte vergeben Sie einen Titel.')
+                });
+                return;
+            }
+
+            if (this.currentThreadId === '' && this.context.type === 'courses') {
+                await this.createBlubberThread({
+                    attributes: {
+                        'context-type': 'course',
+                        'context-id': this.context.id,
+                        'content': this.currentTitle
+                    }
+                });
+                const newThread = this.$store.getters['blubber-threads/lastCreated'];
+                this.currentThreadId = newThread.id;
+            }
+
+            attributes.payload.thread_id = this.currentThreadId;
+
+            this.updateBlock({
+                attributes: attributes,
+                blockId: this.block.id,
+                containerId: this.block.relationships.container.data.id,
+            });
+        },
+    },
+    mounted() {
+        this.initCurrentData();
+    },
+    watch: {
+        currentThreadId(newId) {
+            if (newId === '') {
+                this.currentTitle = '';
+            }
+        }
+    }
+}
+</script> 
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareBlubberComment.vue b/resources/vue/components/courseware/CoursewareBlubberComment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..31b9433d4058c879ea239a259a24b7ff4ae64c6f
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareBlubberComment.vue
@@ -0,0 +1,197 @@
+<template>
+    <div
+        v-if="commentUser"
+        :class="{ 'cw-talk-bubble-own-post': ownComment }"
+        class="cw-talk-bubble"
+    >
+        <div class="cw-talk-bubble-user" v-if="!ownComment">
+            <div class="cw-talk-bubble-avatar">
+                <img :src="userAvatar" />
+            </div>
+            <span>{{ userFormattedName }}</span>
+        </div>
+        <div v-show="!editActive" class="cw-talk-bubble-talktext">
+            <p>
+                <span>{{ content }}</span>
+            </p>
+            <p class="cw-talk-bubble-talktext-time">
+                <iso-date :date="chdate" class="time"/>
+                <button
+                    v-if="ownComment"
+                    :title="$gettext('Bearbeiten')"
+                    class="cw-talk-bubble-button edit-button"
+                    @click="editComment"
+                >
+                </button>
+                <button
+                    v-if="ownComment || userIsTeacher"
+                    :title="$gettext('Löschen')"
+                    class="cw-talk-bubble-button delete-button"
+                    @click="showDeleteDialog = true"
+                >
+                </button>
+            </p>
+        </div>
+        <div
+            v-show="editActive"
+            class="cw-talk-bubble-talktext cw-talk-bubble-talktext-edit"
+        >
+            <textarea
+                v-model="currentContent"
+                ref="commentedit"
+                @keydown.enter="updateComment"
+                @keydown.esc="resetComment"
+            />
+            <button
+                class="button accept"
+                :title="$gettext('Speichern')"
+                @click="updateComment"
+            >
+            </button>
+            <button
+                class="button cancel"
+                :title="$gettext('Abbrechen')"
+                @click="resetComment"
+            >
+            </button>
+        </div>
+        <studip-dialog
+            v-if="showDeleteDialog"
+            :title="$gettext('Beitrag löschen')"
+            :question="$gettext('Möchten Sie diesen Beitrag wirklich löschen')"
+            height="180"
+            width="360"
+            @confirm="deleteComment"
+            @close="showDeleteDialog = false"
+        ></studip-dialog>
+    </div>
+    <div v-else class="cw-talk-bubble">
+        <studip-progress-indicator
+            class="cw-loading-indicator-blubber-comment"
+            :description="$gettext('Lade Beitrag...')"
+        />
+    </div>
+</template>
+
+<script>
+import IsoDate from './IsoDate.vue';
+import StudipDialog from '../StudipDialog.vue';
+import StudipProgressIndicator from '../StudipProgressIndicator.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blubber-comment',
+    components: {
+        IsoDate,
+        StudipDialog,
+        StudipProgressIndicator,
+    },
+    props: {
+        commentId: String,
+    },
+    data() {
+        return {
+            editActive: false,
+            currentContent: '',
+            showDeleteDialog: false
+        }
+    },
+    computed: {
+        ...mapGetters({
+            blubberCommentsById: 'blubber-comments/byId',
+            userId: 'userId',
+            usersById: 'users/byId',
+            userIsTeacher: 'userIsTeacher',
+        }),
+        comment() {
+            let comment = this.blubberCommentsById({ id: this.commentId });
+            if(comment) {
+                return comment;
+            }
+            return null;
+        },
+        content() {
+            return this.comment?.attributes?.content;
+        },
+        chdate() {
+            return this.comment?.attributes?.chdate;
+        },
+        userFormattedName() {
+            if (this.commentUser) {
+                return this.commentUser.attributes['formatted-name']
+            }
+            return '';
+        },
+        userAvatar() {
+            if (this.commentUser) {
+                return this.commentUser.meta.avatar.small;
+            }
+            return '';
+        },
+        ownComment() {
+            if (this.commentUser && this.commentUser.id === this.userId) {
+                return true;
+            }
+            return false;
+        },
+        commentUser() {
+            let commentUserId = this.comment?.relationships?.author?.data?.id;
+            if(commentUserId) {
+                return this.usersById({ id: commentUserId });
+            }
+            return null;
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadUsers: 'users/loadById',
+            updateBlubberComment: 'updateBlubberComment',
+            companionWarning: 'companionWarning',
+            deleteBlubberComment: 'deleteBlubberComment'
+        }),
+        initCurrent() {
+            this.currentContent = this.content;
+        },
+        adjustHeight() {
+            let textarea = this.$refs.commentedit;
+            textarea.style.height = textarea.scrollHeight + 'px';
+        },
+        editComment() {
+            this.editActive = true;
+            this.$nextTick(() => {
+                this.adjustHeight();
+            });
+        },
+        async updateComment() {
+            if(this.currentContent === '') {
+                this.companionWarning({
+                    info: this.$gettext('Bitte schreiben Sie etwas in das Textfeld.')
+                });
+            }
+            await this.updateBlubberComment({ 
+                    content: this.currentContent,
+                    id: this.comment.id
+            });
+            this.editActive = false;
+        },
+        resetComment() {
+            this.currentContent = this.content;
+            this.editActive = false;
+        },
+        deleteComment() {
+            this.deleteBlubberComment({
+                id: this.commentId
+            });
+            this.$emit('delete');
+        }
+    },
+    mounted() {
+        this.initCurrent();
+    },
+    watch: {
+        editActive(edit) {
+            this.$emit('editing', this.editActive);
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/CoursewareBlubberThread.vue b/resources/vue/components/courseware/CoursewareBlubberThread.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2c78f47b37ea857d083503d32abe6a943bc4dfbc
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewareBlubberThread.vue
@@ -0,0 +1,142 @@
+<template>
+    <div class="cw-blubber-thread">
+        <ul
+            v-show="!loadingThreads && threadComments.length > 0"
+            class="cw-blubber-comments"
+            ref="commentsRef"
+        >
+            <courseware-blubber-comment
+                v-for="comment in threadComments"
+                :key="comment.id"
+                :comment-id="comment.id"
+                @delete="loadThread(threadId)"
+                @editing="editingComment"
+            />
+        </ul>
+        <courseware-companion-box
+            v-show="!loadingThreads && threadComments.length === 0"
+            class="cw-blubber-thread-empty"
+            :msgCompanion="$gettext('Bisher wurde noch nicht diskutiert.')"
+            mood="pointing"
+        />
+        <div v-show="!loadingThreads" class="cw-blubber-thread-add-comment">
+            <textarea
+                v-model="newComment"
+                :placeholder="$gettext('Schreiben Sie eine Nachricht...')"
+                spellcheck="true"
+                @keydown.enter="createComment"
+            ></textarea>
+            <button class="button" @click="createComment">
+                <translate>Absenden</translate>
+            </button>
+        </div>
+        <studip-progress-indicator
+            v-show="loadingThreads"
+            class="cw-loading-indicator-blubber-comment"
+            :description="$gettext('Lade Beiträge...')"
+        />
+    </div>
+</template>
+
+<script>
+import CoursewareBlubberComment from './CoursewareBlubberComment.vue';
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
+import StudipProgressIndicator from '../StudipProgressIndicator.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blubber-thread',
+    components: {
+        CoursewareBlubberComment,
+        CoursewareCompanionBox,
+        StudipProgressIndicator
+    },
+    props: {
+        threadId: String,
+    },
+    data() {
+        return {
+            newComment: '',
+            loadingThreads: true,
+            updater: null,
+            editingComments: false,
+        }
+    },
+    computed: {
+        ...mapGetters({
+            blubberThreadById: 'blubber-threads/byId',
+            blubberCommentsById: 'blubber-comments/byId',
+        }),
+        blubberThread() {
+            return this.blubberThreadById({ id: this.threadId });
+        },
+        threadComments() {
+            let comments = this.blubberThread?.relationships?.comments?.data;
+            if (comments) {
+                return comments;
+            }
+            return [];
+        },
+        threadTitle() {
+            return this.blubberThread?.attributes?.content;
+        }
+    },
+    methods: {
+        ...mapActions({
+            loadBlubberThread: 'loadBlubberThread',
+            createBlubberComment: 'createBlubberComment',
+        }),
+        async createComment() {
+            await this.createBlubberComment({
+                threadId: this.threadId,
+                content: this.newComment
+            });
+            this.newComment = '';
+        },
+        scrollDown() {
+            this.$nextTick( () => {
+                let ref = this.$refs["commentsRef"];
+                if (ref) {
+                    ref.scrollTop = ref.scrollHeight;
+                }
+            });
+        },
+        async loadThread(threadId) {
+            await this.loadBlubberThread({ threadId: threadId});
+            this.$emit('threadContent', this.threadTitle);
+        },
+        editingComment(event) {
+            this.editingComments = event;
+        }
+    },
+    mounted() {
+        this.$nextTick(async() => {
+            if (this.threadId) {
+                await this.loadThread(this.threadId);
+                this.loadingThreads = false;
+                this.scrollDown();
+            }
+        });
+        this.updater = setInterval(() => {
+            if (this.threadId) {
+                this.loadThread(this.threadId);
+            }
+        }, 10000);
+    },
+    beforeDestroy() {
+        clearInterval(this.updater)
+    },
+    watch: {
+        threadId(newId) {
+            if (newId) {
+                this.loadThread(newId);
+            }
+        },
+        threadComments(newComments, oldComments) {
+            if (newComments.length !== oldComments.length && !this.editingComments) {
+                this.scrollDown();
+            }
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/resources/vue/components/courseware/container-components.js b/resources/vue/components/courseware/container-components.js
index 7ce04e483fc8e83a1e07e5eb0fc2892f82f8415e..c1b52da1f51ee3b14f780dfe3fe6b2d0a7c04fd1 100755
--- a/resources/vue/components/courseware/container-components.js
+++ b/resources/vue/components/courseware/container-components.js
@@ -4,6 +4,7 @@ import CoursewareBlockAdderArea from './CoursewareBlockAdderArea.vue';
 // blocks
 import CoursewareAudioBlock from './CoursewareAudioBlock.vue';
 import CoursewareBeforeAfterBlock from './CoursewareBeforeAfterBlock.vue';
+import CoursewareBlubberBlock from './CoursewareBlubberBlock.vue';
 import CoursewareCanvasBlock from './CoursewareCanvasBlock.vue';
 import CoursewareChartBlock from './CoursewareChartBlock.vue';
 import CoursewareCodeBlock from './CoursewareCodeBlock.vue';
@@ -33,6 +34,7 @@ const ContainerComponents = {
     // blocks
     CoursewareAudioBlock,
     CoursewareBeforeAfterBlock,
+    CoursewareBlubberBlock,
     CoursewareCanvasBlock,
     CoursewareChartBlock,
     CoursewareCodeBlock,
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 1b87c8b31c5d33f7a1967f35ef3feb9041db1737..ee785347feb174884d1158e39f4f9fccfd910d31 100755
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -102,7 +102,9 @@ const mountApp = (STUDIP, createApp, element) => {
                     'semesters',
                     'sem-classes',
                     'sem-types',
-                    'terms-of-use'
+                    'terms-of-use',
+                    'blubber-threads',
+                    'blubber-comments'
                 ],
                 httpClient,
             }),
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index bb101d5cb7a52fb187372a5769863dc83ec3ea84..996d605444f0e52052673c3b81befd00c4705176 100755
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -966,6 +966,171 @@ export const actions = {
 
         return dispatch('loadFeedback', blockId);
     },
+
+    async createTaskGroup({ dispatch, rootGetters }, { taskGroup }) {
+        await dispatch('courseware-task-groups/create', taskGroup, { root: true });
+
+        const id = taskGroup.relationships.target.data.id;
+        const target = rootGetters['courseware-structural-elements/byId']({ id });
+
+        return dispatch('courseware-structure/loadDescendants', { root: target });
+    },
+
+    async loadTask({ dispatch }, { taskId }) {
+        return dispatch(
+            'courseware-tasks/loadById',
+            {
+                id: taskId,
+                options: {
+                    include: 'solver,task-group,task-group.lecturer',
+                },
+            },
+            { root: true }
+        );
+    },
+
+    async updateTask({ dispatch }, { attributes, taskId }) {
+        const task = {
+            type: 'courseware-tasks',
+            attributes: attributes,
+            id: taskId,
+        };
+        await dispatch('courseware-tasks/update', task, { root: true });
+
+        return dispatch('loadTask', { taskId: task.id });
+    },
+
+    async deleteTask({ dispatch }, { task }) {
+        const data = {
+            id: task.id,
+        };
+        await dispatch('courseware-tasks/delete', data, { root: true });
+    },
+
+    async createTaskFeedback({ dispatch }, { taskFeedback }) {
+        await dispatch('courseware-task-feedback/create', taskFeedback, { root: true });
+
+        return dispatch('loadTask', { taskId: taskFeedback.relationships.task.data.id });
+    },
+
+    async updateTaskFeedback({ dispatch }, { attributes, taskFeedbackId }) {
+        const taskFeedback = {
+            type: 'courseware-task-feedback',
+            attributes: attributes,
+            id: taskFeedbackId,
+        };
+        await dispatch('courseware-task-feedback/update', taskFeedback, { root: true });
+
+        return dispatch('courseware-task-feedback/loadById', { id: taskFeedback.id }, { root: true });
+    },
+
+    async deleteTaskFeedback({ dispatch }, { taskFeedbackId }) {
+        const data = {
+            id: taskFeedbackId,
+        };
+        await dispatch('courseware-task-feedback/delete', data, { root: true });
+    },
+
+    setPurposeFilter({ commit }, purpose) {
+        commit('setPurposeFilter', purpose);
+    },
+    setBookmarkFilter({ commit }, course) {
+        commit('setBookmarkFilter', course);
+    },
+
+    async loadCourseBlubberThreads({ dispatch, rootGetters }, { cid }) {
+        const parent = {
+            type: 'courses',
+            id: cid
+        };
+        const relationship = 'blubber-threads';
+        const options = {};
+        await dispatch('courses/loadRelated', { parent, relationship, options }, { root: true });
+        const threads = rootGetters['courses/related']({parent, relationship});
+
+        return threads;
+    },
+
+    loadBlubberThread({ dispatch, rootGetters }, { threadId }) {
+        return dispatch(
+            'blubber-threads/loadById',
+            {
+                id: threadId,
+                options: {
+                    include: 'comments',
+                },
+            },
+            { root: true }
+        ).then( async () => {
+            const thread = rootGetters['blubber-threads/byId']({ id: threadId });
+
+            for (let threadComment of thread.relationships.comments.data) {
+                let comment = rootGetters['blubber-comments/byId']({ id: threadComment.id });
+                let commentUserId = comment.relationships.author.data.id;
+                let user = rootGetters['users/byId']({ id: commentUserId });
+
+                if (user === undefined) {
+                    await dispatch('users/loadById', { id: commentUserId });
+                }
+            }
+        });
+    },
+
+    createBlubberThread({ dispatch }, { attributes }) {
+        const blubberThread = {
+            type: 'blubber-threads',
+            attributes: attributes
+        };
+
+        return dispatch('blubber-threads/create', blubberThread, { root: true });
+    },
+
+    async updateBlubberThread({ dispatch }, { content, threadId }) {
+        const blubberThread = {
+            type: 'blubber-threads',
+            attributes: {
+                content: content
+            },
+            id: threadId,
+        };
+        await dispatch('blubber-threads/update', blubberThread, { root: true });
+
+        return dispatch('blubber-threads/loadById', { id: blubberThread.id }, { root: true });
+    },
+
+    async createBlubberComment({ dispatch }, { content, threadId }) {
+        const data = {
+            data: {
+                attributes: {
+                    content: content,
+                }
+            }
+        };
+        const url = `blubber-threads/${threadId}/comments`;
+        await state.httpClient.post(url, data, {});
+
+        return dispatch('loadBlubberThread', { threadId: threadId });
+    },
+
+    async updateBlubberComment({ dispatch }, { content, id }) {
+        const blubberComment = {
+            type: 'blubber-comments',
+            attributes: {
+                content: content
+            },
+            id: id,
+        };
+        await dispatch('blubber-comments/update', blubberComment, { root: true });
+
+        return dispatch('blubber-comments/loadById', { id: blubberComment.id }, { root: true });
+    },
+
+    deleteBlubberComment({ dispatch }, { id }) {
+        const data = {
+            id: id,
+        };
+        dispatch('blubber-comments/delete', data, { root: true });
+    },
 };
 
 /* eslint no-param-reassign: ["error", { "props": false }] */