FEATURE: Add "delete on owner reply" bookmark functionality (#10231)

This adds an option to "delete on owner reply" to bookmarks. If you select this option in the modal, then reply to the topic the bookmark is in, the bookmark will be deleted on reply.

This PR also changes the checkboxes for these additional bookmark options to an Integer column in the DB with a combobox to select the option you want.

The use cases are:

* Sometimes I will bookmark the topics to read it later. In this case we definitely don’t need to keep the bookmark after I replied to it.
* Sometimes I will read the topic in mobile and I will prefer to reply in PC later. Or I may have to do some research before reply. So I will bookmark it for reply later.
This commit is contained in:
Martin Brennan 2020-07-21 10:00:39 +10:00 committed by GitHub
parent 949c8923a4
commit 41b43a2a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 178 additions and 73 deletions

View File

@ -27,6 +27,20 @@ const LATER_TODAY_CUTOFF_HOUR = 17;
const LATER_TODAY_MAX_HOUR = 18; const LATER_TODAY_MAX_HOUR = 18;
const MOMENT_MONDAY = 1; const MOMENT_MONDAY = 1;
const MOMENT_THURSDAY = 4; const MOMENT_THURSDAY = 4;
const AUTO_DELETE_PREFERENCES = [
{
id: 0,
name: I18n.t("bookmarks.auto_delete_preference.never")
},
{
id: 1,
name: I18n.t("bookmarks.auto_delete_preference.when_reminder_sent")
},
{
id: 2,
name: I18n.t("bookmarks.auto_delete_preference.on_owner_reply")
}
];
const BOOKMARK_BINDINGS = { const BOOKMARK_BINDINGS = {
enter: { handler: "saveAndClose" }, enter: { handler: "saveAndClose" },
@ -65,7 +79,6 @@ export default Controller.extend(ModalFunctionality, {
mouseTrap: null, mouseTrap: null,
userTimezone: null, userTimezone: null,
showOptions: false, showOptions: false,
options: null,
onShow() { onShow() {
this.setProperties({ this.setProperties({
@ -79,7 +92,6 @@ export default Controller.extend(ModalFunctionality, {
lastCustomReminderTime: null, lastCustomReminderTime: null,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser), userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
showOptions: false, showOptions: false,
options: {},
model: this.model || {} model: this.model || {}
}); });
@ -143,19 +155,26 @@ export default Controller.extend(ModalFunctionality, {
_loadBookmarkOptions() { _loadBookmarkOptions() {
this.set( this.set(
"options.deleteWhenReminderSent", "autoDeletePreference",
this.model.deleteWhenReminderSent || this.model.autoDeletePreference || this._preferredDeleteOption() || 0
localStorage.bookmarkOptionsDeleteWhenReminderSent === "true"
); );
// we want to make sure the options panel opens so the user // we want to make sure the options panel opens so the user
// knows they have set these options previously. run next otherwise // knows they have set these options previously. run next otherwise
// the modal is not visible when it tries to slide down the options // the modal is not visible when it tries to slide down the options
if (this.options.deleteWhenReminderSent) { if (this.autoDeletePreference) {
next(() => this.toggleOptionsPanel()); next(() => this.toggleOptionsPanel());
} }
}, },
_preferredDeleteOption() {
let preferred = localStorage.bookmarkDeleteOption;
if (preferred && preferred !== "") {
preferred = parseInt(preferred, 10);
}
return preferred;
},
_loadLastUsedCustomReminderDatetime() { _loadLastUsedCustomReminderDatetime() {
let lastTime = localStorage.lastCustomBookmarkReminderTime; let lastTime = localStorage.lastCustomBookmarkReminderTime;
let lastDate = localStorage.lastCustomBookmarkReminderDate; let lastDate = localStorage.lastCustomBookmarkReminderDate;
@ -217,6 +236,11 @@ export default Controller.extend(ModalFunctionality, {
return REMINDER_TYPES; return REMINDER_TYPES;
}, },
@discourseComputed()
autoDeletePreferences: () => {
return AUTO_DELETE_PREFERENCES;
},
showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"), showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"),
get showLaterToday() { get showLaterToday() {
@ -294,7 +318,7 @@ export default Controller.extend(ModalFunctionality, {
localStorage.lastCustomBookmarkReminderDate = this.customReminderDate; localStorage.lastCustomBookmarkReminderDate = this.customReminderDate;
} }
localStorage.bookmarkOptionsDeleteWhenReminderSent = this.options.deleteWhenReminderSent; localStorage.bookmarkDeleteOption = this.autoDeletePreference;
let reminderType; let reminderType;
if (this.selectedReminderType === REMINDER_TYPES.NONE) { if (this.selectedReminderType === REMINDER_TYPES.NONE) {
@ -311,7 +335,7 @@ export default Controller.extend(ModalFunctionality, {
name: this.model.name, name: this.model.name,
post_id: this.model.postId, post_id: this.model.postId,
id: this.model.id, id: this.model.id,
delete_when_reminder_sent: this.options.deleteWhenReminderSent auto_delete_preference: this.autoDeletePreference
}; };
if (this._editingExistingBookmark()) { if (this._editingExistingBookmark()) {
@ -323,7 +347,7 @@ export default Controller.extend(ModalFunctionality, {
this.afterSave({ this.afterSave({
reminderAt: reminderAtISO, reminderAt: reminderAtISO,
reminderType: this.selectedReminderType, reminderType: this.selectedReminderType,
deleteWhenReminderSent: this.options.deleteWhenReminderSent, autoDeletePreference: this.autoDeletePreference,
id: this.model.id, id: this.model.id,
name: this.model.name name: this.model.name
}); });
@ -335,7 +359,7 @@ export default Controller.extend(ModalFunctionality, {
this.afterSave({ this.afterSave({
reminderAt: reminderAtISO, reminderAt: reminderAtISO,
reminderType: this.selectedReminderType, reminderType: this.selectedReminderType,
deleteWhenReminderSent: this.options.deleteWhenReminderSent, autoDeletePreference: this.autoDeletePreference,
id: response.id, id: response.id,
name: this.model.name name: this.model.name
}); });

View File

@ -110,6 +110,10 @@ export default Controller.extend(bufferedProperty("model"), {
this._super(...arguments); this._super(...arguments);
this.appEvents.on("post:show-revision", this, "_showRevision"); this.appEvents.on("post:show-revision", this, "_showRevision");
this.appEvents.on("post:created", this, () => {
this._removeDeleteOnOwnerReplyBookmarks();
this.appEvents.trigger("post-stream:refresh", { force: true });
});
this.setProperties({ this.setProperties({
selectedPostIds: [], selectedPostIds: [],
@ -183,6 +187,15 @@ export default Controller.extend(bufferedProperty("model"), {
: null; : null;
}, },
_removeDeleteOnOwnerReplyBookmarks() {
let posts = this.model.get("postStream").posts;
posts
.filter(p => p.bookmarked && p.bookmark_auto_delete_preference === 2) // 2 is on_owner_reply
.forEach(p => {
p.clearBookmark();
});
},
_forceRefreshPostStream() { _forceRefreshPostStream() {
this.appEvents.trigger("post-stream:refresh", { force: true }); this.appEvents.trigger("post-stream:refresh", { force: true });
}, },

View File

@ -305,7 +305,7 @@ const Post = RestModel.extend({
postId: this.id, postId: this.id,
id: this.bookmark_id, id: this.bookmark_id,
reminderAt: this.bookmark_reminder_at, reminderAt: this.bookmark_reminder_at,
deleteWhenReminderSent: this.bookmark_delete_when_reminder_sent, autoDeletePreference: this.bookmark_auto_delete_preference,
name: this.bookmark_name name: this.bookmark_name
}, },
title: this.bookmark_id title: this.bookmark_id
@ -324,8 +324,7 @@ const Post = RestModel.extend({
bookmarked: true, bookmarked: true,
bookmark_reminder_at: savedData.reminderAt, bookmark_reminder_at: savedData.reminderAt,
bookmark_reminder_type: savedData.reminderType, bookmark_reminder_type: savedData.reminderType,
bookmark_delete_when_reminder_sent: bookmark_auto_delete_preference: savedData.autoDeletePreference,
savedData.deleteWhenReminderSent,
bookmark_name: savedData.name, bookmark_name: savedData.name,
bookmark_id: savedData.id bookmark_id: savedData.id
}); });
@ -334,19 +333,24 @@ const Post = RestModel.extend({
}, },
afterDelete: topicBookmarked => { afterDelete: topicBookmarked => {
this.set("topic.bookmarked", topicBookmarked); this.set("topic.bookmarked", topicBookmarked);
this.setProperties({ this.clearBookmark();
bookmark_reminder_at: null,
bookmark_reminder_type: null,
bookmark_name: null,
bookmark_id: null,
bookmarked: false
});
this.appEvents.trigger("page:bookmark-post-toggled", this); this.appEvents.trigger("page:bookmark-post-toggled", this);
} }
}); });
}); });
}, },
clearBookmark() {
this.setProperties({
bookmark_reminder_at: null,
bookmark_reminder_type: null,
bookmark_name: null,
bookmark_id: null,
bookmarked: false,
bookmark_auto_delete_preference: null
});
},
updateActionsSummary(json) { updateActionsSummary(json) {
if (json && json.id === this.id) { if (json && json.id === this.id) {
json = Post.munge(json); json = Post.munge(json);

View File

@ -73,12 +73,7 @@ export default DiscourseRoute.extend({
// completely clear out all the bookmark related attributes // completely clear out all the bookmark related attributes
// because they are not in the response if bookmarked == false // because they are not in the response if bookmarked == false
if (closestPost && !closestPost.bookmarked) { if (closestPost && !closestPost.bookmarked) {
closestPost.setProperties({ closestPost.clearBookmark();
bookmark_reminder_at: null,
bookmark_reminder_type: null,
bookmark_name: null,
bookmark_id: null
});
} }
if (!isEmpty(topic.draft)) { if (!isEmpty(topic.draft)) {

View File

@ -14,10 +14,13 @@
</div> </div>
<div class="bookmark-options-panel"> <div class="bookmark-options-panel">
<label for="delete_when_reminder_sent"> <label class="control-label" for="bookmark_auto_delete_preference">{{i18n "bookmarks.auto_delete_preference.label"}}</label>
{{i18n "bookmarks.delete_when_reminder_sent"}} {{combo-box
{{input id="delete_when_reminder_sent" type="checkbox" checked=options.deleteWhenReminderSent}} content=autoDeletePreferences
</label> value=autoDeletePreference
class="bookmark-option-selector"
onChange=(action (mut autoDeletePreference))
}}
</div> </div>
{{#if showExistingReminderAt }} {{#if showExistingReminderAt }}

View File

@ -35,9 +35,9 @@
{{#if site.mobileView}} {{#if site.mobileView}}
<td> <td>
{{#if bookmark.post_user_avatar_template}} {{#if bookmark.post_user_avatar_template}}
<a href={{bookmark.postUser.path}} data-user-card={{bookmark.post_user_username}}> <a href={{bookmark.postUser.path}} data-user-card={{bookmark.post_user_username}}>
{{avatar bookmark.postUser avatarTemplatePath="avatar_template" usernamePath="username" namePath="name" imageSize="small"}} {{avatar bookmark.postUser avatarTemplatePath="avatar_template" usernamePath="username" namePath="name" imageSize="small"}}
</a> </a>
{{/if}} {{/if}}
</td> </td>
{{/if}} {{/if}}

View File

@ -75,14 +75,24 @@
margin-left: 0.5em; margin-left: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
background: transparent; background: transparent;
padding-right: 6px;
} }
.bookmark-options-panel { .bookmark-options-panel {
.select-kit {
width: 100%;
}
margin-bottom: 18px; margin-bottom: 18px;
display: none; display: none;
input[type="checkbox"] { label {
margin-right: 0.85em; display: flex;
span {
display: block;
flex: 1;
}
} }
} }
} }

View File

@ -13,7 +13,7 @@ class BookmarksController < ApplicationController
reminder_type: params[:reminder_type], reminder_type: params[:reminder_type],
reminder_at: params[:reminder_at], reminder_at: params[:reminder_at],
options: { options: {
delete_when_reminder_sent: params[:delete_when_reminder_sent] auto_delete_preference: params[:auto_delete_preference] || 0
} }
) )
@ -40,7 +40,7 @@ class BookmarksController < ApplicationController
reminder_type: params[:reminder_type], reminder_type: params[:reminder_type],
reminder_at: params[:reminder_at], reminder_at: params[:reminder_at],
options: { options: {
delete_when_reminder_sent: params[:delete_when_reminder_sent] auto_delete_preference: params[:auto_delete_preference] || 0
} }
) )

View File

@ -44,6 +44,14 @@ class Bookmark < ActiveRecord::Base
self.reminder_at.blank? && self.reminder_type.blank? self.reminder_at.blank? && self.reminder_type.blank?
end end
def delete_when_reminder_sent?
self.auto_delete_preference == Bookmark.auto_delete_preferences[:when_reminder_sent]
end
def delete_on_owner_reply?
self.auto_delete_preference == Bookmark.auto_delete_preferences[:on_owner_reply]
end
scope :pending_reminders, ->(before_time = Time.now.utc) do scope :pending_reminders, ->(before_time = Time.now.utc) do
where("reminder_at IS NOT NULL AND reminder_at <= :before_time", before_time: before_time) where("reminder_at IS NOT NULL AND reminder_at <= :before_time", before_time: before_time)
end end
@ -53,7 +61,7 @@ class Bookmark < ActiveRecord::Base
end end
def self.reminder_types def self.reminder_types
@reminder_type = Enum.new( @reminder_types ||= Enum.new(
later_today: 1, later_today: 1,
next_business_day: 2, next_business_day: 2,
tomorrow: 3, tomorrow: 3,
@ -65,6 +73,14 @@ class Bookmark < ActiveRecord::Base
) )
end end
def self.auto_delete_preferences
@auto_delete_preferences ||= Enum.new(
never: 0,
when_reminder_sent: 1,
on_owner_reply: 2
)
end
def self.count_per_day(opts = nil) def self.count_per_day(opts = nil)
opts ||= {} opts ||= {}
result = where('bookmarks.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago) result = where('bookmarks.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago)
@ -91,7 +107,7 @@ end
# updated_at :datetime not null # updated_at :datetime not null
# reminder_last_sent_at :datetime # reminder_last_sent_at :datetime
# reminder_set_at :datetime # reminder_set_at :datetime
# delete_when_reminder_sent :boolean default(FALSE), not null # auto_delete_preference :integer
# #
# Indexes # Indexes
# #

View File

@ -54,7 +54,7 @@ class PostSerializer < BasicPostSerializer
:bookmark_id, :bookmark_id,
:bookmark_reminder_type, :bookmark_reminder_type,
:bookmark_name, :bookmark_name,
:bookmark_delete_when_reminder_sent, :bookmark_auto_delete_preference,
:raw, :raw,
:actions_summary, :actions_summary,
:moderator?, :moderator?,
@ -335,7 +335,11 @@ class PostSerializer < BasicPostSerializer
bookmarked bookmarked
end end
def include_bookmark_delete_when_reminder_sent? def include_bookmark_auto_delete_preference?
bookmarked
end
def include_bookmark_delete_on_owner_reply?
bookmarked bookmarked
end end
@ -361,8 +365,8 @@ class PostSerializer < BasicPostSerializer
post_bookmark&.name post_bookmark&.name
end end
def bookmark_delete_when_reminder_sent def bookmark_auto_delete_preference
post_bookmark&.delete_when_reminder_sent post_bookmark&.auto_delete_preference
end end
def bookmark_id def bookmark_id

View File

@ -319,7 +319,11 @@ en:
no_timezone: 'You have not set a timezone yet. You will not be able to set reminders. Set one up <a href="%{basePath}/my/preferences/profile">in your profile</a>.' no_timezone: 'You have not set a timezone yet. You will not be able to set reminders. Set one up <a href="%{basePath}/my/preferences/profile">in your profile</a>.'
invalid_custom_datetime: "The date and time you provided is invalid, please try again." invalid_custom_datetime: "The date and time you provided is invalid, please try again."
list_permission_denied: "You do not have permission to view this user's bookmarks." list_permission_denied: "You do not have permission to view this user's bookmarks."
delete_when_reminder_sent: "Delete this bookmark when the reminder notification is sent." auto_delete_preference:
label: "Automatically delete"
never: "Never"
when_reminder_sent: "Once the reminder is sent"
on_owner_reply: "After I reply to this topic"
search_placeholder: "Search bookmarks by name, topic title, or post content" search_placeholder: "Search bookmarks by name, topic title, or post content"
search: "Search" search: "Search"
reminders: reminders:

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class AddDeleteOptionToBookmarks < ActiveRecord::Migration[6.0]
def up
add_column :bookmarks, :auto_delete_preference, :integer, index: true, null: false, default: 0
DB.exec("UPDATE bookmarks SET auto_delete_preference = 1 WHERE delete_when_reminder_sent")
end
def down
remove_column :bookmarks, :auto_delete_preference
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RemoveBookmarksDeleteWhenReminderSent < ActiveRecord::Migration[6.0]
def up
remove_column :bookmarks, :delete_when_reminder_sent
end
def down
add_column :bookmarks, :delete_when_reminder_sent, :boolean, default: false
end
end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class BookmarkManager class BookmarkManager
DEFAULT_OPTIONS = { delete_when_reminder_sent: false }
include HasErrors include HasErrors
def initialize(user) def initialize(user)
@ -24,7 +22,7 @@ class BookmarkManager
reminder_type: reminder_type, reminder_type: reminder_type,
reminder_at: reminder_at, reminder_at: reminder_at,
reminder_set_at: Time.zone.now reminder_set_at: Time.zone.now
}.merge(default_options(options)) }.merge(options)
) )
if bookmark.errors.any? if bookmark.errors.any?
@ -84,7 +82,7 @@ class BookmarkManager
reminder_at: reminder_at, reminder_at: reminder_at,
reminder_type: reminder_type, reminder_type: reminder_type,
reminder_set_at: Time.zone.now reminder_set_at: Time.zone.now
}.merge(default_options(options)) }.merge(options)
) )
if bookmark.errors.any? if bookmark.errors.any?
@ -104,8 +102,4 @@ class BookmarkManager
return if reminder_type.blank? return if reminder_type.blank?
reminder_type.is_a?(Integer) ? reminder_type : Bookmark.reminder_types[reminder_type.to_sym] reminder_type.is_a?(Integer) ? reminder_type : Bookmark.reminder_types[reminder_type.to_sym]
end end
def default_options(options)
DEFAULT_OPTIONS.merge(options) { |key, old, new| new.nil? ? old : new }
end
end end

View File

@ -202,6 +202,7 @@ class PostCreator
create_embedded_topic create_embedded_topic
@post.link_post_uploads @post.link_post_uploads
update_uploads_secure_status update_uploads_secure_status
delete_owned_bookmarks
ensure_in_allowed_users if guardian.is_staff? ensure_in_allowed_users if guardian.is_staff?
unarchive_message if !@opts[:import_mode] unarchive_message if !@opts[:import_mode]
DraftSequence.next!(@user, draft_key) if !@opts[:import_mode] DraftSequence.next!(@user, draft_key) if !@opts[:import_mode]
@ -402,6 +403,14 @@ class PostCreator
@post.update_uploads_secure_status @post.update_uploads_secure_status
end end
def delete_owned_bookmarks
return if !@post.topic_id
@user.bookmarks.where(
topic_id: @post.topic_id,
auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply]
).destroy_all
end
def handle_spam def handle_spam
if @spam if @spam
GroupMessage.create(Group[:moderators].name, GroupMessage.create(Group[:moderators].name,

View File

@ -654,6 +654,19 @@ describe PostCreator do
end end
end end
context "when the user has bookmarks with auto_delete_preference on_owner_reply" do
before do
Fabricate(:bookmark, topic: topic, user: user, auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply])
Fabricate(:bookmark, topic: topic, user: user, auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply])
Fabricate(:bookmark, topic: topic, user: user)
Fabricate(:bookmark, user: user)
end
it "deletes the bookmarks" do
creator.create
expect(Bookmark.where(user: user).count).to eq(2)
end
end
context "topic stats" do context "topic stats" do
before do before do
PostCreator.new( PostCreator.new(

View File

@ -43,24 +43,13 @@ RSpec.describe BookmarkManager do
end end
context "when options are provided" do context "when options are provided" do
let(:options) { { delete_when_reminder_sent: true } } let(:options) { { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } }
it "saves any additional options successfully" do it "saves any additional options successfully" do
subject.create(post_id: post.id, name: name, options: options) subject.create(post_id: post.id, name: name, options: options)
bookmark = Bookmark.find_by(user: user) bookmark = Bookmark.find_by(user: user)
expect(bookmark.delete_when_reminder_sent).to eq(true) expect(bookmark.auto_delete_preference).to eq(1)
end
end
context "when options are provided with null values" do
let(:options) { { delete_when_reminder_sent: nil } }
it "saves defaults successfully" do
subject.create(post_id: post.id, name: name, options: options)
bookmark = Bookmark.find_by(user: user)
expect(bookmark.delete_when_reminder_sent).to eq(false)
end end
end end
@ -192,12 +181,12 @@ RSpec.describe BookmarkManager do
end end
context "when options are provided" do context "when options are provided" do
let(:options) { { delete_when_reminder_sent: true } } let(:options) { { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } }
it "saves any additional options successfully" do it "saves any additional options successfully" do
update_bookmark update_bookmark
bookmark.reload bookmark.reload
expect(bookmark.delete_when_reminder_sent).to eq(true) expect(bookmark.auto_delete_preference).to eq(1)
end end
end end

View File

@ -34,9 +34,9 @@ RSpec.describe BookmarkReminderNotificationHandler do
expect(bookmark.reload.no_reminder?).to eq(true) expect(bookmark.reload.no_reminder?).to eq(true)
end end
context "when the delete_when_reminder_sent boolean is true " do context "when the auto_delete_preference is when_reminder_sent" do
it "deletes the bookmark after the reminder gets sent" do it "deletes the bookmark after the reminder gets sent" do
bookmark.update(delete_when_reminder_sent: true) bookmark.update(auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent])
subject.send_notification(bookmark) subject.send_notification(bookmark)
expect(Bookmark.find_by(id: bookmark.id)).to eq(nil) expect(Bookmark.find_by(id: bookmark.id)).to eq(nil)
end end

View File

@ -1,4 +1,5 @@
import I18n from "I18n"; import I18n from "I18n";
import selectKit from "helpers/select-kit-helper";
import { import {
acceptance, acceptance,
loggedInUser, loggedInUser,
@ -115,7 +116,8 @@ test("Opening the options panel and remembering the option", async assert => {
exists(".bookmark-options-panel"), exists(".bookmark-options-panel"),
"it should open the options panel" "it should open the options panel"
); );
await click("#delete_when_reminder_sent"); await selectKit(".bookmark-option-selector").expand();
await selectKit(".bookmark-option-selector").selectRowByValue(1);
await click("#save-bookmark"); await click("#save-bookmark");
await openEditBookmarkModal(); await openEditBookmarkModal();
@ -123,9 +125,11 @@ test("Opening the options panel and remembering the option", async assert => {
exists(".bookmark-options-panel"), exists(".bookmark-options-panel"),
"it should reopen the options panel" "it should reopen the options panel"
); );
assert.ok( assert.equal(
exists(".bookmark-options-panel #delete_when_reminder_sent:checked"), selectKit(".bookmark-option-selector")
"it should pre-check delete when reminder sent option" .header()
.value(),
1
); );
assert.verifySteps(["none"]); assert.verifySteps(["none"]);
}); });