mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 08:43:25 +08:00
FEATURE: MVP Bookmarks with reminders user list changes (#8999)
* This PR changes the user activity bookmarks stream to show a new list of bookmarks based on the Bookmark record. * If a bookmark has a name or reminder it will be shown as metadata above the topic title in the list * The categories, tags, topic status, and assigned show for each bookmarked post based on the post topic * Bookmarks can be deleted from the [...] menu in the list * As well as this, the list of bookmarks from the quick access panel is now drawn from the Bookmarks table for a user: * All of this new functionality is gated behind the enable_bookmarks_with_reminders site setting The /bookmarks/ route now redirects directly to /user/:username/activity/bookmarks-with-reminders * The structure of the Ember for the list of bookmarks is not ideal, this is an MVP PR so we can start testing this functionality internally. There is a little repeated code from topic.js.es6. There is an ongoing effort to start standardizing these lists that will be addressed in future PRs. * This PR also fixes issues with feature detection for at_desktop bookmark reminders
This commit is contained in:
parent
849631188f
commit
e1eb5fb9b3
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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() {
|
||||
|
|
|
@ -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 `<span dir="${titleDir}">${fancyTitle}</span>`;
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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()}`);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -18,9 +18,15 @@
|
|||
{{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
|
||||
</li>
|
||||
{{#if user.showBookmarks}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.bookmarks'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
|
||||
</li>
|
||||
{{#if bookmarksWithRemindersEnabled}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.bookmarksWithReminders'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
|
||||
</li>
|
||||
{{else}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.bookmarks'}}{{i18n 'user_action_groups.3'}}{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{plugin-outlet name="user-activity-bottom"
|
||||
connectorTagName='li'
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{{#if noContent}}
|
||||
<div class='alert alert-info'>{{noResultsHelp}}</div>
|
||||
{{else}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<table class="topic-list">
|
||||
<thead>
|
||||
<th>{{i18n "topic.title"}}</th>
|
||||
<th>{{i18n "post.bookmarks.created"}}</th>
|
||||
<th>{{i18n "activity"}}</th>
|
||||
<th> </th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each content as |bookmark|}}
|
||||
<tr class="topic-list-item bookmark-list-item">
|
||||
<td class="main-link">
|
||||
<span class="link-top-line">
|
||||
<div class="bookmark-metadata">
|
||||
{{#if bookmark.name}}
|
||||
<span class="bookmark-metadata-item">
|
||||
{{d-icon "info-circle"}}{{bookmark.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if bookmark.reminder_at}}
|
||||
<span class="bookmark-metadata-item">
|
||||
{{d-icon "far-clock"}}{{bookmark.formattedReminder}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{topic-status topic=bookmark}}
|
||||
{{topic-link bookmark}}
|
||||
</span>
|
||||
{{#if bookmark.excerpt}}
|
||||
<p class="post-excerpt">{{html-safe bookmark.excerpt}}</p>
|
||||
{{/if}}
|
||||
<div class="link-bottom-line">
|
||||
{{category-link bookmark.category}}
|
||||
{{discourse-tags bookmark mode="list" tagsForUser=tagsForUser}}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{format-date bookmark.created_at format="tiny"}}</td>
|
||||
{{raw "list/activity-column" topic=bookmark class="num" tagName="td"}}
|
||||
<td>
|
||||
{{bookmark-actions-dropdown bookmark=bookmark removeBookmark=(action "removeBookmark")}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/if}}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
91
app/serializers/user_bookmark_serializer.rb
Normal file
91
app/serializers/user_bookmark_serializer.rb
Normal file
|
@ -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
|
|
@ -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{}} <a href='{basePath}/new'># new</a> 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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
32
lib/bookmark_query.rb
Normal file
32
lib/bookmark_query.rb
Normal file
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user