diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index a7048bdd3f750a0b6cebb6f7e99c2b74e708c3db..3870d9a2055b7477f44b754d3f636480b82194fd 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -199,6 +199,7 @@ 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->post('/blubber-threads', Routes\Blubber\ThreadsCreate::class);
         $group->patch('/blubber-threads/{id}', Routes\Blubber\ThreadsUpdate::class);
 
         // create, read, update and delete BlubberComments
diff --git a/lib/classes/JsonApi/Routes/Blubber/Authority.php b/lib/classes/JsonApi/Routes/Blubber/Authority.php
index b03b6aafad6eb18031175382206b3279981d6876..5f56abf69c616bf91a87198db863c0c00d878560 100644
--- a/lib/classes/JsonApi/Routes/Blubber/Authority.php
+++ b/lib/classes/JsonApi/Routes/Blubber/Authority.php
@@ -4,6 +4,7 @@ namespace JsonApi\Routes\Blubber;
 
 use BlubberComment;
 use BlubberThread;
+use Course;
 use User;
 
 class Authority
@@ -23,6 +24,16 @@ class Authority
         return self::userIsAuthor($user);
     }
 
+    public static function canCreateCourseBlubberThread(User $user, Course $course)
+    {
+        return self::userIsTeacher($user, $course);
+    }
+
+    public static function canEditCourseBlubberThread(User $user, Course $course)
+    {
+        return self::userIsTeacher($user, $course);
+    }
+
     public static function canCreateComment(User $user, BlubberThread $resource)
     {
         return self::userIsAuthor($user) && $resource->isCommentable($user->id);
@@ -57,4 +68,12 @@ class Authority
     {
         return $GLOBALS['perm']->have_perm('autor', $user->id);
     }
+
+    /**
+     * @SuppressWarnings(PHPMD.Superglobals)
+     */
+    private static function userIsTeacher(User $user, Course $course)
+    {
+        return $GLOBALS['perm']->have_studip_perm('tutor', $course->id, $user->id);
+    }
 }
diff --git a/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php b/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
index b5bc9435e301e44b03cb3335d24568313f7fccd7..78f8fb2775fdd7c1120dccca389ffeee6268df8b 100644
--- a/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
+++ b/lib/classes/JsonApi/Routes/Blubber/ThreadsCreate.php
@@ -24,29 +24,43 @@ 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 {
+            $contextId = self::arrayGet($json, 'data.attributes.context-id', '');
+            $course = \Course::find($contextId);
+            if (!Authority::canCreateCourseBlubberThread($user = $this->getUser($request), $course)) {
+                throw new AuthorizationFailedException();
+            }
         }
 
+        $content = self::arrayGet($json, 'data.attributes.content', '');
+        $visible_in_stream = self::arrayGet($json, 'data.attributes.is-visible-in-stream', 1);
+
         $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,
+                'visible_in_stream' => $visible_in_stream,
                 '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
index 42688c8fd60e7c92b1f3a5f5e61e68b41ef8a207..3770d2bf96d32c2197efe0286cdcf42d977a62c0 100644
--- a/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php
+++ b/lib/classes/JsonApi/Routes/Blubber/ThreadsUpdate.php
@@ -5,6 +5,7 @@ namespace JsonApi\Routes\Blubber;
 use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Http\Message\ResponseInterface as Response;
 use JsonApi\Errors\AuthorizationFailedException;
+use JsonApi\Errors\BadRequestException;
 use JsonApi\Errors\RecordNotFoundException;
 use JsonApi\JsonApiController;
 use JsonApi\Routes\TimestampTrait;
@@ -50,6 +51,20 @@ class ThreadsUpdate extends JsonApiController
             }
         }
 
+        if (self::arrayGet($json, 'data.attributes.content')) {
+            if ($thread['context_type'] !== 'course') {
+                throw new BadRequestException('Only blubber threads of context-type course can be edited.');
+            }
+
+            $course = \Course::find($thread['context_id']);
+            if (!Authority::canEditCourseBlubberThread($this->getUser($request), $course)) {
+                throw new AuthorizationFailedException();
+            }
+
+            $thread['content'] = self::arrayGet($json, 'data.attributes.content');
+            $thread->store();
+        }
+
         return $this->getContentResponse($thread);
     }
 
diff --git a/lib/models/Courseware/BlockTypes/BlockType.php b/lib/models/Courseware/BlockTypes/BlockType.php
index 37e617b8fce13eb8b9531e5c72af9de51bdc2f97..3e301ebcdfa9693afe7fe08fc180630efe212259 100644
--- a/lib/models/Courseware/BlockTypes/BlockType.php
+++ b/lib/models/Courseware/BlockTypes/BlockType.php
@@ -101,6 +101,7 @@ abstract class BlockType
             BiographyCareer::class,
             BiographyGoals::class,
             BiographyPersonalInformation::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..88624a89abc51475efaf8894d592f19dc184ddb5
--- /dev/null
+++ b/lib/models/Courseware/BlockTypes/Blubber.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Courseware\BlockTypes;
+
+use BlubberThread;
+use Course;
+
+/**
+ * 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(): string
+    {
+        $schemaFile = __DIR__.'/Blubber.json';
+        return 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'];
+
+        $course = Course::find($rangeId);
+
+        if ( $threadId === '' || $rangeId === '' || !$course) {
+            return $this->initialPayload();
+        }
+
+        $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/blocks/blubber.scss b/resources/assets/stylesheets/scss/courseware/blocks/blubber.scss
new file mode 100644
index 0000000000000000000000000000000000000000..385840eabddf9636901d454d6a0b512f6c0c381c
--- /dev/null
+++ b/resources/assets/stylesheets/scss/courseware/blocks/blubber.scss
@@ -0,0 +1,42 @@
+@use '../../../mixins.scss' as *;
+
+.cw-block-blubber-content {
+    border: solid thin var(--content-color-40);
+    border-top: none;
+
+    .cw-blubber-thread {
+        background-color: var(--white);
+        border: unset;
+        width: unset;
+        max-width: unset;
+        margin-right: 0;
+    }
+
+    .cw-blubber-comments {
+        padding-left: 0;
+        padding-bottom: 10px;
+        max-height: 400px;
+        overflow-y: scroll;
+        overflow-x: hidden;
+        scrollbar-width: thin;
+        scrollbar-color: var(--base-color) var(--content-color-10);
+    }
+
+    .cw-blubber-thread-add-comment {
+        border-top: solid thin var(--content-color-40);
+        padding-top: 1em;
+        margin: 10px;
+        textarea {
+            width: calc(100% - 6px);
+            resize: none;
+            border: solid thin var(--content-color-40);
+            &:active {
+                border: solid thin var(--content-color-80);
+            }
+        }
+    }
+
+    .cw-blubber-thread-empty {
+        margin: 0 10px 10px 10px;
+    }
+}
diff --git a/resources/assets/stylesheets/scss/courseware/variables.scss b/resources/assets/stylesheets/scss/courseware/variables.scss
index 37be478da63bd95c1363ff3308bf587a37e757d2..033c99fa0059cb956efb098ed74dfe5c0e9be767 100644
--- a/resources/assets/stylesheets/scss/courseware/variables.scss
+++ b/resources/assets/stylesheets/scss/courseware/variables.scss
@@ -54,6 +54,7 @@ $border-colors: (
 
 $blockadder-items: (
     before-after: block-comparison,
+    blubber: blubber,
     canvas: block-canvas,
     gallery: block-gallery,
     image-map: block-imagemap,
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlubberBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBlubberBlock.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ccb310db90a8ee8aa8da30b32fca13b9caa1d969
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlubberBlock.vue
@@ -0,0 +1,184 @@
+<template>
+    <div class="cw-block cw-block-blubber">
+        <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 keine Blubber-Konversation angelegt.')"
+                    mood="unsure"
+                />
+            </template>
+            <template v-if="canEdit" #edit>
+                <form v-if="isTeacher && context.type === 'courses'" class="default" @submit.prevent="">
+                    <label>
+                        {{ $gettext('Blubber Konversation') }}
+                        <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>
+                        {{ $gettext('Titel') }}
+                        <input type="text" v-model="currentTitle" required/>
+                    </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>{{ $gettext('Informationen zum Blubber-Block') }}</p>
+            </template>
+        </courseware-default-block>
+    </div>
+</template>
+
+<script>
+import BlockComponents from './block-components.js';
+import blockMixin from '@/vue/mixins/courseware/block.js';
+import CoursewareBlubberThread from './CoursewareBlubberThread.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blubber-block',
+    mixins: [blockMixin],
+    components: Object.assign(BlockComponents, { CoursewareBlubberThread }),
+    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;
+            if (this.context.type === 'courses') {
+                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.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,
+                        'is-visible-in-stream': false
+                    }
+                });
+                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>
+<style lang="scss">
+@import '../../../../assets/stylesheets/scss/courseware/blocks/blubber.scss';
+</style>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5fc28bd14635a60cf33b5afeb2c1824d5b6c4a6a
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlubberComment.vue
@@ -0,0 +1,210 @@
+<template>
+    <li
+        v-if="commentUser"
+        :class="{ 'talk-bubble-own-post': ownComment }"
+        class="talk-bubble-wrapper"
+    >
+        <div v-if="!ownComment" class="talk-bubble-avatar">
+            <a :href="userProfileURL" :title="userFormattedName">
+                <img :src="userAvatar" />
+            </a>
+        </div>
+        <div class="talk-bubble" :class="{ editing: editActive }">
+            <div class="talk-bubble-content">
+                <header v-if="!ownComment" class="talk-bubble-header">
+                    <a :href="userProfileURL">{{ userFormattedName }}</a>
+                </header>
+                <div class="talk-bubble-talktext">
+                    <template v-if="!editActive">
+                        <div v-html="comment.attributes['content-html']" class="html"></div>
+                        <div class="talk-bubble-footer">
+                            <span class="talk-bubble-talktext-time"><studip-date-time :timestamp="chdate"
+                                    :relative="true"></studip-date-time></span>
+                            <a href="#" v-if="ownComment" @click.prevent.stop="editComment" class="edit_comment"
+                                :title="$gettext('Bearbeiten')">
+                                <studip-icon shape="edit" :size="14" />
+                            </a>
+                            <a href="#" @click.prevent="answerComment" class="answer_comment"
+                                :title="$gettext('Hierauf antworten')">
+                                <studip-icon shape="reply" :size="14" />
+                            </a>
+                            <a href="#" v-if="ownComment || userIsTeacher" @click.prevent="showDeleteDialog = true" class="answer_comment"
+                                :title="$gettext('Löschen')">
+                                <studip-icon shape="trash" :size="14" />
+                            </a>
+
+                        </div>
+                    </template>
+                    <div v-else class="talk-bubble-edit">
+                    <textarea
+                        v-model="currentContent"
+                        ref="commentedit"
+                        @keydown.enter.exact.prevent="updateComment"
+                        @keyup.escape.exact="resetComment"
+                    ></textarea>
+                        <button @click="updateComment" :title="$gettext('Speichern')">
+                            <studip-icon shape="accept" />
+                        </button>
+                        <button @click="resetComment" :title="$gettext('Abbrechen')">
+                            <studip-icon shape="decline" />
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </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>
+    </li>
+    <li v-else class="cw-talk-bubble">
+        <studip-progress-indicator
+            class="cw-loading-indicator-blubber-comment"
+            :description="$gettext('Lade Beitrag…')"
+        />
+    </li>
+</template>
+
+<script>
+import StudipDialog from '../../StudipDialog.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+import { mapActions, mapGetters } from 'vuex';
+
+export default {
+    name: 'courseware-blubber-comment',
+    components: {
+        StudipDialog,
+        StudipProgressIndicator,
+    },
+    props: {
+        commentId: String,
+        editing: Boolean,
+    },
+    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 new Date(this.comment?.attributes?.chdate) / 1000;
+        },
+        userFormattedName() {
+            if (this.commentUser) {
+                return this.commentUser.attributes['formatted-name']
+            }
+            return '';
+        },
+        userAvatar() {
+            if (this.commentUser) {
+                return this.commentUser.meta.avatar.medium;
+            }
+            return '';
+        },
+        userProfileURL() {
+            if (this.commentUser) {
+                return STUDIP.URLHelper.base_url + 'dispatch.php/profile?username=' + this.commentUser.attributes.username;
+            }
+            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';
+        },
+        answerComment() {
+            const quoteContent = this.content.replace(/\[quote[^\]]*\].*\[\/quote\]/g, '').trim();
+            const quote = `[quote=${this.userFormattedName}]${quoteContent} [/quote]\n`;
+            this.$emit('answer', quote);
+        },
+        editComment() {
+            this.editActive = true;
+            this.$nextTick(() => {
+                this.adjustHeight();
+                this.$refs.commentedit.focus();
+            });
+        },
+        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 ? this.commentId : null);
+        },
+        editing(edit) {
+            if (edit) {
+                this.editComment();
+            }
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlubberThread.vue b/resources/vue/components/courseware/blocks/CoursewareBlubberThread.vue
new file mode 100644
index 0000000000000000000000000000000000000000..504448d3d15ef5a50fce71287f43aae6eb7a4e1a
--- /dev/null
+++ b/resources/vue/components/courseware/blocks/CoursewareBlubberThread.vue
@@ -0,0 +1,156 @@
+<template>
+    <div class="cw-blubber-thread blubber_thread">
+        <ol
+            v-show="!loadingThreads && threadComments.length > 0"
+            class="cw-blubber-comments comments"
+            aria-live="polite"
+            ref="commentsRef"
+        >
+            <courseware-blubber-comment
+                v-for="comment in threadComments"
+                :key="comment.id"
+                :comment-id="comment.id"
+                :editing="comment.id === editingComments"
+                @answer="answerComment"
+                @delete="loadThread(threadId)"
+                @editing="editingComment"
+            />
+        </ol>
+        <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
+                ref="composer"
+                v-model="newComment"
+                :placeholder="$gettext('Schreiben Sie eine Nachricht…')"
+                spellcheck="true"
+                @keydown.enter.exact="createComment"
+                @keyup.up.exact="editPreviousComment"
+            ></textarea>
+            <button class="button" @click="createComment">
+                {{ $gettext('Absenden') }}
+            </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 '../layouts/CoursewareCompanionBox.vue';
+import StudipProgressIndicator from '../../StudipProgressIndicator.vue';
+import JSUpdater from '@/assets/javascripts/lib/jsupdater.js';
+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: null,
+        }
+    },
+    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',
+            companionInfo: 'companionInfo',
+        }),
+        async createComment() {
+            if (this.newComment) {
+                await this.createBlubberComment({
+                    threadId: this.threadId,
+                    content: this.newComment
+                });
+                this.newComment = '';
+            } else {
+                this.companionInfo({ info: this.$gettext('Leere Beiträge können nicht erstellt werden.') });
+            }
+        },
+        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);
+        },
+        answerComment(content) {
+            this.newComment = content;
+            this.$refs.composer.focus();
+        },
+        editingComment(event) {
+            this.editingComments = event;
+        },
+        editPreviousComment() {
+            const comments = this.threadComments;
+            if (comments.length > 0) {
+                this.editingComments = comments[comments.length - 1].id;
+            }
+        }
+    },
+    mounted() {
+        this.$nextTick(async() => {
+            if (this.threadId) {
+                await this.loadThread(this.threadId);
+                this.loadingThreads = false;
+                this.scrollDown();
+                JSUpdater.register('blubber', () => this.loadThread(this.threadId), { threads: [this.threadId] });
+            }
+        });
+    },
+    watch: {
+        threadId(newId) {
+            if (newId) {
+                this.loadThread(newId);
+            }
+        },
+        threadComments(newComments, oldComments) {
+            if (newComments.length !== oldComments.length && !this.editingComments) {
+                this.scrollDown();
+            }
+        }
+    }
+}
+</script>
diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js
index ce0b7e958c12422eca501192bf37f15b70227580..0a89bef5e89d4bc802a9be914f66038bab3cd948 100644
--- a/resources/vue/components/courseware/containers/container-components.js
+++ b/resources/vue/components/courseware/containers/container-components.js
@@ -7,6 +7,7 @@ import CoursewareBiographyAchievementsBlock from '../blocks/CoursewareBiographyA
 import CoursewareBiographyCareerBlock from '../blocks/CoursewareBiographyCareerBlock.vue';
 import CoursewareBiographyGoalsBlock from '../blocks/CoursewareBiographyGoalsBlock.vue';
 import CoursewareBiographyPersonalInformationBlock from '../blocks/CoursewareBiographyPersonalInformationBlock.vue';
+import CoursewareBlubberBlock from '../blocks/CoursewareBlubberBlock.vue';
 import CoursewareCanvasBlock from '../blocks/CoursewareCanvasBlock.vue';
 import CoursewareChartBlock from '../blocks/CoursewareChartBlock.vue';
 import CoursewareCodeBlock from '../blocks/CoursewareCodeBlock.vue';
@@ -45,6 +46,7 @@ const ContainerComponents = {
     CoursewareBiographyCareerBlock,
     CoursewareBiographyGoalsBlock,
     CoursewareBiographyPersonalInformationBlock,
+    CoursewareBlubberBlock,
     CoursewareCanvasBlock,
     CoursewareChartBlock,
     CoursewareCodeBlock,
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 5dc5a1f1f48d0f456c8f882fcb2316b2b61c5d58..9e935313357dcb81a658d5bd5db63f69125fb2f3 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -1374,6 +1374,100 @@ export const actions = {
             options,
         });
     },
+
+    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) {
+                    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 });
+    },
     async loadUnitProgresses(context, { unitId }) {
         const response = await state.httpClient.get(`courseware-units/${unitId}/courseware-user-progresses`);
         if (response.status === 200) {
diff --git a/tests/jsonapi/BlubberThreadsCreateTest.php b/tests/jsonapi/BlubberThreadsCreateTest.php
index d2bdaea1a7eaa64b9beab279d3b0fa5c6885d5c4..7cbdd1492b795cb0eafd70a81b58c25d675909a0 100644
--- a/tests/jsonapi/BlubberThreadsCreateTest.php
+++ b/tests/jsonapi/BlubberThreadsCreateTest.php
@@ -36,7 +36,7 @@ class BlubberThreadsCreateTest extends \Codeception\Test\Unit
         // given
         $credentials = $this->tester->getCredentialsForTestAutor();
 
-        $response = $this->createThread($credentials, 'private');
+        $response = $this->createThread($credentials, ['context-type' => 'private']);
         $this->tester->assertTrue($response->isSuccessfulDocument([201]));
 
         $document = $response->document();
@@ -70,30 +70,48 @@ class BlubberThreadsCreateTest extends \Codeception\Test\Unit
         $this->tester->assertSame($credentials['id'], $links[0]['id']);
     }
 
+    public function testCreateCourseThreadSucessfully()
+    {
+        // given
+        $credentials = $this->tester->getCredentialsForTestDozent();
+        $course_id = 'a07535cf2f8a72df33c12ddfa4b53dde';
+        $thread_title = 'Test-Thread';
+        $attributes = [
+            'context-type' => 'course',
+            'context-id' => $course_id,
+            'content' => $thread_title
+        ];
+
+        $response = $this->createThread($credentials, $attributes);
+        $this->tester->assertTrue($response->isSuccessfulDocument([201]));
+
+        $document = $response->document();
+        $this->tester->assertTrue($document->isSingleResourceDocument());
+
+        $resourceObject = $document->primaryResource();
+
+        $this->tester->assertSame('course', $resourceObject->attribute('context-type'));
+        $this->tester->assertSame($thread_title, $resourceObject->attribute('content'));
+    }
+
     public function testFailToCreateAnotherTypeOfThread()
     {
         // given
         $credentials = $this->tester->getCredentialsForTestAutor();
 
-        $response = $this->createThread($credentials, 'course');
-        $this->tester->assertSame(400, $response->getStatusCode());
-
-        $response = $this->createThread($credentials, 'institute');
+        $response = $this->createThread($credentials, ['context-type' => 'institute']);
         $this->tester->assertSame(400, $response->getStatusCode());
 
-        $response = $this->createThread($credentials, 'public');
+        $response = $this->createThread($credentials, ['context-type' => 'public']);
         $this->tester->assertSame(400, $response->getStatusCode());
     }
 
-
-    private function createThread($credentials, $contextType = 'private')
+    private function createThread($credentials, $attributes)
     {
         $body = [
             'data' => [
                 'type' => Schema::TYPE,
-                'attributes' => [
-                    'context-type' => $contextType
-                ]
+                'attributes' => $attributes
             ]
         ];