0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/app/serializers/topic_list_item_serializer.rb
Mark VanLandingham 1e5e20ffc5
FEATURE: Notification/tracking changes for nested replies (#39750)
Reply consolidation by bucket for nested view. Replies under the same
parent now collapse into one notification ("3 new replies in your topic"
/ "to your post"). Clicking lands you at that bucket's level with
sort=new&collapse_replies=true so you only see the new content. Flat
topics keep the existing single-row consolidated shape (specs guard
against regression). Root posts that don't explicitly reply to
anyone resolve to the OP, so the OP still gets notified for fresh roots
in their own topic.
                                                                  
Auto-Track instead of auto-Watch for the OP. PMs and flat topics are
unchanged.
  
Topic-list "new replies" dot. Replaces the unread/new count badges for
nested topics when there's new content since the user's last visit. The
count-based UI doesn't fit nested's read pattern.
                                                                  
collapse_replies=true URL param. On the nested view, starts replies
hidden behind an Expand button. Set automatically by consolidated
notifications.

---------

Co-authored-by: Rafael Silva <xfalcox@gmail.com>
2026-05-07 12:33:56 -05:00

189 lines
4.6 KiB
Ruby
Vendored

# frozen_string_literal: true
class TopicListItemSerializer < ListableTopicSerializer
include TopicTagsMixin
attributes :views,
:like_count,
:has_summary,
:archetype,
:last_poster_username,
:category_id,
:op_like_count,
:op_can_like,
:op_liked,
:first_post_id,
:pinned_globally,
:liked_post_numbers,
:featured_link,
:featured_link_root_domain,
:allowed_user_count,
:participant_groups,
:is_hot,
:is_nested_view,
:has_new_replies
has_many :posters, serializer: TopicPosterSerializer, embed: :objects
has_many :participants, serializer: TopicPosterSerializer, embed: :objects
def include_participant_groups?
object.private_message?
end
def posters
object.posters || object.posters_summary || []
end
def op_like_count
object.first_post && object.first_post.like_count
end
def include_op_can_like?
serialize_topic_op_likes_data_enabled?
end
def op_can_like
return false if !scope.user || !object.first_post
first_post = object.first_post
return false if first_post.user_id == scope.user.id
return false unless scope.post_can_act?(first_post, :like)
first_post_liked =
PostAction.where(
user_id: scope.user.id,
post_id: first_post.id,
post_action_type_id: PostActionType.types[:like],
).first
return scope.can_delete?(first_post_liked) if first_post_liked
true
end
def include_op_liked?
serialize_topic_op_likes_data_enabled?
end
def op_liked
return false if !scope.user || !object.first_post
PostAction.where(
user_id: scope.user.id,
post_id: object.first_post.id,
post_action_type_id: PostActionType.types[:like],
).exists?
end
def include_first_post_id?
serialize_topic_op_likes_data_enabled?
end
def first_post_id
return false if !object.first_post
object.first_post.id
end
def last_poster_username
posters.find { |poster| poster.user.id == object.last_post_user_id }.try(:user).try(:username)
end
def category_id
# If it's a shared draft, show the destination topic instead
if object.includes_destination_category && object.shared_draft
return object.shared_draft.category_id
end
object.category_id
end
def participants
object.participants_summary || []
end
def participant_groups
object.participant_groups_summary || []
end
def include_liked_post_numbers?
include_post_action? :like
end
def include_post_action?(action)
object.user_data && object.user_data.post_action_data &&
object.user_data.post_action_data.key?(PostActionType.types[action])
end
def liked_post_numbers
object.user_data.post_action_data[PostActionType.types[:like]]
end
def include_participants?
object.private_message?
end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end
def include_featured_link_root_domain?
SiteSetting.topic_featured_link_enabled && object.featured_link.present?
end
def allowed_user_count
# Don't use count as it will result in a query
object.allowed_users.length
end
def include_allowed_user_count?
object.private_message?
end
def is_hot
TopicHotScore.hottest_topic_ids.include?(object.id)
end
def include_is_hot?
theme_enabled = theme_modifier_helper.serialize_topic_is_hot
plugin_enabled = DiscoursePluginRegistry.apply_modifier(:serialize_topic_is_hot, false)
theme_enabled || plugin_enabled
end
def is_nested_view
true
end
def include_is_nested_view?
object.nested_view?
end
# The *only* "topic has new content" signal we render for nested topics —
# the existing unread_posts/new_posts/unseen badges are deliberately not
# shown when is_nested_view is true (see post-count-or-badges.gjs).
def has_new_replies
true
end
def include_has_new_replies?
return false unless scope.user
return false unless object.nested_view?
last_visited = object.user_data&.last_visited_at
return false if last_visited.blank?
return false if object.last_post_user_id == scope.user.id
object.bumped_at > last_visited
end
private
def serialize_topic_op_likes_data_enabled?
theme_enabled = theme_modifier_helper.serialize_topic_op_likes_data
plugin_enabled = DiscoursePluginRegistry.apply_modifier(:serialize_topic_op_likes_data, false)
theme_enabled || plugin_enabled
end
def theme_modifier_helper
@theme_modifier_helper ||= ThemeModifierHelper.new(request: scope.request)
end
end