diff --git a/app/assets/javascripts/discourse/controllers/bookmark.js b/app/assets/javascripts/discourse/controllers/bookmark.js index 88ed8e00f20..39baaa07b26 100644 --- a/app/assets/javascripts/discourse/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/controllers/bookmark.js @@ -7,6 +7,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { ajax } from "discourse/lib/ajax"; import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; +import { REMINDER_TYPES } from "discourse/lib/bookmark"; // global shortcuts that interfere with these modal shortcuts, they are rebound when the // modal is closed @@ -20,19 +21,6 @@ const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "d", "t"]; const START_OF_DAY_HOUR = 8; const LATER_TODAY_CUTOFF_HOUR = 17; const LATER_TODAY_MAX_HOUR = 18; -const REMINDER_TYPES = { - AT_DESKTOP: "at_desktop", - LATER_TODAY: "later_today", - NEXT_BUSINESS_DAY: "next_business_day", - TOMORROW: "tomorrow", - NEXT_WEEK: "next_week", - NEXT_MONTH: "next_month", - CUSTOM: "custom", - LAST_CUSTOM: "last_custom", - NONE: "none", - START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week", - LATER_THIS_WEEK: "later_this_week" -}; const BOOKMARK_BINDINGS = { enter: { handler: "saveAndClose" }, diff --git a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js index 3cd0827a6f1..a75949ba23c 100644 --- a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js +++ b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js @@ -1,10 +1,12 @@ import showModal from "discourse/lib/show-modal"; import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button"; +import { formattedReminderTime } from "discourse/lib/bookmark"; export default { name: "topic-footer-buttons", - initialize() { + initialize(container) { + const currentUser = container.lookup("current-user:main"); registerTopicFooterButton({ id: "share-and-invite", icon: "link", @@ -84,7 +86,12 @@ export default { registerTopicFooterButton({ dependentKeys: ["topic.bookmarked", "topic.isPrivateMessage"], id: "bookmark", - icon: "bookmark", + icon() { + if (this.get("topic.bookmark_reminder_at")) { + return "discourse-bookmark-clock"; + } + return "bookmark"; + }, priority: 1000, classNames() { const bookmarked = this.get("topic.bookmarked"); @@ -94,11 +101,21 @@ export default { const bookmarked = this.get("topic.bookmarked"); return bookmarked ? "bookmarked.clear_bookmarks" : "bookmarked.title"; }, - title() { + translatedTitle() { const bookmarked = this.get("topic.bookmarked"); - return bookmarked - ? "bookmarked.help.unbookmark" - : "bookmarked.help.bookmark"; + const bookmark_reminder_at = this.get("topic.bookmark_reminder_at"); + if (bookmarked) { + if (bookmark_reminder_at) { + return I18n.t("bookmarked.help.unbookmark_with_reminder", { + reminder_at: formattedReminderTime( + bookmark_reminder_at, + currentUser.resolvedTimezone() + ) + }); + } + return I18n.t("bookmarked.help.unbookmark"); + } + return I18n.t("bookmarked.help.bookmark"); }, action: "toggleBookmark", dropdown() { diff --git a/app/assets/javascripts/discourse/lib/bookmark.js b/app/assets/javascripts/discourse/lib/bookmark.js new file mode 100644 index 00000000000..8874034e9a2 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/bookmark.js @@ -0,0 +1,31 @@ +export function formattedReminderTime(reminderAt, timezone) { + let reminderAtDate = moment.tz(reminderAt, timezone); + let formatted = reminderAtDate.format(I18n.t("dates.time")); + let now = moment.tz(timezone); + let tomorrow = moment(now).add(1, "day"); + + if (reminderAtDate.isSame(tomorrow, "date")) { + return I18n.t("bookmarks.reminders.tomorrow_with_time", { + time: formatted + }); + } else if (reminderAtDate.isSame(now, "date")) { + return I18n.t("bookmarks.reminders.today_with_time", { time: formatted }); + } + return I18n.t("bookmarks.reminders.at_time", { + date_time: reminderAtDate.format(I18n.t("dates.long_with_year")) + }); +} + +export const REMINDER_TYPES = { + AT_DESKTOP: "at_desktop", + LATER_TODAY: "later_today", + NEXT_BUSINESS_DAY: "next_business_day", + TOMORROW: "tomorrow", + NEXT_WEEK: "next_week", + NEXT_MONTH: "next_month", + CUSTOM: "custom", + LAST_CUSTOM: "last_custom", + NONE: "none", + START_OF_NEXT_BUSINESS_WEEK: "start_of_next_business_week", + LATER_THIS_WEEK: "later_this_week" +}; diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index c783fe88279..ce951d6edcf 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -401,6 +401,7 @@ const Topic = RestModel.extend({ if (firstPost) { firstPost.set("bookmarked", true); if (this.siteSettings.enable_bookmarks_with_reminders) { + this.set("bookmark_reminder_at", firstPost.bookmark_reminder_at); firstPost.set("bookmarked_with_reminder", true); } return [firstPost.id]; @@ -440,7 +441,7 @@ const Topic = RestModel.extend({ if (this.siteSettings.enable_bookmarks_with_reminders) { return firstPost.toggleBookmarkWithReminder().then(response => { this.set("bookmarking", false); - if (response.closedWithoutSaving) { + if (response && response.closedWithoutSaving) { this.set("bookmarked", false); } else { return this.afterTopicBookmarked(firstPost); @@ -459,6 +460,7 @@ const Topic = RestModel.extend({ return ajax(`/t/${this.id}/remove_bookmarks`, { type: "PUT" }) .then(() => { this.toggleProperty("bookmarked"); + this.set("bookmark_reminder_at", null); if (posts) { const updated = []; posts.forEach(post => { @@ -474,6 +476,7 @@ const Topic = RestModel.extend({ updated.push(post.id); } }); + firstPost.set("bookmarked_with_reminder", false); return updated; } }) diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js b/app/assets/javascripts/discourse/widgets/post-menu.js index c21d60f6989..c8374058ee4 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js +++ b/app/assets/javascripts/discourse/widgets/post-menu.js @@ -5,6 +5,7 @@ import { h } from "virtual-dom"; import showModal from "discourse/lib/show-modal"; import { Promise } from "rsvp"; import ENV from "discourse-common/config/environment"; +import { formattedReminderTime } from "discourse/lib/bookmark"; const LIKE_ACTION = 2; const VIBRATE_DURATION = 5; @@ -314,12 +315,13 @@ registerButton("bookmarkWithReminder", (attrs, state, siteSettings) => { classNames.push("bookmarked"); if (attrs.bookmarkReminderAt) { - let reminderAtDate = moment(attrs.bookmarkReminderAt).tz( + let formattedReminder = formattedReminderTime( + attrs.bookmarkReminderAt, Discourse.currentUser.resolvedTimezone() ); title = "bookmarks.created_with_reminder"; titleOptions = { - date: reminderAtDate.format(I18n.t("dates.long_with_year")) + date: formattedReminder }; } else if (attrs.bookmarkReminderType === "at_desktop") { title = "bookmarks.created_with_at_desktop_reminder"; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 1b60137985e..2406f4e79b4 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -454,7 +454,8 @@ nav.post-controls { color: $tertiary; } } - .bookmark.bookmarked .d-icon-bookmark { + .bookmark.bookmarked .d-icon-bookmark, + .bookmark.bookmarked .d-icon-discourse-bookmark-clock { color: $tertiary; } .feature-on-profile.featured-on-profile .d-icon-id-card { diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 1d37052b882..e6c008c62dd 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -237,7 +237,8 @@ a.reply-to-tab { #topic-footer-buttons { @include clearfix; padding: 20px 0 0 0; - .d-icon-bookmark.bookmarked { + .d-icon-bookmark.bookmarked, + .d-icon-discourse-bookmark-clock.bookmarked { color: $tertiary; } .combobox { diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 1aec5f442d5..4bace8d9edc 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -61,6 +61,7 @@ class TopicViewSerializer < ApplicationSerializer :is_warning, :chunk_size, :bookmarked, + :bookmark_reminder_at, :message_archived, :topic_timer, :private_topic_timer, @@ -192,6 +193,14 @@ class TopicViewSerializer < ApplicationSerializer end end + def include_bookmark_reminder_at? + SiteSetting.enable_bookmarks_with_reminders? && bookmarked + end + + def bookmark_reminder_at + object.first_post_bookmark_reminder_at + end + def topic_timer TopicTimerSerializer.new(object.topic.public_topic_timer, root: false) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3f25460ee59..64492a2bc7f 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -302,11 +302,13 @@ en: help: bookmark: "Click to bookmark the first post on this topic" unbookmark: "Click to remove all bookmarks in this topic" + unbookmark_with_reminder: "Click to remove all bookmarks and reminders in this topic. You have a reminder set %{reminder_at} for this topic." + bookmarks: 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_reminder: "you've bookmarked this post with a reminder %{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?" @@ -326,6 +328,9 @@ en: custom: "Custom date and time" last_custom: "Last" none: "No reminder needed" + today_with_time: "today at %{time}" + tomorrow_with_time: "tomorrow at %{time}" + at_time: "at %{date_time}" drafts: resume: "Resume" diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 82ae26c573c..42405f8e337 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -354,6 +354,10 @@ class TopicView @topic.bookmarks.exists?(user_id: @user.id) end + def first_post_bookmark_reminder_at + @topic.first_post.bookmarks.where(user: @user).pluck_first(:reminder_at) + end + MAX_PARTICIPANTS = 24 def post_counts_by_user diff --git a/test/javascripts/lib/bookmark-test.js b/test/javascripts/lib/bookmark-test.js new file mode 100644 index 00000000000..1e2530a5307 --- /dev/null +++ b/test/javascripts/lib/bookmark-test.js @@ -0,0 +1,51 @@ +import { formattedReminderTime } from "discourse/lib/bookmark"; + +QUnit.module("lib:bookmark", { + beforeEach() { + // set the current now time for all tests + let now = moment.tz("2020-04-11 08:00:00", "Australia/Brisbane"); + sandbox.useFakeTimers(now.valueOf()); + } +}); + +QUnit.test( + "formattedReminderTime works when the reminder time is tomorrow", + assert => { + let reminderAt = "2020-04-12 09:45:00"; + let reminderAtDate = moment + .tz(reminderAt, "Australia/Brisbane") + .format("H:mm a"); + assert.equal( + formattedReminderTime(reminderAt, "Australia/Brisbane"), + "tomorrow at " + reminderAtDate + ); + } +); + +QUnit.test( + "formattedReminderTime works when the reminder time is today", + assert => { + let reminderAt = "2020-04-11 09:45:00"; + let reminderAtDate = moment + .tz(reminderAt, "Australia/Brisbane") + .format("H:mm a"); + assert.equal( + formattedReminderTime(reminderAt, "Australia/Brisbane"), + "today at " + reminderAtDate + ); + } +); + +QUnit.test( + "formattedReminderTime works when the reminder time is in the future", + assert => { + let reminderAt = "2020-04-15 09:45:00"; + let reminderAtDate = moment + .tz(reminderAt, "Australia/Brisbane") + .format("H:mm a"); + assert.equal( + formattedReminderTime(reminderAt, "Australia/Brisbane"), + "at Apr 15, 2020 " + reminderAtDate + ); + } +);