Skip to content

Commit

Permalink
Improving bookmarks part 1 (#8466)
Browse files Browse the repository at this point in the history
Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated!

This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality:

* We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them.
* Posts now have a new button in their actions menu that has the icon of an actual book
* Clicking the button opens the new bookmark modal.
* Both name and the reminder type are optional.
* If you close the modal without doing anything, the bookmark is saved with no reminder.
* If you click the Cancel button, no bookmark is saved at all.
* All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you).
* If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark.
* A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked.

This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
  • Loading branch information
martin-brennan committed Dec 11, 2019
1 parent b73a133 commit 6261339
Show file tree
Hide file tree
Showing 29 changed files with 903 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ yield (hash activeTile=this.activeTile) }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{d-icon icon}}
{{text}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Component from "@ember/component";

export default Component.extend({
classNames: ["tap-tile-grid"],
activeTile: null
});
12 changes: 12 additions & 0 deletions app/assets/javascripts/discourse/components/tap-tile.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Component from "@ember/component";
import { propertyEqual } from "discourse/lib/computed";

export default Component.extend({
classNames: ["tap-tile"],
classNameBindings: ["active"],
click() {
this.onSelect(this.tileId);
},

active: propertyEqual("activeTile", "tileId")
});
217 changes: 217 additions & 0 deletions app/assets/javascripts/discourse/controllers/bookmark.js.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { htmlSafe } from "@ember/template";
import { ajax } from "discourse/lib/ajax";
import { reads } from "@ember/object/computed";

const START_OF_DAY_HOUR = 8;
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"
};

export default Controller.extend(ModalFunctionality, {
loading: false,
errorMessage: null,
name: null,
selectedReminderType: null,
closeWithoutSaving: false,
isSavingBookmarkManually: false,
onCloseWithoutSaving: null,

onShow() {
this.setProperties({
errorMessage: null,
name: null,
selectedReminderType: null,
closeWithoutSaving: false,
isSavingBookmarkManually: false
});
},

// we always want to save the bookmark unless the user specifically
// clicks the save or cancel button to mimic browser behaviour
onClose() {
if (!this.closeWithoutSaving && !this.isSavingBookmarkManually) {
this.saveBookmark();
}
if (this.onCloseWithoutSaving && this.closeWithoutSaving) {
this.onCloseWithoutSaving();
}
},

usingMobileDevice: reads("site.mobileView"),

@discourseComputed()
reminderTypes: () => {
return REMINDER_TYPES;
},

@discourseComputed()
showLaterToday() {
return !this.laterToday().isSame(this.tomorrow(), "date");
},

@discourseComputed()
laterTodayFormatted() {
return htmlSafe(
I18n.t("bookmarks.reminders.later_today", {
date: this.laterToday().format(I18n.t("dates.time"))
})
);
},

@discourseComputed()
tomorrowFormatted() {
return htmlSafe(
I18n.t("bookmarks.reminders.tomorrow", {
date: this.tomorrow().format(I18n.t("dates.time_short_day"))
})
);
},

@discourseComputed()
nextBusinessDayFormatted() {
return htmlSafe(
I18n.t("bookmarks.reminders.next_business_day", {
date: this.nextBusinessDay().format(I18n.t("dates.time_short_day"))
})
);
},

@discourseComputed()
nextWeekFormatted() {
return htmlSafe(
I18n.t("bookmarks.reminders.next_week", {
date: this.nextWeek().format(I18n.t("dates.month_day_time"))
})
);
},

@discourseComputed()
nextMonthFormatted() {
return htmlSafe(
I18n.t("bookmarks.reminders.next_month", {
date: this.nextMonth().format(I18n.t("dates.month_day_time"))
})
);
},

@discourseComputed()
userHasTimezoneSet() {
return !_.isEmpty(this.userTimezone());
},

saveBookmark() {
const reminderAt = this.reminderAt();
const data = {
reminder_type: this.selectedReminderType,
reminder_at: reminderAt ? reminderAt.toISOString() : null,
name: this.name,
post_id: this.model.postId
};

return ajax("/bookmarks", { type: "POST", data });
},

reminderAt() {
if (!this.selectedReminderType) {
return;
}

switch (this.selectedReminderType) {
case REMINDER_TYPES.AT_DESKTOP:
// TODO: Implement at desktop bookmark reminder functionality
return "";
case REMINDER_TYPES.LATER_TODAY:
return this.laterToday();
case REMINDER_TYPES.NEXT_BUSINESS_DAY:
return this.nextBusinessDay();
case REMINDER_TYPES.TOMORROW:
return this.tomorrow();
case REMINDER_TYPES.NEXT_WEEK:
return this.nextWeek();
case REMINDER_TYPES.NEXT_MONTH:
return this.nextMonth();
case REMINDER_TYPES.CUSTOM:
// TODO: Implement custom bookmark reminder times
return "";
}
},

nextWeek() {
return this.startOfDay(this.now().add(7, "days"));
},

nextMonth() {
return this.startOfDay(this.now().add(1, "month"));
},

nextBusinessDay() {
const currentDay = this.now().isoWeekday(); // 1=Mon, 7=Sun
let next = null;

// friday
if (currentDay === 5) {
next = this.now().add(3, "days");
// saturday
} else if (currentDay === 6) {
next = this.now().add(2, "days");
} else {
next = this.now().add(1, "day");
}

return this.startOfDay(next);
},

tomorrow() {
return this.startOfDay(this.now().add(1, "day"));
},

startOfDay(momentDate) {
return momentDate.hour(START_OF_DAY_HOUR).startOf("hour");
},

userTimezone() {
return this.currentUser.timezone;
},

now() {
return moment.tz(this.userTimezone());
},

laterToday() {
let later = this.now().add(3, "hours");
return later.minutes() < 30
? later.minutes(30)
: later.add(30, "minutes").startOf("hour");
},

actions: {
saveAndClose() {
this.isSavingBookmarkManually = true;
this.saveBookmark()
.then(() => this.send("closeModal"))
.catch(e => {
this.isSavingBookmarkManually = false;
popupAjaxError(e);
});
},

closeWithoutSavingBookmark() {
this.closeWithoutSaving = true;
this.send("closeModal");
},

selectReminderType(type) {
this.set("selectedReminderType", type);
}
}
});
10 changes: 10 additions & 0 deletions app/assets/javascripts/discourse/controllers/topic.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,16 @@ export default Controller.extend(bufferedProperty("model"), {
}
},

toggleBookmarkWithReminder(post) {
if (!this.currentUser) {
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
} else if (post) {
return post.toggleBookmarkWithReminder();
} else {
return this.model.toggleBookmarkWithReminder();
}
},

toggleFeaturedOnProfile() {
if (!this.currentUser) return;

Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/discourse/lib/transform-post.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function transformBasicPost(post) {
username: post.username,
avatar_template: post.avatar_template,
bookmarked: post.bookmarked,
bookmarkedWithReminder: post.bookmarked_with_reminder,
bookmarkReminderAt: post.bookmark_reminder_at,
yours: post.yours,
shareUrl: post.get("shareUrl"),
staff: post.staff,
Expand Down
33 changes: 33 additions & 0 deletions app/assets/javascripts/discourse/models/post.js.es6
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Composer from "discourse/models/composer";
import { Promise } from "rsvp";
import Site from "discourse/models/site";
import User from "discourse/models/user";
import showModal from "discourse/lib/show-modal";

const Post = RestModel.extend({
// TODO: Remove this once one instantiate all `Discourse.Post` models via the store.
Expand Down Expand Up @@ -336,6 +337,32 @@ const Post = RestModel.extend({
});
},

toggleBookmarkWithReminder() {
this.toggleProperty("bookmarkedWithReminder");
if (this.bookmarkedWithReminder) {
let controller = showModal("bookmark", {
model: {
postId: this.id
},
title: "post.bookmarks.create",
modalClass: "bookmark-with-reminder"
});
controller.setProperties({
onCloseWithoutSaving: () => {
this.toggleProperty("bookmarkedWithReminder");
this.appEvents.trigger("post-stream:refresh", { id: this.id });
}
});
} else {
return Post.destroyBookmark(this.id)
.then(() => this.appEvents.trigger("page:bookmark-post-toggled", this))
.catch(error => {
this.toggleProperty("bookmarkedWithReminder");
throw new Error(error);
});
}
},

updateActionsSummary(json) {
if (json && json.id === this.id) {
json = Post.munge(json);
Expand Down Expand Up @@ -385,6 +412,12 @@ Post.reopenClass({
});
},

destroyBookmark(postId) {
return ajax(`/posts/${postId}/bookmark`, {
type: "DELETE"
});
},

deleteMany(post_ids, { agreeWithFirstReplyFlag = true } = {}) {
return ajax("/posts/destroy_many", {
type: "DELETE",
Expand Down
49 changes: 49 additions & 0 deletions app/assets/javascripts/discourse/templates/modal/bookmark.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{{#d-modal-body}}
{{#conditional-loading-spinner condition=loading}}
{{#if errorMessage}}
<div class="control-group">
<div class="controls">
<div class='alert alert-error'>{{errorMessage}}</div>
</div>
</div>
{{/if}}

<div class="control-group">
<label class="control-label" for="name">
{{i18n 'post.bookmarks.name'}}
</label>

{{input value=name name="name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}}
</div>

<div class="control-group">
<label class="control-label" for="set_reminder">
{{i18n 'post.bookmarks.set_reminder'}}
</label>

{{#if userHasTimezoneSet}}
{{#tap-tile-grid activeTile=selectedReminderType as |grid|}}
{{#if usingMobileDevice}}
<!-- {{tap-tile icon="desktop" text=(i18n "bookmarks.reminders.at_desktop") tileId=reminderTypes.AT_DESKTOP activeTile=grid.activeTile onSelect=(action "selectReminderType")}} -->
{{/if}}

{{#if showLaterToday}}
{{tap-tile icon="far-moon" text=laterTodayFormatted tileId=reminderTypes.LATER_TODAY activeTile=grid.activeTile onSelect=(action "selectReminderType")}}
{{/if}}
{{tap-tile icon="briefcase" text=nextBusinessDayFormatted tileId=reminderTypes.NEXT_BUSINESS_DAY activeTile=grid.activeTile onSelect=(action "selectReminderType")}}
{{tap-tile icon="far-sun" text=tomorrowFormatted tileId=reminderTypes.TOMORROW activeTile=grid.activeTile onSelect=(action "selectReminderType")}}
{{tap-tile icon="far-clock" text=nextWeekFormatted tileId=reminderTypes.NEXT_WEEK activeTile=grid.activeTile onSelect=(action "selectReminderType")}}
{{tap-tile icon="far-calendar-plus" text=nextMonthFormatted tileId=reminderTypes.NEXT_MONTH activeTile=grid.activeTile onSelect=(action "selectReminderType")}}
<!-- {{tap-tile icon="calendar-alt" text=(I18n "bookmarks.reminders.custom") tileId=reminderTypes.CUSTOM activeTile=grid.activeTile onSelect=(action "selectReminderType")}} -->
{{/tap-tile-grid}}
{{else}}
<div class="alert alert-info">{{{i18n "bookmarks.no_timezone" basePath=basePath }}}</div>
{{/if}}
</div>

<div class="control-group">
{{d-button label="bookmarks.save" class="btn-primary" action=(action "saveAndClose")}}
{{d-modal-cancel close=(action "closeWithoutSavingBookmark")}}
</div>
{{/conditional-loading-spinner}}
{{/d-modal-body}}
1 change: 1 addition & 0 deletions app/assets/javascripts/discourse/templates/topic.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
expandHidden=(action "expandHidden")
newTopicAction=(action "replyAsNewTopic")
toggleBookmark=(action "toggleBookmark")
toggleBookmarkWithReminder=(action "toggleBookmarkWithReminder")
togglePostType=(action "togglePostType")
rebakePost=(action "rebakePost")
changePostOwner=(action "changePostOwner")
Expand Down
Loading

1 comment on commit 6261339

@discoursebot
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/suspicious-time-date-formats/136927/1

Please sign in to comment.