FEATURE: Allow custom date + time for bookmark reminders (#9185)
A custom date and time can now be selected for a bookmark reminder The reminder will not happen at the exact time but rather at the next 5 minute interval of the bookmark reminder schedule. This PR also fixes issues with bulk deleting topic bookmarks.
This commit is contained in:
parent
793f39139a
commit
849631188f
|
@ -1,4 +1,5 @@
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
@ -25,6 +26,8 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
closeWithoutSaving: false,
|
closeWithoutSaving: false,
|
||||||
isSavingBookmarkManually: false,
|
isSavingBookmarkManually: false,
|
||||||
onCloseWithoutSaving: null,
|
onCloseWithoutSaving: null,
|
||||||
|
customReminderDate: null,
|
||||||
|
customReminderTime: null,
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
|
@ -32,7 +35,9 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
name: null,
|
name: null,
|
||||||
selectedReminderType: null,
|
selectedReminderType: null,
|
||||||
closeWithoutSaving: false,
|
closeWithoutSaving: false,
|
||||||
isSavingBookmarkManually: false
|
isSavingBookmarkManually: false,
|
||||||
|
customReminderDate: null,
|
||||||
|
customReminderTime: null
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -40,7 +45,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
// clicks the save or cancel button to mimic browser behaviour
|
// clicks the save or cancel button to mimic browser behaviour
|
||||||
onClose() {
|
onClose() {
|
||||||
if (!this.closeWithoutSaving && !this.isSavingBookmarkManually) {
|
if (!this.closeWithoutSaving && !this.isSavingBookmarkManually) {
|
||||||
this.saveBookmark();
|
this.saveBookmark().catch(e => this.handleSaveError(e));
|
||||||
}
|
}
|
||||||
if (this.onCloseWithoutSaving && this.closeWithoutSaving) {
|
if (this.onCloseWithoutSaving && this.closeWithoutSaving) {
|
||||||
this.onCloseWithoutSaving();
|
this.onCloseWithoutSaving();
|
||||||
|
@ -50,6 +55,11 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
usingMobileDevice: reads("site.mobileView"),
|
usingMobileDevice: reads("site.mobileView"),
|
||||||
showBookmarkReminderControls: true,
|
showBookmarkReminderControls: true,
|
||||||
|
|
||||||
|
@discourseComputed("selectedReminderType")
|
||||||
|
customDateTimeSelected(selectedReminderType) {
|
||||||
|
return selectedReminderType === REMINDER_TYPES.CUSTOM;
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed()
|
@discourseComputed()
|
||||||
reminderTypes: () => {
|
reminderTypes: () => {
|
||||||
return REMINDER_TYPES;
|
return REMINDER_TYPES;
|
||||||
|
@ -113,6 +123,11 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
saveBookmark() {
|
saveBookmark() {
|
||||||
const reminderAt = this.reminderAt();
|
const reminderAt = this.reminderAt();
|
||||||
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
|
const reminderAtISO = reminderAt ? reminderAt.toISOString() : null;
|
||||||
|
|
||||||
|
if (!reminderAt) {
|
||||||
|
return Promise.reject(I18n.t("bookmarks.invalid_custom_datetime"));
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
reminder_type: this.selectedReminderType,
|
reminder_type: this.selectedReminderType,
|
||||||
reminder_at: reminderAtISO,
|
reminder_at: reminderAtISO,
|
||||||
|
@ -134,8 +149,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
|
|
||||||
switch (this.selectedReminderType) {
|
switch (this.selectedReminderType) {
|
||||||
case REMINDER_TYPES.AT_DESKTOP:
|
case REMINDER_TYPES.AT_DESKTOP:
|
||||||
// TODO: Implement at desktop bookmark reminder functionality
|
return null;
|
||||||
return "";
|
|
||||||
case REMINDER_TYPES.LATER_TODAY:
|
case REMINDER_TYPES.LATER_TODAY:
|
||||||
return this.laterToday();
|
return this.laterToday();
|
||||||
case REMINDER_TYPES.NEXT_BUSINESS_DAY:
|
case REMINDER_TYPES.NEXT_BUSINESS_DAY:
|
||||||
|
@ -147,8 +161,18 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
case REMINDER_TYPES.NEXT_MONTH:
|
case REMINDER_TYPES.NEXT_MONTH:
|
||||||
return this.nextMonth();
|
return this.nextMonth();
|
||||||
case REMINDER_TYPES.CUSTOM:
|
case REMINDER_TYPES.CUSTOM:
|
||||||
// TODO: Implement custom bookmark reminder times
|
const customDateTime = moment.tz(
|
||||||
return "";
|
this.customReminderDate + " " + this.customReminderTime,
|
||||||
|
this.userTimezone()
|
||||||
|
);
|
||||||
|
if (!customDateTime.isValid()) {
|
||||||
|
this.setProperties({
|
||||||
|
customReminderTime: null,
|
||||||
|
customReminderDate: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return customDateTime;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -200,15 +224,21 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
: later.add(30, "minutes").startOf("hour");
|
: later.add(30, "minutes").startOf("hour");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleSaveError(e) {
|
||||||
|
this.isSavingBookmarkManually = false;
|
||||||
|
if (typeof e === "string") {
|
||||||
|
bootbox.alert(e);
|
||||||
|
} else {
|
||||||
|
popupAjaxError(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
saveAndClose() {
|
saveAndClose() {
|
||||||
this.isSavingBookmarkManually = true;
|
this.isSavingBookmarkManually = true;
|
||||||
this.saveBookmark()
|
this.saveBookmark()
|
||||||
.then(() => this.send("closeModal"))
|
.then(() => this.send("closeModal"))
|
||||||
.catch(e => {
|
.catch(e => this.handleSaveError(e));
|
||||||
this.isSavingBookmarkManually = false;
|
|
||||||
popupAjaxError(e);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
closeWithoutSavingBookmark() {
|
closeWithoutSavingBookmark() {
|
||||||
|
|
|
@ -35,8 +35,20 @@
|
||||||
{{tap-tile icon="far-sun" text=tomorrowFormatted tileId=reminderTypes.TOMORROW activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
{{tap-tile icon="far-sun" text=tomorrowFormatted tileId=reminderTypes.TOMORROW activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
||||||
{{tap-tile icon="far-clock" text=nextWeekFormatted tileId=reminderTypes.NEXT_WEEK activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
{{tap-tile icon="far-clock" text=nextWeekFormatted tileId=reminderTypes.NEXT_WEEK activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
||||||
{{tap-tile icon="far-calendar-plus" text=nextMonthFormatted tileId=reminderTypes.NEXT_MONTH activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
{{tap-tile icon="far-calendar-plus" text=nextMonthFormatted tileId=reminderTypes.NEXT_MONTH activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
||||||
<!-- {{tap-tile icon="calendar-alt" text=(I18n "bookmarks.reminders.custom") tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}} -->
|
{{tap-tile icon="calendar-alt" text=(I18n "bookmarks.reminders.custom") tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onChange=(action "selectReminderType")}}
|
||||||
{{/tap-tile-grid}}
|
{{/tap-tile-grid}}
|
||||||
|
{{#if customDateTimeSelected}}
|
||||||
|
<div class="control-group">
|
||||||
|
{{d-icon "calendar-alt"}}
|
||||||
|
{{date-picker-future
|
||||||
|
value=customReminderDate
|
||||||
|
onSelect=(action (mut customReminderDate))
|
||||||
|
}}
|
||||||
|
{{d-icon "far-clock"}}
|
||||||
|
{{input placeholder="--:--" type="time" value=customReminderTime}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="alert alert-info">{{i18n "bookmarks.no_timezone" basePath=basePath }}</div>
|
<div class="alert alert-info">{{i18n "bookmarks.no_timezone" basePath=basePath }}</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -512,9 +512,13 @@ class PostsController < ApplicationController
|
||||||
params.require(:post_id)
|
params.require(:post_id)
|
||||||
|
|
||||||
existing_bookmark = Bookmark.find_by(post_id: params[:post_id], user_id: current_user.id)
|
existing_bookmark = Bookmark.find_by(post_id: params[:post_id], user_id: current_user.id)
|
||||||
existing_bookmark.destroy if existing_bookmark.present?
|
topic_bookmarked = false
|
||||||
|
|
||||||
|
if existing_bookmark.present?
|
||||||
topic_bookmarked = Bookmark.exists?(topic_id: existing_bookmark.topic_id, user_id: current_user.id)
|
topic_bookmarked = Bookmark.exists?(topic_id: existing_bookmark.topic_id, user_id: current_user.id)
|
||||||
|
existing_bookmark.destroy
|
||||||
|
end
|
||||||
|
|
||||||
render json: success_json.merge(topic_bookmarked: topic_bookmarked)
|
render json: success_json.merge(topic_bookmarked: topic_bookmarked)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -311,6 +311,7 @@ en:
|
||||||
confirm_clear: "Are you sure you want to clear all your bookmarks from this topic?"
|
confirm_clear: "Are you sure you want to clear all your bookmarks from this topic?"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
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."
|
||||||
reminders:
|
reminders:
|
||||||
at_desktop: "Next time I'm at my desktop"
|
at_desktop: "Next time I'm at my desktop"
|
||||||
later_today: "Later today <br/>{{date}}"
|
later_today: "Later today <br/>{{date}}"
|
||||||
|
|
|
@ -43,7 +43,7 @@ class BookmarkManager
|
||||||
|
|
||||||
Bookmark.transaction do
|
Bookmark.transaction do
|
||||||
topic_bookmarks.each do |bookmark|
|
topic_bookmarks.each do |bookmark|
|
||||||
raise Discourse::InvalidAccess.new if !Guardian.new(user).can_delete?(bookmark)
|
raise Discourse::InvalidAccess.new if !Guardian.new(@user).can_delete?(bookmark)
|
||||||
bookmark.destroy
|
bookmark.destroy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -134,8 +134,8 @@ RSpec.describe BookmarkManager do
|
||||||
|
|
||||||
describe ".destroy_for_topic" do
|
describe ".destroy_for_topic" do
|
||||||
let!(:topic) { Fabricate(:topic) }
|
let!(:topic) { Fabricate(:topic) }
|
||||||
let(:bookmark1) { Fabricate(:bookmark, topic: topic, post: Fabricate(:post, topic: topic), user: user) }
|
let!(:bookmark1) { Fabricate(:bookmark, topic: topic, post: Fabricate(:post, topic: topic), user: user) }
|
||||||
let(:bookmark2) { Fabricate(:bookmark, topic: topic, post: Fabricate(:post, topic: topic), user: user) }
|
let!(:bookmark2) { Fabricate(:bookmark, topic: topic, post: Fabricate(:post, topic: topic), user: user) }
|
||||||
|
|
||||||
it "destroys all bookmarks for the topic for the specified user" do
|
it "destroys all bookmarks for the topic for the specified user" do
|
||||||
subject.destroy_for_topic(topic)
|
subject.destroy_for_topic(topic)
|
||||||
|
|
|
@ -457,6 +457,31 @@ describe PostsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#destroy_bookmark" do
|
||||||
|
fab!(:post) { Fabricate(:post) }
|
||||||
|
fab!(:bookmark) { Fabricate(:bookmark, user: user, post: post, topic: post.topic) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes the bookmark" do
|
||||||
|
bookmark_id = bookmark.id
|
||||||
|
delete "/posts/#{post.id}/bookmark.json"
|
||||||
|
expect(Bookmark.find_by(id: bookmark_id)).to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the user still has bookmarks in the topic" do
|
||||||
|
before do
|
||||||
|
Fabricate(:bookmark, user: user, post: Fabricate(:post, topic: post.topic), topic: topic)
|
||||||
|
end
|
||||||
|
it "marks topic_bookmaked as true" do
|
||||||
|
delete "/posts/#{post.id}/bookmark.json"
|
||||||
|
expect(JSON.parse(response.body)['topic_bookmarked']).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#bookmark' do
|
describe '#bookmark' do
|
||||||
include_examples 'action requires login', :put, "/posts/2/bookmark.json"
|
include_examples 'action requires login', :put, "/posts/2/bookmark.json"
|
||||||
let!(:post) { post_by_user }
|
let!(:post) { post_by_user }
|
||||||
|
|
Loading…
Reference in New Issue