diff --git a/app/assets/javascripts/discourse/components/bookmark-actions-dropdown.js.es6 b/app/assets/javascripts/discourse/components/bookmark-actions-dropdown.js.es6
new file mode 100644
index 00000000000..6e72fafc448
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/bookmark-actions-dropdown.js.es6
@@ -0,0 +1,33 @@
+import { computed } from "@ember/object";
+import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
+import { action } from "@ember/object";
+
+export default DropdownSelectBoxComponent.extend({
+ classNames: ["bookmark-actions-dropdown"],
+ pluginApiIdentifiers: ["bookmark-actions-dropdown"],
+ selectKitOptions: {
+ icon: null,
+ translatedNone: "...",
+ showFullTitle: true
+ },
+
+ content: computed(() => {
+ return [
+ {
+ id: "remove",
+ icon: "trash-alt",
+ name: I18n.t("post.bookmarks.actions.delete_bookmark.name"),
+ description: I18n.t(
+ "post.bookmarks.actions.delete_bookmark.description"
+ )
+ }
+ ];
+ }),
+
+ @action
+ onChange(selectedAction) {
+ if (selectedAction === "remove") {
+ this.removeBookmark(this.bookmark);
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/bookmark.js.es6 b/app/assets/javascripts/discourse/controllers/bookmark.js.es6
index 109678a7b2c..b4b74ec57ef 100644
--- a/app/assets/javascripts/discourse/controllers/bookmark.js.es6
+++ b/app/assets/javascripts/discourse/controllers/bookmark.js.es6
@@ -124,7 +124,7 @@ export default Controller.extend(ModalFunctionality, {
const reminderAt = this.reminderAt();
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
- if (!reminderAt) {
+ if (!reminderAt && this.selectedReminderType === REMINDER_TYPES.CUSTOM) {
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
}
diff --git a/app/assets/javascripts/discourse/controllers/user-activity-bookmarks-with-reminders.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity-bookmarks-with-reminders.js.es6
new file mode 100644
index 00000000000..49982b12449
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/user-activity-bookmarks-with-reminders.js.es6
@@ -0,0 +1,54 @@
+import Controller from "@ember/controller";
+import { inject } from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
+import Bookmark from "discourse/models/bookmark";
+
+export default Controller.extend({
+ application: inject(),
+ user: inject(),
+
+ content: null,
+ loading: false,
+ noResultsHelp: null,
+
+ loadItems() {
+ this.setProperties({
+ content: [],
+ loading: true,
+ noResultsHelp: null
+ });
+
+ return this.model
+ .loadItems()
+ .then(response => {
+ if (response && response.no_results_help) {
+ this.set("noResultsHelp", response.no_results_help);
+ }
+
+ if (response && response.bookmarks) {
+ let bookmarks = [];
+ response.bookmarks.forEach(bookmark => {
+ bookmarks.push(Bookmark.create(bookmark));
+ });
+ this.content.pushObjects(bookmarks);
+ }
+ })
+ .finally(() =>
+ this.setProperties({
+ loaded: true,
+ loading: false
+ })
+ );
+ },
+
+ @discourseComputed("loaded", "content.length")
+ noContent(loaded, content) {
+ return loaded && content.length === 0;
+ },
+
+ actions: {
+ removeBookmark(bookmark) {
+ return bookmark.destroy().then(() => this.loadItems());
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
index 12333fcf074..03828c439b2 100644
--- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6
@@ -3,6 +3,7 @@ import { inject as service } from "@ember/service";
import Controller, { inject as controller } from "@ember/controller";
import { exportUserArchive } from "discourse/lib/export-csv";
import { observes } from "discourse-common/utils/decorators";
+import { setting } from "discourse/lib/computed";
export default Controller.extend({
application: controller(),
@@ -11,6 +12,7 @@ export default Controller.extend({
userActionType: null,
canDownloadPosts: alias("user.viewingSelf"),
+ bookmarksWithRemindersEnabled: setting("enable_bookmarks_with_reminders"),
@observes("userActionType", "model.stream.itemsLoaded")
_showFooter: function() {
diff --git a/app/assets/javascripts/discourse/models/bookmark.js.es6 b/app/assets/javascripts/discourse/models/bookmark.js.es6
index 2ae4e63e4cd..0473c108118 100644
--- a/app/assets/javascripts/discourse/models/bookmark.js.es6
+++ b/app/assets/javascripts/discourse/models/bookmark.js.es6
@@ -1,8 +1,16 @@
+import Category from "discourse/models/category";
+import { isRTL } from "discourse/lib/text-direction";
+import { censor } from "pretty-text/censored-words";
+import { emojiUnescape } from "discourse/lib/text";
+import Site from "discourse/models/site";
+import { longDate } from "discourse/lib/formatter";
+import PreloadStore from "preload-store";
import { none } from "@ember/object/computed";
import { computed } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { Promise } from "rsvp";
import RestModel from "discourse/models/rest";
+import discourseComputed from "discourse-common/utils/decorators";
const Bookmark = RestModel.extend({
newBookmark: none("id"),
@@ -18,6 +26,107 @@ const Bookmark = RestModel.extend({
return ajax(this.url, {
type: "DELETE"
});
+ },
+
+ @discourseComputed("highest_post_number", "url")
+ lastPostUrl(highestPostNumber) {
+ return this.urlForPostNumber(highestPostNumber);
+ },
+
+ // Helper to build a Url with a post number
+ urlForPostNumber(postNumber) {
+ let url = Discourse.getURL(`/t/${this.topic_id}`);
+ if (postNumber > 0) {
+ url += `/${postNumber}`;
+ }
+ return url;
+ },
+
+ // returns createdAt if there's no bumped date
+ @discourseComputed("bumped_at", "createdAt")
+ bumpedAt(bumped_at, createdAt) {
+ if (bumped_at) {
+ return new Date(bumped_at);
+ } else {
+ return createdAt;
+ }
+ },
+
+ @discourseComputed("bumpedAt", "createdAt")
+ bumpedAtTitle(bumpedAt, createdAt) {
+ const firstPost = I18n.t("first_post");
+ const lastPost = I18n.t("last_post");
+ const createdAtDate = longDate(createdAt);
+ const bumpedAtDate = longDate(bumpedAt);
+
+ return I18n.messageFormat("topic.bumped_at_title_MF", {
+ FIRST_POST: firstPost,
+ CREATED_AT: createdAtDate,
+ LAST_POST: lastPost,
+ BUMPED_AT: bumpedAtDate
+ });
+ },
+
+ @discourseComputed("title")
+ fancyTitle(title) {
+ let fancyTitle = censor(
+ emojiUnescape(title) || "",
+ Site.currentProp("censored_regexp")
+ );
+
+ if (this.siteSettings.support_mixed_text_direction) {
+ const titleDir = isRTL(title) ? "rtl" : "ltr";
+ return `${fancyTitle}`;
+ }
+ return fancyTitle;
+ },
+
+ @discourseComputed("created_at")
+ createdAt(created_at) {
+ return new Date(created_at);
+ },
+
+ @discourseComputed("tags")
+ visibleListTags(tags) {
+ if (!tags || !this.siteSettings.suppress_overlapping_tags_in_list) {
+ return tags;
+ }
+
+ const title = this.title;
+ const newTags = [];
+
+ tags.forEach(function(tag) {
+ if (title.toLowerCase().indexOf(tag) === -1) {
+ newTags.push(tag);
+ }
+ });
+
+ return newTags;
+ },
+
+ @discourseComputed("category_id")
+ category(categoryId) {
+ return Category.findById(categoryId);
+ },
+
+ @discourseComputed("reminder_at")
+ formattedReminder(bookmarkReminderAt) {
+ const currentUser = PreloadStore.get("currentUser");
+ return moment
+ .tz(bookmarkReminderAt, currentUser.timezone || moment.tz.guess())
+ .format(I18n.t("dates.long_with_year"));
+ },
+
+ loadItems() {
+ return ajax(`/u/${this.user.username}/bookmarks.json`, { cache: "false" });
+ }
+});
+
+Bookmark.reopenClass({
+ create(args) {
+ args = args || {};
+ args.siteSettings = args.siteSettings || Discourse.SiteSettings;
+ return this._super(args);
}
});
diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6
index fa97e11ea77..a3c9ee26c66 100644
--- a/app/assets/javascripts/discourse/models/topic.js.es6
+++ b/app/assets/javascripts/discourse/models/topic.js.es6
@@ -136,7 +136,12 @@ const Topic = RestModel.extend({
const createdAtDate = longDate(createdAt);
const bumpedAtDate = longDate(bumpedAt);
- return `${firstPost}: ${createdAtDate}\n${lastPost}: ${bumpedAtDate}`;
+ return I18n.messageFormat("topic.bumped_at_title_MF", {
+ FIRST_POST: firstPost,
+ CREATED_AT: createdAtDate,
+ LAST_POST: lastPost,
+ BUMPED_AT: bumpedAtDate
+ });
},
@discourseComputed("created_at")
@@ -259,7 +264,7 @@ const Topic = RestModel.extend({
// Helper to build a Url with a post number
urlForPostNumber(postNumber) {
let url = this.url;
- if (postNumber && postNumber > 0) {
+ if (postNumber > 0) {
url += `/${postNumber}`;
}
return url;
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 7af461aa309..026b27926a0 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -5,6 +5,7 @@ import EmberObject, { computed, getProperties } from "@ember/object";
import { ajax } from "discourse/lib/ajax";
import { url } from "discourse/lib/computed";
import RestModel from "discourse/models/rest";
+import Bookmark from "discourse/models/bookmark";
import UserStream from "discourse/models/user-stream";
import UserPostsStream from "discourse/models/user-posts-stream";
import Singleton from "discourse/mixins/singleton";
@@ -52,6 +53,11 @@ const User = RestModel.extend({
return UserStream.create({ user: this });
},
+ @discourseComputed()
+ bookmarks() {
+ return Bookmark.create({ user: this });
+ },
+
@discourseComputed()
postsStream() {
return UserPostsStream.create({ user: this });
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index 16720a568ef..fb13822bb26 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -122,6 +122,9 @@ export default function() {
this.route("replies");
this.route("likesGiven", { path: "likes-given" });
this.route("bookmarks");
+ this.route("bookmarksWithReminders", {
+ path: "bookmarks-with-reminders"
+ });
this.route("pending");
this.route("drafts");
}
diff --git a/app/assets/javascripts/discourse/routes/discovery.js.es6 b/app/assets/javascripts/discourse/routes/discovery.js.es6
index d8129506668..1dd2985c46e 100644
--- a/app/assets/javascripts/discourse/routes/discovery.js.es6
+++ b/app/assets/javascripts/discourse/routes/discovery.js.es6
@@ -13,16 +13,27 @@ export default DiscourseRoute.extend(OpenComposer, {
},
beforeModel(transition) {
- const user = User;
+ // the new bookmark list is radically different to this topic-based one,
+ // including being able to show links to multiple posts to the same topic
+ // and being based on a different model. better to just redirect
const url = transition.intent.url;
+ if (
+ this.siteSettings.enable_bookmarks_with_reminders &&
+ url === "/bookmarks"
+ ) {
+ this.transitionTo(
+ "userActivity.bookmarksWithReminders",
+ this.currentUser
+ );
+ }
if (
(url === "/" || url === "/latest" || url === "/categories") &&
transition.targetName.indexOf("discovery.top") === -1 &&
- user.currentProp("should_be_redirected_to_top")
+ User.currentProp("should_be_redirected_to_top")
) {
- user.currentProp("should_be_redirected_to_top", false);
- const period = user.currentProp("redirected_to_top.period") || "all";
+ User.currentProp("should_be_redirected_to_top", false);
+ const period = User.currentProp("redirected_to_top.period") || "all";
this.replaceWith(`discovery.top${period.capitalize()}`);
}
},
diff --git a/app/assets/javascripts/discourse/routes/user-activity-bookmarks-with-reminders.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-bookmarks-with-reminders.js.es6
new file mode 100644
index 00000000000..fc8bd078264
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user-activity-bookmarks-with-reminders.js.es6
@@ -0,0 +1,29 @@
+import DiscourseRoute from "discourse/routes/discourse";
+
+export default DiscourseRoute.extend({
+ noContentHelpKey: "user_activity.no_bookmarks",
+
+ queryParams: {
+ acting_username: { refreshModel: true }
+ },
+
+ model() {
+ return this.modelFor("user").get("bookmarks");
+ },
+
+ renderTemplate() {
+ this.render("user_bookmarks");
+ },
+
+ setupController(controller, model) {
+ controller.set("model", model);
+ controller.loadItems();
+ },
+
+ actions: {
+ didTransition() {
+ this.controllerFor("user-activity")._showFooter();
+ return true;
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs
index 6c39cc46181..2623e57c050 100644
--- a/app/assets/javascripts/discourse/templates/user/activity.hbs
+++ b/app/assets/javascripts/discourse/templates/user/activity.hbs
@@ -18,9 +18,15 @@
{{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
{{#if user.showBookmarks}}
-
- {{#link-to 'userActivity.bookmarks'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
-
+ {{#if bookmarksWithRemindersEnabled}}
+
+ {{#link-to 'userActivity.bookmarksWithReminders'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
+
+ {{else}}
+
+ {{#link-to 'userActivity.bookmarks'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
+
+ {{/if}}
{{/if}}
{{plugin-outlet name="user-activity-bottom"
connectorTagName='li'
diff --git a/app/assets/javascripts/discourse/templates/user/bookmarks.hbs b/app/assets/javascripts/discourse/templates/user/bookmarks.hbs
new file mode 100644
index 00000000000..228490bfa4f
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/user/bookmarks.hbs
@@ -0,0 +1,51 @@
+{{#if noContent}}
+ {{noResultsHelp}}
+{{else}}
+ {{#conditional-loading-spinner condition=loading}}
+
+
+ {{i18n "topic.title"}} |
+ {{i18n "post.bookmarks.created"}} |
+ {{i18n "activity"}} |
+ |
+
+
+ {{#each content as |bookmark|}}
+
+
+
+
+ {{#if bookmark.name}}
+
+ {{d-icon "info-circle"}}{{bookmark.name}}
+
+ {{/if}}
+ {{#if bookmark.reminder_at}}
+
+ {{d-icon "far-clock"}}{{bookmark.formattedReminder}}
+
+ {{/if}}
+
+
+ {{topic-status topic=bookmark}}
+ {{topic-link bookmark}}
+
+ {{#if bookmark.excerpt}}
+ {{html-safe bookmark.excerpt}}
+ {{/if}}
+
+ {{category-link bookmark.category}}
+ {{discourse-tags bookmark mode="list" tagsForUser=tagsForUser}}
+
+ |
+ {{format-date bookmark.created_at format="tiny"}} |
+ {{raw "list/activity-column" topic=bookmark class="num" tagName="td"}}
+
+ {{bookmark-actions-dropdown bookmark=bookmark removeBookmark=(action "removeBookmark")}}
+ |
+
+ {{/each}}
+
+
+ {{/conditional-loading-spinner}}
+{{/if}}
diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
index 6ae6460fac4..95c3477ffd6 100644
--- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6
+++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6
@@ -321,6 +321,8 @@ registerButton("bookmarkWithReminder", (attrs, state, siteSettings) => {
titleOptions = {
date: reminderAtDate.format(I18n.t("dates.long_with_year"))
};
+ } else if (attrs.bookmarkReminderType === "at_desktop") {
+ title = "bookmarks.created_with_at_desktop_reminder";
} else {
title = "bookmarks.created";
}
diff --git a/app/assets/javascripts/discourse/widgets/quick-access-bookmarks.js.es6 b/app/assets/javascripts/discourse/widgets/quick-access-bookmarks.js.es6
index ca1642b8975..b63c2b527b6 100644
--- a/app/assets/javascripts/discourse/widgets/quick-access-bookmarks.js.es6
+++ b/app/assets/javascripts/discourse/widgets/quick-access-bookmarks.js.es6
@@ -16,7 +16,11 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
},
showAllHref() {
- return `${this.attrs.path}/activity/bookmarks`;
+ if (this.siteSettings.enable_bookmarks_with_reminders) {
+ return `${this.attrs.path}/activity/bookmarks-with-reminders`;
+ } else {
+ return `${this.attrs.path}/activity/bookmarks`;
+ }
},
emptyStatePlaceholderItem() {
@@ -24,6 +28,50 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
},
findNewItems() {
+ if (this.siteSettings.enable_bookmarks_with_reminders) {
+ return this.loadBookmarksWithReminders();
+ } else {
+ return this.loadUserActivityBookmarks();
+ }
+ },
+
+ itemHtml(bookmark) {
+ return this.attach("quick-access-item", {
+ icon: this.icon(bookmark),
+ href: postUrl(
+ bookmark.slug,
+ bookmark.topic_id,
+ bookmark.post_number || bookmark.linked_post_number
+ ),
+ content: bookmark.title,
+ username: bookmark.username
+ });
+ },
+
+ icon(bookmark) {
+ if (bookmark.reminder_at) {
+ return "discourse-bookmark-clock";
+ }
+ return ICON;
+ },
+
+ loadBookmarksWithReminders() {
+ return ajax(`/u/${this.currentUser.username}/bookmarks.json`, {
+ cache: "false",
+ data: {
+ limit: this.estimateItemLimit()
+ }
+ }).then(result => {
+ // The empty state help text for bookmarks page is localized on the
+ // server.
+ if (result.no_results_help) {
+ this.state.emptyStatePlaceholderItemText = result.no_results_help;
+ }
+ return result.bookmarks;
+ });
+ },
+
+ loadUserActivityBookmarks() {
return ajax("/user_actions.json", {
cache: "false",
data: {
@@ -38,14 +86,5 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
this.state.emptyStatePlaceholderItemText = no_results_help;
return user_actions;
});
- },
-
- itemHtml(bookmark) {
- return this.attach("quick-access-item", {
- icon: ICON,
- href: postUrl(bookmark.slug, bookmark.topic_id, bookmark.post_number),
- content: bookmark.title,
- username: bookmark.username
- });
}
});
diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss
index bd4f95a01ea..2ac61a44871 100644
--- a/app/assets/stylesheets/common/base/_topic-list.scss
+++ b/app/assets/stylesheets/common/base/_topic-list.scss
@@ -54,6 +54,14 @@
}
}
+.topic-list-item {
+ .post-excerpt {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ font-size: $font-down-2;
+ }
+}
+
.topic-list-main-link {
font-size: $font-up-1;
a.title {
diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss
index adb1631a9b3..e2fd9158a81 100644
--- a/app/assets/stylesheets/common/base/tagging.scss
+++ b/app/assets/stylesheets/common/base/tagging.scss
@@ -153,10 +153,12 @@ $tag-color: $primary-medium;
display: inline-block;
}
-.topic-list-item .discourse-tags {
- display: inline-flex;
- font-weight: normal;
- font-size: $font-down-1;
+.topic-list-item {
+ .discourse-tags {
+ display: inline-flex;
+ font-weight: normal;
+ font-size: $font-down-1;
+ }
}
.categories-list .topic-list-latest .discourse-tags {
diff --git a/app/assets/stylesheets/common/components/bookmark-list-item.scss b/app/assets/stylesheets/common/components/bookmark-list-item.scss
new file mode 100644
index 00000000000..7de562d1d31
--- /dev/null
+++ b/app/assets/stylesheets/common/components/bookmark-list-item.scss
@@ -0,0 +1,19 @@
+.bookmark-list-item {
+ .bookmark-metadata {
+ font-size: $font-down-2;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.2em;
+
+ &-item {
+ margin-right: 0.5em;
+ display: flex;
+ align-items: center;
+ }
+
+ .d-icon {
+ margin-right: 0.2em;
+ }
+ }
+}
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 9405620286f..837f19b8281 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -10,14 +10,15 @@ class UsersController < ApplicationController
:enable_second_factor_totp, :disable_second_factor, :list_second_factors,
:update_second_factor, :create_second_factor_backup, :select_avatar,
:notification_level, :revoke_auth_token, :register_second_factor_security_key,
- :create_second_factor_security_key, :feature_topic, :clear_featured_topic
+ :create_second_factor_security_key, :feature_topic, :clear_featured_topic,
+ :bookmarks
]
skip_before_action :check_xhr, only: [
:show, :badges, :password_reset_show, :password_reset_update, :update, :account_created,
:activate_account, :perform_account_activation, :user_preferences_redirect, :avatar,
:my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login, :summary,
- :feature_topic, :clear_featured_topic
+ :feature_topic, :clear_featured_topic, :bookmarks
]
before_action :second_factor_check_confirmed_password, only: [
@@ -1378,6 +1379,20 @@ class UsersController < ApplicationController
render json: success_json
end
+ def bookmarks
+ user = fetch_user_from_params
+ bookmarks = BookmarkQuery.new(user, params).list_all
+
+ if bookmarks.empty?
+ render json: {
+ bookmarks: [],
+ no_results_help: I18n.t("user_activity.no_bookmarks.self")
+ }
+ else
+ render_serialized(bookmarks, UserBookmarkSerializer, root: 'bookmarks')
+ end
+ end
+
HONEYPOT_KEY ||= 'HONEYPOT_KEY'
CHALLENGE_KEY ||= 'CHALLENGE_KEY'
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index 780a004d6dd..a0f37d3d70e 100644
--- a/app/serializers/topic_view_serializer.rb
+++ b/app/serializers/topic_view_serializer.rb
@@ -182,7 +182,11 @@ class TopicViewSerializer < ApplicationSerializer
end
def bookmarked
- object.topic_user&.bookmarked
+ if SiteSetting.enable_bookmarks_with_reminders?
+ object.has_bookmarks?
+ else
+ object.topic_user&.bookmarked
+ end
end
def topic_timer
diff --git a/app/serializers/user_bookmark_serializer.rb b/app/serializers/user_bookmark_serializer.rb
new file mode 100644
index 00000000000..adef053d308
--- /dev/null
+++ b/app/serializers/user_bookmark_serializer.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require_relative 'post_item_excerpt'
+
+class UserBookmarkSerializer < ApplicationSerializer
+ include PostItemExcerpt
+ include TopicTagsMixin
+
+ attributes :id,
+ :created_at,
+ :topic_id,
+ :linked_post_number,
+ :post_id,
+ :name,
+ :reminder_at,
+ :title,
+ :deleted,
+ :hidden,
+ :category_id,
+ :closed,
+ :archived,
+ :archetype,
+ :highest_post_number,
+ :bumped_at,
+ :slug,
+ :username
+
+ def closed
+ object.topic_closed
+ end
+
+ def archived
+ object.topic_archived
+ end
+
+ def linked_post_number
+ object.post.post_number
+ end
+
+ def title
+ object.topic.title
+ end
+
+ def deleted
+ object.topic.deleted_at.present? || object.post.deleted_at.present?
+ end
+
+ def hidden
+ object.post.hidden
+ end
+
+ def category_id
+ object.topic.category_id
+ end
+
+ def archetype
+ object.topic.archetype
+ end
+
+ def archived
+ object.topic.archived
+ end
+
+ def closed
+ object.topic.closed
+ end
+
+ def highest_post_number
+ object.topic.highest_post_number
+ end
+
+ def bumped_at
+ object.topic.bumped_at
+ end
+
+ def raw
+ object.post.raw
+ end
+
+ def cooked
+ object.post.cooked
+ end
+
+ def slug
+ object.topic.slug
+ end
+
+ def username
+ object.post.user.username
+ end
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d8ff29639d7..4d61f5a28a7 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -307,6 +307,7 @@ en:
created: "you've bookmarked this post"
not_bookmarked: "bookmark this post"
created_with_reminder: "you've bookmarked this post with a reminder at %{date}"
+ created_with_at_desktop_reminder: "you've bookmarked this post and will be reminded next time you are at your desktop"
remove: "Remove Bookmark"
confirm_clear: "Are you sure you want to clear all your bookmarks from this topic?"
save: "Save"
@@ -2116,6 +2117,8 @@ en:
other { {BOTH, select, true{and } false {are } other{}} # new topics}
} remaining, or {CATEGORY, select, true {browse other topics in {catLink}} false {{latestLink}} other {}}"
+ bumped_at_title_MF: "{FIRST_POST}: {CREATED_AT}\n{LAST_POST}: {BUMPED_AT}"
+
browse_all_categories: Browse all categories
view_latest_topics: view latest topics
@@ -2667,6 +2670,7 @@ en:
bookmarks:
create: "Create bookmark"
+ created: "Created"
name: "Name"
name_placeholder: "Name the bookmark to help jog your memory"
set_reminder: "Set a reminder"
diff --git a/config/routes.rb b/config/routes.rb
index 38047b91eb7..7f7a457af3f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -469,6 +469,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormat.username }
+ get "#{root_path}/:username/bookmarks" => "users#bookmarks", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormat.username }
delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormat.username }
diff --git a/lib/bookmark_query.rb b/lib/bookmark_query.rb
new file mode 100644
index 00000000000..ab18ea9edb5
--- /dev/null
+++ b/lib/bookmark_query.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+##
+# Allows us to query Bookmark records for lists. Used mainly
+# in the user/activity/bookmarks page.
+
+class BookmarkQuery
+ def initialize(user, params)
+ @user = user
+ @params = params
+ end
+
+ def list_all
+ results = user_bookmarks
+ .joins('INNER JOIN topics ON topics.id = bookmarks.topic_id')
+ .joins('INNER JOIN posts ON posts.id = bookmarks.post_id')
+ .joins('INNER JOIN users ON users.id = posts.user_id')
+ .order('created_at DESC')
+
+ if @params[:limit]
+ results = results.limit(@params[:limit])
+ end
+
+ results
+ end
+
+ private
+
+ def user_bookmarks
+ Bookmark.where(user: @user).includes(:topic).includes(post: :user)
+ end
+end
diff --git a/lib/bookmark_reminder_notification_handler.rb b/lib/bookmark_reminder_notification_handler.rb
index 166c46274ea..83f0b9fb3d0 100644
--- a/lib/bookmark_reminder_notification_handler.rb
+++ b/lib/bookmark_reminder_notification_handler.rb
@@ -54,7 +54,7 @@ class BookmarkReminderNotificationHandler
def self.send_at_desktop_reminder(user:, request_user_agent:)
return if !SiteSetting.enable_bookmarks_with_reminders
- return if MobileDetection.mobile_device?(BrowserDetection.device(request_user_agent).to_s)
+ return if MobileDetection.mobile_device?(request_user_agent)
return if !user_has_pending_at_desktop_reminders?(user)
diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index 62f953b6fd5..3123e19b46e 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -349,6 +349,11 @@ class TopicView
end
end
+ def has_bookmarks?
+ return false if @user.blank?
+ @topic.bookmarks.exists?(user_id: @user.id)
+ end
+
MAX_PARTICIPANTS = 24
def post_counts_by_user
diff --git a/spec/lib/bookmark_reminder_notification_handler_spec.rb b/spec/lib/bookmark_reminder_notification_handler_spec.rb
index e64c9b1c704..a2bf4a30af5 100644
--- a/spec/lib/bookmark_reminder_notification_handler_spec.rb
+++ b/spec/lib/bookmark_reminder_notification_handler_spec.rb
@@ -10,31 +10,36 @@ RSpec.describe BookmarkReminderNotificationHandler do
before do
SiteSetting.enable_bookmarks_with_reminders = true
end
+ fab!(:reminder) do
+ Fabricate(
+ :bookmark,
+ user: user,
+ reminder_type: Bookmark.reminder_types[:at_desktop],
+ reminder_at: nil,
+ reminder_set_at: Time.zone.now
+ )
+ end
+ let(:user_agent) { "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" }
- context "when the user agent is for mobile" do
- let(:user_agent) { "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" }
- it "does not attempt to send any reminders" do
- DistributedMutex.expects(:synchronize).never
- send_reminder
- end
+ before do
+ Discourse.redis.flushall
end
- context "when the user agent is for desktop" do
- let(:user_agent) { "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" }
- fab!(:reminder) do
- Fabricate(
- :bookmark,
- user: user,
- reminder_type: Bookmark.reminder_types[:at_desktop],
- reminder_at: nil,
- reminder_set_at: Time.zone.now
- )
+ context "when there are pending bookmark at desktop reminders" do
+ before do
+ described_class.cache_pending_at_desktop_reminder(user)
end
- context "when there are pending bookmark at desktop reminders" do
- before do
- described_class.cache_pending_at_desktop_reminder(user)
+ context "when the user agent is for mobile" do
+ let(:user_agent) { "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" }
+ it "does not attempt to send any reminders" do
+ DistributedMutex.expects(:synchronize).never
+ send_reminder
end
+ end
+
+ context "when the user agent is for desktop" do
+ let(:user_agent) { "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36" }
it "deletes the key in redis" do
send_reminder
@@ -49,23 +54,23 @@ RSpec.describe BookmarkReminderNotificationHandler do
expect(reminder.reload.reminder_set_at).to eq(nil)
end
end
+ end
- context "when there are no pending bookmark at desktop reminders" do
- it "does nothing" do
- DistributedMutex.expects(:synchronize).never
- send_reminder
- end
+ context "when there are no pending bookmark at desktop reminders" do
+ it "does nothing" do
+ DistributedMutex.expects(:synchronize).never
+ send_reminder
+ end
+ end
+
+ context "when enable bookmarks with reminders is disabled" do
+ before do
+ SiteSetting.enable_bookmarks_with_reminders = false
end
- context "when enable bookmarks with reminders is disabled" do
- before do
- SiteSetting.enable_bookmarks_with_reminders = false
- end
-
- it "does nothing" do
- BrowserDetection.expects(:device).never
- send_reminder
- end
+ it "does nothing" do
+ BrowserDetection.expects(:device).never
+ send_reminder
end
end