FEATURE: Go to last unread for topic-level bookmark links (#14396)

Instead of going to the OP of the topic for topic-level bookmarks
(which are bookmarks where for_topic is true) when clicking on the
bookmark in the quick access menu or on the user bookmark list,
this commit takes the user to the last unread post in
the topic instead. This should be generally more useful than landing
on the unchanging OP.

To make this work nicely, I needed to add the last_read_post_number to
the BookmarkQuery based on the TopicUser association. It should not add
too much extra weight to the query, because it is limited to the user
that we are fetching bookmarks for.

Also fixed an issue where the bookmark serializer highest_post_number was
not taking into account whether the user was staff, which is when we
should use highest_staff_post_number instead.
This commit is contained in:
Martin Brennan 2021-09-21 13:49:56 +10:00 committed by GitHub
parent 7a8b5cdd5c
commit 27699648ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 32 deletions

View File

@ -125,9 +125,20 @@ const Bookmark = RestModel.extend({
).capitalize(); ).capitalize();
}, },
@discourseComputed("linked_post_number", "fancy_title", "topic_id") @discourseComputed()
topicLink(linked_post_number, fancy_title, id) { topicForList() {
return Topic.create({ id, fancy_title, linked_post_number }); // for topic level bookmarks we want to jump to the last unread post URL,
// which the topic-link helper does by default if no linked post number is
// provided
const linkedPostNumber = this.for_topic ? null : this.linked_post_number;
return Topic.create({
id: this.topic_id,
fancy_title: this.fancy_title,
linked_post_number: linkedPostNumber,
last_read_post_number: this.last_read_post_number,
highest_post_number: this.highest_post_number,
});
}, },
loadItems(params) { loadItems(params) {

View File

@ -32,7 +32,7 @@
{{d-icon "thumbtack" class="bookmark-pinned"}} {{d-icon "thumbtack" class="bookmark-pinned"}}
{{/if}} {{/if}}
{{~topic-status topic=bookmark.topicStatus~}} {{~topic-status topic=bookmark.topicStatus~}}
{{topic-link bookmark.topicLink}} {{topic-link bookmark.topicForList}}
</div> </div>
</span> </span>
<div class="link-bottom-line"> <div class="link-bottom-line">

View File

@ -42,13 +42,18 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
}, },
itemHtml(bookmark) { itemHtml(bookmark) {
// for topic level bookmarks we want to jump to the last unread post
// instead of the OP
let postNumber;
if (bookmark.for_topic) {
postNumber = bookmark.last_read_post_number + 1;
} else {
postNumber = bookmark.linked_post_number;
}
return this.attach("quick-access-item", { return this.attach("quick-access-item", {
icon: this.icon(bookmark), icon: this.icon(bookmark),
href: postUrl( href: postUrl(bookmark.slug, bookmark.topic_id, postNumber),
bookmark.slug,
bookmark.topic_id,
bookmark.post_number || bookmark.linked_post_number
),
title: bookmark.name, title: bookmark.name,
content: bookmark.title, content: bookmark.title,
username: bookmark.post_user_username, username: bookmark.post_user_username,

View File

@ -24,6 +24,7 @@ class UserBookmarkSerializer < ApplicationSerializer
:archived, :archived,
:archetype, :archetype,
:highest_post_number, :highest_post_number,
:last_read_post_number,
:bumped_at, :bumped_at,
:slug, :slug,
:post_user_username, :post_user_username,
@ -88,7 +89,15 @@ class UserBookmarkSerializer < ApplicationSerializer
end end
def highest_post_number def highest_post_number
topic.highest_post_number scope.is_staff? ? topic.highest_staff_post_number : topic.highest_post_number
end
def last_read_post_number
topic_user&.last_read_post_number
end
def topic_user
@topic_user ||= topic.topic_users.find { |tu| tu.user_id == scope.user.id }
end end
def bumped_at def bumped_at

View File

@ -68,9 +68,13 @@ class BookmarkQuery
private private
def user_bookmarks def user_bookmarks
# There is guaranteed to be a TopicUser record if the user has bookmarked
# a topic, see BookmarkManager
Bookmark.where(user: @user) Bookmark.where(user: @user)
.includes(post: :user) .includes(post: :user)
.includes(post: { topic: :tags }) .includes(post: { topic: :tags })
.includes(topic: :topic_users)
.references(:post) .references(:post)
.where(topic_users: { user_id: @user.id })
end end
end end

View File

@ -17,6 +17,8 @@ RSpec.describe BookmarkQuery do
describe "#list_all" do describe "#list_all" do
fab!(:bookmark1) { Fabricate(:bookmark, user: user) } fab!(:bookmark1) { Fabricate(:bookmark, user: user) }
fab!(:bookmark2) { Fabricate(:bookmark, user: user) } fab!(:bookmark2) { Fabricate(:bookmark, user: user) }
let!(:topic_user1) { Fabricate(:topic_user, topic: bookmark1.topic, user: user) }
let!(:topic_user2) { Fabricate(:topic_user, topic: bookmark2.topic, user: user) }
it "returns all the bookmarks for a user" do it "returns all the bookmarks for a user" do
expect(bookmark_query.list_all.count).to eq(2) expect(bookmark_query.list_all.count).to eq(2)
@ -41,11 +43,22 @@ RSpec.describe BookmarkQuery do
expect(preloaded_bookmarks.any?).to eq(true) expect(preloaded_bookmarks.any?).to eq(true)
end end
it "does not query topic_users for the bookmark topic that are not the current user" do
topic_user3 = Fabricate(:topic_user, topic: bookmark1.topic)
bookmark = bookmark_query.list_all.find do |b|
b.topic_id == bookmark1.topic_id
end
expect(bookmark.topic.topic_users.map(&:user_id)).to contain_exactly(user.id)
end
context "when q param is provided" do context "when q param is provided" do
before do before do
@post = Fabricate(:post, raw: "Some post content here", topic: Fabricate(:topic, title: "Bugfix game for devs")) @post = Fabricate(:post, raw: "Some post content here", topic: Fabricate(:topic, title: "Bugfix game for devs"))
@bookmark3 = Fabricate(:bookmark, user: user, name: "Check up later") @bookmark3 = Fabricate(:bookmark, user: user, name: "Check up later")
@bookmark4 = Fabricate(:bookmark, user: user, post: @post) @bookmark4 = Fabricate(:bookmark, user: user, post: @post)
Fabricate(:topic_user, user: user, topic: @bookmark3.topic)
Fabricate(:topic_user, user: user, topic: @bookmark4.topic)
end end
it "can search by bookmark name" do it "can search by bookmark name" do
@ -174,6 +187,13 @@ RSpec.describe BookmarkQuery do
let!(:bookmark4) { Fabricate(:bookmark, user: user, updated_at: 4.days.ago, reminder_at: nil) } let!(:bookmark4) { Fabricate(:bookmark, user: user, updated_at: 4.days.ago, reminder_at: nil) }
let!(:bookmark5) { Fabricate(:bookmark, user: user, updated_at: 3.days.ago, reminder_at: nil) } let!(:bookmark5) { Fabricate(:bookmark, user: user, updated_at: 3.days.ago, reminder_at: nil) }
before do
[bookmark1, bookmark2, bookmark3, bookmark4, bookmark5].each do |bm|
Fabricate(:topic_user, topic: bm.topic, user: user)
bm.reload
end
end
it "order defaults to updated_at DESC" do it "order defaults to updated_at DESC" do
expect(bookmark_query.list_all.map(&:id)).to eq([ expect(bookmark_query.list_all.map(&:id)).to eq([
bookmark1.id, bookmark1.id,
@ -195,7 +215,6 @@ RSpec.describe BookmarkQuery do
bookmark2.id, bookmark2.id,
bookmark3.id bookmark3.id
]) ])
end end
it "shows pinned bookmarks first ordered by reminder_at ASC then updated_at DESC" do it "shows pinned bookmarks first ordered by reminder_at ASC then updated_at DESC" do

View File

@ -9,7 +9,8 @@ RSpec.describe UserBookmarkList do
before do before do
22.times do 22.times do
Fabricate(:bookmark, user: user) bookmark = Fabricate(:bookmark, user: user)
Fabricate(:topic_user, topic: bookmark.topic, user: user)
end end
end end

View File

@ -6,10 +6,9 @@ RSpec.describe UserBookmarkSerializer do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:post) { Fabricate(:post, user: user) } let(:post) { Fabricate(:post, user: user) }
let!(:bookmark) { Fabricate(:bookmark, name: 'Test', user: user, post: post) } let!(:bookmark) { Fabricate(:bookmark, name: 'Test', user: user, post: post) }
let(:bookmark_list) { BookmarkQuery.new(user: bookmark.user).list_all.to_ary }
it "serializes all properties correctly" do it "serializes all properties correctly" do
s = UserBookmarkSerializer.new(bookmark_list.last) s = UserBookmarkSerializer.new(bookmark, scope: Guardian.new(user))
expect(s.id).to eq(bookmark.id) expect(s.id).to eq(bookmark.id)
expect(s.created_at).to eq_time(bookmark.created_at) expect(s.created_at).to eq_time(bookmark.created_at)
@ -33,24 +32,17 @@ RSpec.describe UserBookmarkSerializer do
expect(s.post_user_avatar_template).not_to eq(nil) expect(s.post_user_avatar_template).not_to eq(nil)
end end
context "when the topic is deleted" do it "uses the correct highest_post_number column based on whether the user is staff" do
before do Fabricate(:post, topic: bookmark.topic)
bookmark.topic.trash! Fabricate(:post, topic: bookmark.topic)
bookmark.reload Fabricate(:whisper, topic: bookmark.topic)
end bookmark.reload
it "it has nothing to serialize" do serializer = UserBookmarkSerializer.new(bookmark, scope: Guardian.new(user))
expect(bookmark_list).to eq([])
end
end
context "when the post is deleted" do expect(serializer.highest_post_number).to eq(3)
before do
bookmark.post.trash!
bookmark.reload
end
it "it has nothing to serialize" do
expect(bookmark_list).to eq([])
end
end
user.update!(admin: true)
expect(serializer.highest_post_number).to eq(4)
end
end end