diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2025-11-19 09:33:25 +0100 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2025-11-19 09:33:25 +0100 |
| commit | a26562a296ca6bbd39f34029ec8fb8e0069172f2 (patch) | |
| tree | 482eb74e05f257734c1e3c7cab02c1bdd65d8bdd | |
| parent | 91268e7072c5140c516b6725cc8e6525f4c4834e (diff) | |
Resolve "Forum: Refaktorierung der Kategorie, Topic und Diskussions Metadaten sowie Behebung der fehlerhaften Anzeige ungelesener Beiträge"
Closes #6058
Merge request studip/studip!4613
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Forum/Category.php | 15 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Forum/Discussion.php | 11 | ||||
| -rw-r--r-- | lib/classes/JsonApi/Schemas/Forum/Topic.php | 13 | ||||
| -rw-r--r-- | lib/models/Forum/Category.php | 22 | ||||
| -rw-r--r-- | lib/models/Forum/Discussion.php | 26 | ||||
| -rw-r--r-- | lib/models/Forum/Topic.php | 21 | ||||
| -rw-r--r-- | resources/vue/apps/forum/discussions/Show.vue | 2 | ||||
| -rw-r--r-- | resources/vue/components/forum/categories/CategoryItem.vue | 16 | ||||
| -rw-r--r-- | resources/vue/components/forum/discussions/DiscussionIndex.vue | 8 | ||||
| -rw-r--r-- | resources/vue/components/forum/discussions/DiscussionTimeline.vue | 33 | ||||
| -rw-r--r-- | resources/vue/components/forum/posts/Post.vue | 4 | ||||
| -rw-r--r-- | resources/vue/components/forum/topics/TopicItem.vue | 16 | ||||
| -rw-r--r-- | resources/vue/store/pinia/forum/ForumPost.js | 9 |
13 files changed, 122 insertions, 74 deletions
diff --git a/lib/classes/JsonApi/Schemas/Forum/Category.php b/lib/classes/JsonApi/Schemas/Forum/Category.php index 5e3012b..0835937 100644 --- a/lib/classes/JsonApi/Schemas/Forum/Category.php +++ b/lib/classes/JsonApi/Schemas/Forum/Category.php @@ -46,15 +46,16 @@ class Category extends SchemaProvider */ public function getResourceMeta($resource) { - $metaData = $resource->getMetaData(); + $metadata = $resource->getMetadata(); return [ - 'topics-count' => (int) $metaData['topics_count'], - 'discussions-count' => (int) $metaData['discussions_count'], - 'postings-count' => (int) $metaData['postings_count'], - 'user-read-index' => (int) $metaData['user_read_index'], - 'users-count' => (int) $metaData['users_count'], - 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '', + 'topics-count' => (int) $metadata['topics_count'], + 'discussions-count' => (int) $metadata['discussions_count'], + 'postings-count' => (int) $metadata['postings_count'], + 'unread-postings-count' => (int) $metadata['unread_postings_count'], + 'user-read-index' => (int) $metadata['user_read_index'], + 'users-count' => (int) $metadata['users_count'], + 'recent-activity' => $metadata['recent_activity'] ? date('c', $metadata['recent_activity']) : '', ]; } diff --git a/lib/classes/JsonApi/Schemas/Forum/Discussion.php b/lib/classes/JsonApi/Schemas/Forum/Discussion.php index 9252aaa..a8c5c57 100644 --- a/lib/classes/JsonApi/Schemas/Forum/Discussion.php +++ b/lib/classes/JsonApi/Schemas/Forum/Discussion.php @@ -53,13 +53,14 @@ class Discussion extends SchemaProvider */ public function getResourceMeta($resource) { - $metaData = $resource->getMetaData(); + $metadata = $resource->getMetadata(); return [ - 'postings-count' => (int) $metaData['postings_count'], - 'recent-postings-count' => (int) $metaData['recent_postings_count'], - 'user-read-index' => (int) $metaData['user_read_index'], - 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '' + 'postings-count' => (int) $metadata['postings_count'], + 'recent-postings-count' => (int) $metadata['recent_postings_count'], + 'unread-postings-count' => (int) $metadata['unread_postings_count'], + 'user-read-index' => (int) $metadata['user_read_index'], + 'recent-activity' => $metadata['recent_activity'] ? date('c', $metadata['recent_activity']) : '' ]; } diff --git a/lib/classes/JsonApi/Schemas/Forum/Topic.php b/lib/classes/JsonApi/Schemas/Forum/Topic.php index 22a8c83..6a9659b 100644 --- a/lib/classes/JsonApi/Schemas/Forum/Topic.php +++ b/lib/classes/JsonApi/Schemas/Forum/Topic.php @@ -47,14 +47,15 @@ class Topic extends SchemaProvider */ public function getResourceMeta($resource) { - $metaData = $resource->getMetaData(); + $metadata = $resource->getMetadata(); return [ - 'discussions-count' => (int) $metaData['discussions_count'], - 'postings-count' => (int) $metaData['postings_count'], - 'user-read-index' => (int) $metaData['user_read_index'], - 'users-count' => (int) $metaData['users_count'], - 'recent-activity' => $metaData['recent_activity'] ? date('c', $metaData['recent_activity']) : '', + 'discussions-count' => (int) $metadata['discussions_count'], + 'postings-count' => (int) $metadata['postings_count'], + 'unread-postings-count' => (int) $metadata['unread_postings_count'], + 'user-read-index' => (int) $metadata['user_read_index'], + 'users-count' => (int) $metadata['users_count'], + 'recent-activity' => $metadata['recent_activity'] ? date('c', $metadata['recent_activity']) : '', ]; } diff --git a/lib/models/Forum/Category.php b/lib/models/Forum/Category.php index 6ee52c8..74cd189 100644 --- a/lib/models/Forum/Category.php +++ b/lib/models/Forum/Category.php @@ -56,9 +56,9 @@ class Category extends \SimpleORMap return self::findBySQL("range_id = ? ORDER BY position ASC, mkdate DESC", [$range_id]); } - public function getMetaData(): array + public function getMetadata(): array { - return DBManager::get()->fetchOne( + $metadata= DBManager::get()->fetchOne( "SELECT COUNT(DISTINCT`forum_topics`.`topic_id`) AS 'topics_count', COUNT(DISTINCT `forum_discussions`.`discussion_id`) AS 'discussions_count', @@ -73,7 +73,15 @@ class Category extends \SimpleORMap ON fpr.discussion_id = fd2.discussion_id AND fpr.user_id = :user_id WHERE ft2.category_id = :category_id - ) AS 'user_read_index' + ) AS 'user_read_index', + ( + SELECT + COUNT(DISTINCT fp.posting_id) + FROM forum_postings fp + JOIN `forum_discussions` fd USING (discussion_id) + JOIN `forum_topics` ft USING (topic_id) + WHERE ft.category_id = :category_id AND fp.user_id != :user_id + ) AS 'others_postings_count' FROM `forum_topics` LEFT JOIN `forum_discussions` USING (`topic_id`) LEFT JOIN `forum_postings` USING (`discussion_id`) @@ -83,6 +91,14 @@ class Category extends \SimpleORMap 'user_id' => User::findCurrent()?->user_id ] ); + + return [ + ...$metadata, + 'unread_postings_count' => max( + 0, + $metadata['others_postings_count'] - (int) $metadata['user_read_index'] + ) + ]; } public function transformData(): array diff --git a/lib/models/Forum/Discussion.php b/lib/models/Forum/Discussion.php index de82422..5b8edc5 100644 --- a/lib/models/Forum/Discussion.php +++ b/lib/models/Forum/Discussion.php @@ -240,7 +240,7 @@ class Discussion extends SimpleORMap ]; } - public function getMetaData(int $last_visit = 0): array + public function getMetadata(int $last_visit = 0): array { $user_id = User::findCurrent()?->user_id; @@ -249,7 +249,7 @@ class Discussion extends SimpleORMap $last_visit = object_get_visit($this->topic->range_id, $plugin_id, 'last', '', $user_id); } - return DBManager::get()->fetchOne( + $metadata = DBManager::get()->fetchOne( "SELECT COUNT(`posting_id`) 'postings_count', MAX(`mkdate`) 'recent_activity', @@ -261,10 +261,16 @@ class Discussion extends SimpleORMap ) 'user_read_index', ( SELECT - COUNT(DISTINCT fp.posting_id) - FROM forum_postings fp - WHERE fp.discussion_id = :discussion_id AND fp.mkdate > :last_visit - ) AS 'recent_postings_count' + COUNT(DISTINCT posting_id) + FROM forum_postings + WHERE discussion_id = :discussion_id AND mkdate > :last_visit AND `user_id` != :user_id + ) AS 'recent_postings_count', + ( + SELECT + COUNT(DISTINCT posting_id) + FROM forum_postings + WHERE discussion_id = :discussion_id AND `user_id` != :user_id + ) AS 'others_postings_count' FROM `forum_postings` WHERE `discussion_id` = :discussion_id", [ 'discussion_id' => $this->discussion_id, @@ -272,6 +278,14 @@ class Discussion extends SimpleORMap 'last_visit' => $last_visit ] ); + + return [ + ...$metadata, + 'unread_postings_count' => max( + 0, + $metadata['others_postings_count'] - (int) $metadata['user_read_index'] + ) + ]; } public function onCreate(): void diff --git a/lib/models/Forum/Topic.php b/lib/models/Forum/Topic.php index 2bf4c27..9b5ce7f 100644 --- a/lib/models/Forum/Topic.php +++ b/lib/models/Forum/Topic.php @@ -119,9 +119,9 @@ class Topic extends SimpleORMap ); } - public function getMetaData(): array + public function getMetadata(): array { - return DBManager::get()->fetchOne( + $metadata = DBManager::get()->fetchOne( "SELECT COUNT(DISTINCT `forum_discussions`.`discussion_id`) AS 'discussions_count', COUNT(DISTINCT `forum_postings`.`posting_id`) AS 'postings_count', @@ -135,7 +135,14 @@ class Topic extends SimpleORMap ON fpr.discussion_id = fd2.discussion_id AND fpr.user_id = :user_id WHERE fd2.topic_id = :topic_id - ) AS 'user_read_index' + ) AS 'user_read_index', + ( + SELECT + COUNT(DISTINCT fp.posting_id) + FROM forum_postings fp + JOIN `forum_discussions` fd USING (discussion_id) + WHERE fd.topic_id = :topic_id AND fp.user_id != :user_id + ) AS 'others_postings_count' FROM `forum_discussions` LEFT JOIN `forum_postings` USING (`discussion_id`) WHERE `forum_discussions`.`topic_id` = :topic_id", @@ -144,6 +151,14 @@ class Topic extends SimpleORMap 'user_id' => User::findCurrent()?->user_id ] ); + + return [ + ...$metadata, + 'unread_postings_count' => max( + 0, + $metadata['others_postings_count'] - (int) $metadata['user_read_index'] + ) + ]; } public function transformData(): array diff --git a/resources/vue/apps/forum/discussions/Show.vue b/resources/vue/apps/forum/discussions/Show.vue index 4fa4fad..ca1cabc 100644 --- a/resources/vue/apps/forum/discussions/Show.vue +++ b/resources/vue/apps/forum/discussions/Show.vue @@ -269,7 +269,7 @@ onMounted(async () => { </div> <template #sidebar> - <DiscussionTimeline :discussion="discussion" :posts="posts" :readIndex="read_index" /> + <DiscussionTimeline :discussion="discussion" /> </template> </ForumApp> </template> diff --git a/resources/vue/components/forum/categories/CategoryItem.vue b/resources/vue/components/forum/categories/CategoryItem.vue index 72b646b..c761e12 100644 --- a/resources/vue/components/forum/categories/CategoryItem.vue +++ b/resources/vue/components/forum/categories/CategoryItem.vue @@ -87,14 +87,14 @@ const swapCategory = event => { :title="$gettext('Zur Kategorie')"> <span class="category-title line-clamp-2">{{ category.name }}</span> <span - v-if="!forumConfig.allowGuestAccess && category.meta.postings_count > category.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" class="unread-items-badge" role="status" aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.postings_count - category.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.postings_count - category.meta.user_read_index})" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" > - {{ category.meta.postings_count - category.meta.user_read_index }} + {{ category.meta.unread_postings_count }} </span> </a> </div> @@ -203,14 +203,14 @@ const swapCategory = event => { </span> <span - v-if="!forumConfig.allowGuestAccess && category.meta.postings_count > category.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && category.meta.unread_postings_count" class="unread-items-badge" role="status" aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.postings_count - category.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.postings_count - category.meta.user_read_index})" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: category.meta.unread_postings_count})" > - {{ category.meta.postings_count - category.meta.user_read_index }} + {{ category.meta.unread_postings_count }} </span> </div> <div class="actions"> diff --git a/resources/vue/components/forum/discussions/DiscussionIndex.vue b/resources/vue/components/forum/discussions/DiscussionIndex.vue index 23c2eee..5478489 100644 --- a/resources/vue/components/forum/discussions/DiscussionIndex.vue +++ b/resources/vue/components/forum/discussions/DiscussionIndex.vue @@ -169,14 +169,14 @@ onMounted(() => { <span class="title-with-actions_title discussion-title line-clamp-2 m-0">{{ discussion.title }}</span> <template v-if="!forumConfig.allowGuestAccess"> <span - v-if="redirect !== 'recent' && discussion.meta.postings_count > discussion.meta.user_read_index" + v-if="redirect !== 'recent' && discussion.meta.unread_postings_count" class="unread-items-badge" role="status" aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.postings_count - discussion.meta.user_read_index})" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge.', {count: discussion.meta.unread_postings_count})" > - {{ discussion.meta.postings_count - discussion.meta.user_read_index }} + {{ discussion.meta.unread_postings_count }} </span> <span v-if="redirect === 'recent' && discussion.meta.recent_postings_count" diff --git a/resources/vue/components/forum/discussions/DiscussionTimeline.vue b/resources/vue/components/forum/discussions/DiscussionTimeline.vue index f54aa61..b7c9e1b 100644 --- a/resources/vue/components/forum/discussions/DiscussionTimeline.vue +++ b/resources/vue/components/forum/discussions/DiscussionTimeline.vue @@ -7,18 +7,10 @@ import {$gettext} from '@/assets/javascripts/lib/gettext'; const forumConfig = useForumConfig(); const forumPostStore = useForumPost(); -const props = defineProps({ +defineProps({ discussion: { type: Object, required: true, - }, - posts: { - type: Array, - required: true, - }, - readIndex: { - type: Number, - default: 0 } }); @@ -26,21 +18,18 @@ const scrollerTop = ref(0); const isDragging = ref(false); const unreadScrollPosition = ref(-1); -const othersPosts = computed(() => props.posts.filter(({ author }) => author?.id !== STUDIP.USER_ID)); +const posts = computed(() => forumPostStore.posts); const currentPostIndex = computed(() => forumPostStore.currentPostIndex); +const firstUnreadPostIndex = computed(() => forumPostStore.firstUnreadPostIndex); const currentPostDate = computed(() => { - if (currentPostIndex.value < props.posts.length) { - const date = new Date(props.posts[currentPostIndex.value].mkdate); + if (currentPostIndex.value < posts.value.length) { + const date = new Date(posts.value[currentPostIndex.value].mkdate); return date.toLocaleString(String.locale, { month: 'long', year: 'numeric' }); } return null; }); -const isNewFrom = computed(() => { - return unreadScrollPosition.value >= 0 - && props.readIndex < othersPosts.value.length - && !forumConfig.allowGuestAccess -}); +const isNewFrom = computed(() => firstUnreadPostIndex.value > -1 && !forumConfig.allowGuestAccess); const findPostAtScroll = y => { const postElements = document.querySelectorAll('.post'); @@ -142,12 +131,12 @@ const handleScroll = () => { }; const updateUnreadScrollPosition = () => { - if (props.readIndex === 0) { + if (firstUnreadPostIndex.value === 0) { unreadScrollPosition.value = 0; return; } - const firstUnreadPost = document.querySelector(`[data-index='${props.readIndex}']`); + const firstUnreadPost = document.querySelector(`[data-index='${firstUnreadPostIndex.value}']`); if (!firstUnreadPost) { return; } @@ -196,7 +185,7 @@ onUnmounted(() => { </div> <Transition name="fade"> <div - v-if="isNewFrom && currentPostIndex !== readIndex" + v-if="isNewFrom && currentPostIndex !== firstUnreadPostIndex" class="scroll-area__new-from" :style="{ top: `${unreadScrollPosition}%` @@ -204,7 +193,7 @@ onUnmounted(() => { <button type="button" class="button-base" - @click.stop="jumpToPost(null, readIndex)" + @click.stop="jumpToPost(null, firstUnreadPostIndex)" :title="$gettext('Zum ersten ungelesenen Beitrag')" > {{ $gettext('Neu ab hier') }} @@ -230,7 +219,7 @@ onUnmounted(() => { {{ currentPostDate }} </time> <Transition name="fade"> - <span v-if="isNewFrom && currentPostIndex === readIndex"> + <span v-if="isNewFrom && currentPostIndex === firstUnreadPostIndex"> — {{ $gettext('Neu ab hier') }} </span> </Transition> diff --git a/resources/vue/components/forum/posts/Post.vue b/resources/vue/components/forum/posts/Post.vue index 1f8a5a7..34107a3 100644 --- a/resources/vue/components/forum/posts/Post.vue +++ b/resources/vue/components/forum/posts/Post.vue @@ -139,6 +139,10 @@ onMounted(() => { ); postObserver.observe(postRef.value); + + if (!forumConfig.allowGuestAccess && isUnread.value && forumPostStore.firstUnreadPostIndex < 0) { + forumPostStore.updateFirstUnreadPostIndex(props.index); + } }); onBeforeUnmount(() => postObserver.disconnect()); diff --git a/resources/vue/components/forum/topics/TopicItem.vue b/resources/vue/components/forum/topics/TopicItem.vue index 544c49d..581c47f 100644 --- a/resources/vue/components/forum/topics/TopicItem.vue +++ b/resources/vue/components/forum/topics/TopicItem.vue @@ -83,14 +83,14 @@ const swapTopic = event => { <a class="title-with-actions__link" :href="getTopicURL(topic.id)" :title="$gettext('Zum Thema')"> <span class="topic-title line-clamp-2">{{ topic.name }}</span> <span - v-if="!forumConfig.allowGuestAccess && topic.meta.postings_count > topic.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" class="unread-items-badge" role="status" aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.postings_count - topic.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.postings_count - topic.meta.user_read_index})" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" > - {{ topic.meta.postings_count - topic.meta.user_read_index }} + {{ topic.meta.unread_postings_count }} </span> </a> </div> @@ -191,14 +191,14 @@ const swapTopic = event => { </span> <span - v-if="!forumConfig.allowGuestAccess && topic.meta.postings_count > topic.meta.user_read_index" + v-if="!forumConfig.allowGuestAccess && topic.meta.unread_postings_count" class="unread-items-badge" role="status" aria-live="polite" - :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.postings_count - topic.meta.user_read_index})" - :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.postings_count - topic.meta.user_read_index})" + :aria-label="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" + :title="$gettext('Sie haben %{count} ungelesene Beiträge', {count: topic.meta.unread_postings_count})" > - {{ topic.meta.postings_count - topic.meta.user_read_index }} + {{ topic.meta.unread_postings_count }} </span> </div> diff --git a/resources/vue/store/pinia/forum/ForumPost.js b/resources/vue/store/pinia/forum/ForumPost.js index a5fa749..643b42d 100644 --- a/resources/vue/store/pinia/forum/ForumPost.js +++ b/resources/vue/store/pinia/forum/ForumPost.js @@ -6,6 +6,7 @@ export const useForumPost = defineStore( const posts = ref([]); const currentPostIndex = ref(0); + const firstUnreadPostIndex = ref(-1); function initPosts(newPosts) { posts.value = newPosts; @@ -45,16 +46,22 @@ export const useForumPost = defineStore( currentPostIndex.value = index; } + function updateFirstUnreadPostIndex(index) { + firstUnreadPostIndex.value = index; + } + return { posts, currentPostIndex, + firstUnreadPostIndex, initPosts, addPost, updatePost, removePost, addPostReaction, removePostReaction, - updateCurrentPostIndex + updateCurrentPostIndex, + updateFirstUnreadPostIndex } } ) |
