-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
b73a133
commit 6261339
Showing
29 changed files
with
903 additions
and
15 deletions.
There are no files selected for viewing
1 change: 1 addition & 0 deletions
1
app/assets/javascripts/admin/templates/components/tap-tile-grid.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{{ yield (hash activeTile=this.activeTile) }} |
2 changes: 2 additions & 0 deletions
2
app/assets/javascripts/admin/templates/components/tap-tile.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
{{d-icon icon}} | ||
{{text}} |
6 changes: 6 additions & 0 deletions
6
app/assets/javascripts/discourse/components/tap-tile-grid.js.es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
12
app/assets/javascripts/discourse/components/tap-tile.js.es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
217
app/assets/javascripts/discourse/controllers/bookmark.js.es6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
app/assets/javascripts/discourse/templates/modal/bookmark.hbs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
6261339
There was a problem hiding this comment.
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