diff --git a/app/assets/javascripts/discourse/components/user-stream.js.es6 b/app/assets/javascripts/discourse/components/user-stream.js.es6
index ee32cae3002..7bd7ff01439 100644
--- a/app/assets/javascripts/discourse/components/user-stream.js.es6
+++ b/app/assets/javascripts/discourse/components/user-stream.js.es6
@@ -2,6 +2,10 @@ import LoadMore from "discourse/mixins/load-more";
import ClickTrack from "discourse/lib/click-track";
import { selectedText } from "discourse/lib/utilities";
import Post from "discourse/models/post";
+import DiscourseURL from "discourse/lib/url";
+import Draft from "discourse/models/draft";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { getOwner } from "discourse-common/lib/get-owner";
export default Ember.Component.extend(LoadMore, {
loading: false,
@@ -57,6 +61,41 @@ export default Ember.Component.extend(LoadMore, {
});
},
+ resumeDraft(item) {
+ const composer = getOwner(this).lookup("controller:composer");
+ if (composer.get("model.viewOpen")) {
+ composer.close();
+ }
+ if (item.get("postUrl")) {
+ DiscourseURL.routeTo(item.get("postUrl"));
+ } else {
+ Draft.get(item.draft_key)
+ .then(d => {
+ if (d.draft) {
+ composer.open({
+ draft: d.draft,
+ draftKey: item.draft_key,
+ draftSequence: d.draft_sequence
+ });
+ }
+ })
+ .catch(error => {
+ popupAjaxError(error);
+ });
+ }
+ },
+
+ removeDraft(draft) {
+ const stream = this.get("stream");
+ Draft.clear(draft.draft_key, draft.sequence)
+ .then(() => {
+ stream.load(this.site);
+ })
+ .catch(error => {
+ popupAjaxError(error);
+ });
+ },
+
loadMore() {
if (this.get("loading")) {
return;
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index f3f26754901..bc1764c4420 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -849,7 +849,10 @@ export default Ember.Controller.extend({
if (key === "new_topic") {
this.send("clearTopicDraft");
}
- Draft.clear(key, this.get("model.draftSequence"));
+
+ Draft.clear(key, this.get("model.draftSequence")).then(() => {
+ this.appEvents.trigger("draft:destroyed", key);
+ });
}
},
diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6
index e952c7f1e43..06518682d28 100644
--- a/app/assets/javascripts/discourse/controllers/user.js.es6
+++ b/app/assets/javascripts/discourse/controllers/user.js.es6
@@ -62,6 +62,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
return viewingSelf || isAdmin;
},
+ @computed("viewingSelf", "currentUser.admin")
+ showDrafts(viewingSelf, isAdmin) {
+ return viewingSelf || isAdmin;
+ },
+
@computed("viewingSelf", "currentUser.admin")
showPrivateMessages(viewingSelf, isAdmin) {
return (
diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6
index 801b44c24c4..24f4d8fb794 100644
--- a/app/assets/javascripts/discourse/models/composer.js.es6
+++ b/app/assets/javascripts/discourse/models/composer.js.es6
@@ -12,6 +12,7 @@ export const CREATE_TOPIC = "createTopic",
EDIT_SHARED_DRAFT = "editSharedDraft",
PRIVATE_MESSAGE = "privateMessage",
NEW_PRIVATE_MESSAGE_KEY = "new_private_message",
+ NEW_TOPIC_KEY = "new_topic",
REPLY = "reply",
EDIT = "edit",
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
diff --git a/app/assets/javascripts/discourse/models/user-draft.js.es6 b/app/assets/javascripts/discourse/models/user-draft.js.es6
new file mode 100644
index 00000000000..2f8d431eb73
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user-draft.js.es6
@@ -0,0 +1,47 @@
+import RestModel from "discourse/models/rest";
+import computed from "ember-addons/ember-computed-decorators";
+import { postUrl } from "discourse/lib/utilities";
+import { userPath } from "discourse/lib/url";
+import User from "discourse/models/user";
+
+import {
+ NEW_TOPIC_KEY,
+ NEW_PRIVATE_MESSAGE_KEY
+} from "discourse/models/composer";
+
+export default RestModel.extend({
+ @computed("draft_username")
+ editableDraft(draftUsername) {
+ return draftUsername === User.currentProp("username");
+ },
+
+ @computed("username_lower")
+ userUrl(usernameLower) {
+ return userPath(usernameLower);
+ },
+
+ @computed("topic_id")
+ postUrl(topicId) {
+ if (!topicId) return;
+
+ return postUrl(
+ this.get("slug"),
+ this.get("topic_id"),
+ this.get("post_number")
+ );
+ },
+
+ @computed("draft_key", "post_number")
+ draftType(draftKey, postNumber) {
+ switch (draftKey) {
+ case NEW_TOPIC_KEY:
+ return I18n.t("drafts.new_topic");
+ case NEW_PRIVATE_MESSAGE_KEY:
+ return I18n.t("drafts.new_private_message");
+ default:
+ return postNumber
+ ? I18n.t("drafts.post_reply", { postNumber })
+ : I18n.t("drafts.topic_reply");
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6 b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6
new file mode 100644
index 00000000000..b6d82f8a33e
--- /dev/null
+++ b/app/assets/javascripts/discourse/models/user-drafts-stream.js.es6
@@ -0,0 +1,105 @@
+import { ajax } from "discourse/lib/ajax";
+import { url } from "discourse/lib/computed";
+import RestModel from "discourse/models/rest";
+import UserDraft from "discourse/models/user-draft";
+import { emojiUnescape } from "discourse/lib/text";
+import computed from "ember-addons/ember-computed-decorators";
+
+import {
+ NEW_TOPIC_KEY,
+ NEW_PRIVATE_MESSAGE_KEY
+} from "discourse/models/composer";
+
+export default RestModel.extend({
+ loaded: false,
+
+ init() {
+ this._super();
+ this.setProperties({
+ itemsLoaded: 0,
+ content: [],
+ lastLoadedUrl: null
+ });
+ },
+
+ baseUrl: url(
+ "itemsLoaded",
+ "user.username_lower",
+ "/drafts.json?offset=%@&username=%@"
+ ),
+
+ load(site) {
+ this.setProperties({
+ itemsLoaded: 0,
+ content: [],
+ lastLoadedUrl: null,
+ site: site
+ });
+ return this.findItems();
+ },
+
+ @computed("content.length", "loaded")
+ noContent(contentLength, loaded) {
+ return loaded && contentLength === 0;
+ },
+
+ remove(draft) {
+ let content = this.get("content").filter(
+ item => item.sequence !== draft.sequence
+ );
+ this.setProperties({ content, itemsLoaded: content.length });
+ },
+
+ findItems() {
+ let findUrl = this.get("baseUrl");
+
+ const lastLoadedUrl = this.get("lastLoadedUrl");
+ if (lastLoadedUrl === findUrl) {
+ return Ember.RSVP.resolve();
+ }
+
+ if (this.get("loading")) {
+ return Ember.RSVP.resolve();
+ }
+
+ this.set("loading", true);
+
+ return ajax(findUrl, { cache: "false" })
+ .then(result => {
+ if (result && result.no_results_help) {
+ this.set("noContentHelp", result.no_results_help);
+ }
+ if (result && result.drafts) {
+ const copy = Em.A();
+ result.drafts.forEach(draft => {
+ let draftData = JSON.parse(draft.data);
+ draft.post_number = draftData.postId || null;
+ if (
+ draft.draft_key === NEW_PRIVATE_MESSAGE_KEY ||
+ draft.draft_key === NEW_TOPIC_KEY
+ ) {
+ draft.title = draftData.title;
+ }
+ draft.title = emojiUnescape(
+ Handlebars.Utils.escapeExpression(draft.title)
+ );
+ if (draft.category_id) {
+ draft.category =
+ this.site.categories.findBy("id", draft.category_id) || null;
+ }
+
+ copy.pushObject(UserDraft.create(draft));
+ });
+ this.get("content").pushObjects(copy);
+ this.setProperties({
+ loaded: true,
+ itemsLoaded: this.get("itemsLoaded") + result.drafts.length
+ });
+ }
+ })
+ .finally(() => {
+ this.set("loading", false);
+ this.set("lastLoadedUrl", findUrl);
+ });
+ }
+});
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index 94f972ca27a..e3947378fba 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -13,6 +13,7 @@ import Badge from "discourse/models/badge";
import UserBadge from "discourse/models/user-badge";
import UserActionStat from "discourse/models/user-action-stat";
import UserAction from "discourse/models/user-action";
+import UserDraftsStream from "discourse/models/user-drafts-stream";
import Group from "discourse/models/group";
import { emojiUnescape } from "discourse/lib/text";
import PreloadStore from "preload-store";
@@ -47,6 +48,11 @@ const User = RestModel.extend({
return UserPostsStream.create({ user: this });
},
+ @computed()
+ userDraftsStream() {
+ return UserDraftsStream.create({ user: this });
+ },
+
staff: Em.computed.or("admin", "moderator"),
destroySession() {
diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
index 63c5a2a12e3..c685ba1e833 100644
--- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6
+++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6
@@ -112,6 +112,7 @@ export default function() {
this.route("likesGiven", { path: "likes-given" });
this.route("bookmarks");
this.route("pending");
+ this.route("drafts");
}
);
diff --git a/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6 b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6
new file mode 100644
index 00000000000..2ca94b05e5e
--- /dev/null
+++ b/app/assets/javascripts/discourse/routes/user-activity-drafts.js.es6
@@ -0,0 +1,22 @@
+export default Discourse.Route.extend({
+ model() {
+ let userDraftsStream = this.modelFor("user").get("userDraftsStream");
+ return userDraftsStream.load(this.site).then(() => userDraftsStream);
+ },
+
+ renderTemplate() {
+ this.render("user_stream");
+ },
+
+ setupController(controller, model) {
+ controller.set("model", model);
+ this.appEvents.on("draft:destroyed", this, this.refresh);
+ },
+
+ actions: {
+ didTransition() {
+ this.controllerFor("user-activity")._showFooter();
+ return true;
+ }
+ }
+});
diff --git a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
index a2fb6a62e81..01a6052cf4a 100644
--- a/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
+++ b/app/assets/javascripts/discourse/routes/user-private-messages.js.es6
@@ -1,3 +1,5 @@
+import Draft from "discourse/models/draft";
+
export default Discourse.Route.extend({
renderTemplate() {
this.render("user/messages");
@@ -7,6 +9,23 @@ export default Discourse.Route.extend({
return this.modelFor("user");
},
+ setupController(controller, user) {
+ const composerController = this.controllerFor("composer");
+ controller.set("model", user);
+ if (this.currentUser) {
+ Draft.get("new_private_message").then(data => {
+ if (data.draft) {
+ composerController.open({
+ draft: data.draft,
+ draftKey: "new_private_message",
+ ignoreIfChanged: true,
+ draftSequence: data.draft_sequence
+ });
+ }
+ });
+ }
+ },
+
actions: {
willTransition: function() {
this._super();
diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6
index 47e4b118937..24093fd3a53 100644
--- a/app/assets/javascripts/discourse/routes/user.js.es6
+++ b/app/assets/javascripts/discourse/routes/user.js.es6
@@ -1,5 +1,3 @@
-import Draft from "discourse/models/draft";
-
export default Discourse.Route.extend({
titleToken() {
const username = this.modelFor("user").get("username");
@@ -67,21 +65,6 @@ export default Discourse.Route.extend({
setupController(controller, user) {
controller.set("model", user);
this.searchService.set("searchContext", user.get("searchContext"));
-
- const composerController = this.controllerFor("composer");
- controller.set("model", user);
- if (this.currentUser) {
- Draft.get("new_private_message").then(function(data) {
- if (data.draft) {
- composerController.open({
- draft: data.draft,
- draftKey: "new_private_message",
- ignoreIfChanged: true,
- draftSequence: data.draft_sequence
- });
- }
- });
- }
},
activate() {
diff --git a/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs b/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs
index e4e49ecbc13..c57d183991c 100644
--- a/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs
+++ b/app/assets/javascripts/discourse/templates/components/user-stream-item.hbs
@@ -1,12 +1,20 @@
{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}
{{format-date item.created_at}}
- {{expand-post item=item}}
+ {{#if item.draftType}}
+
{{{item.draftType}}}
+ {{else}}
+ {{expand-post item=item}}
+ {{/if}}
{{category-link item.category}}
@@ -50,3 +58,10 @@
{{/each}}
{{/each}}
+
+{{#if item.editableDraft}}
+
+ {{d-button action=resumeDraft actionParam=item icon="pencil" label='drafts.resume' class="resume-draft"}}
+ {{d-button action=removeDraft actionParam=item icon="times" label='drafts.remove' class="remove-draft"}}
+
+{{/if}}
diff --git a/app/assets/javascripts/discourse/templates/components/user-stream.hbs b/app/assets/javascripts/discourse/templates/components/user-stream.hbs
index e12fd05d516..1e3b3c306e8 100644
--- a/app/assets/javascripts/discourse/templates/components/user-stream.hbs
+++ b/app/assets/javascripts/discourse/templates/components/user-stream.hbs
@@ -1,3 +1,8 @@
{{#each stream.content as |item|}}
- {{user-stream-item item=item removeBookmark=(action "removeBookmark")}}
+ {{user-stream-item
+ item=item
+ removeBookmark=(action "removeBookmark")
+ resumeDraft=(action "resumeDraft")
+ removeDraft=(action "removeDraft")
+ }}
{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs
index f546aff2f4d..75899390d81 100644
--- a/app/assets/javascripts/discourse/templates/user/activity.hbs
+++ b/app/assets/javascripts/discourse/templates/user/activity.hbs
@@ -9,6 +9,11 @@
{{#link-to 'userActivity.replies'}}{{i18n 'user_action_groups.5'}}{{/link-to}}
+ {{#if user.showDrafts}}
+
+ {{#link-to 'userActivity.drafts'}}{{i18n 'user_action_groups.15'}}{{/link-to}}
+
+ {{/if}}
{{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
diff --git a/app/assets/stylesheets/common/components/user-stream-item.scss b/app/assets/stylesheets/common/components/user-stream-item.scss
index 7e904766650..54c2f126a89 100644
--- a/app/assets/stylesheets/common/components/user-stream-item.scss
+++ b/app/assets/stylesheets/common/components/user-stream-item.scss
@@ -36,13 +36,18 @@
}
.time,
- .delete-info {
+ .delete-info,
+ .draft-type {
display: block;
float: right;
color: lighten($primary, 40%);
font-size: $font-down-2;
}
+ .draft-type {
+ clear: right;
+ }
+
.delete-info i {
font-size: $font-0;
}
@@ -83,7 +88,8 @@
padding: 3px 5px 5px 5px;
}
- .remove-bookmark {
+ .remove-bookmark,
+ .remove-draft {
float: right;
margin-top: -4px;
}
@@ -95,6 +101,7 @@
p {
display: inline-block;
+
span {
color: $primary;
}
@@ -131,7 +138,8 @@
}
.user-stream .child-actions, /* DEPRECATED: '.user-stream .child-actions' selector*/
-.user-stream-item-actions {
+.user-stream-item-actions,
+.user-stream-item-draft-actions {
margin-top: 8px;
.avatar-link {
diff --git a/app/assets/stylesheets/desktop/components/user-stream-item.scss b/app/assets/stylesheets/desktop/components/user-stream-item.scss
index af6000f3845..d74b9550f16 100644
--- a/app/assets/stylesheets/desktop/components/user-stream-item.scss
+++ b/app/assets/stylesheets/desktop/components/user-stream-item.scss
@@ -11,7 +11,8 @@
}
.time,
- .delete-info {
+ .delete-info,
+ .draft-type {
margin-right: 8px;
}
diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb
new file mode 100644
index 00000000000..313e8814816
--- /dev/null
+++ b/app/controllers/drafts_controller.rb
@@ -0,0 +1,47 @@
+class DraftsController < ApplicationController
+ requires_login
+
+ skip_before_action :check_xhr, :preload_json
+
+ def index
+ params.require(:username)
+ params.permit(:offset)
+ params.permit(:limit)
+
+ user = fetch_user_from_params
+
+ opts = {
+ user: user,
+ offset: params[:offset],
+ limit: params[:limit]
+ }
+
+ guardian.ensure_can_see_drafts!(user)
+ stream = Draft.stream(opts)
+ stream.each do |d|
+ parsed_data = JSON.parse(d.data)
+ if parsed_data
+ if parsed_data['reply']
+ d.raw = parsed_data['reply']
+ end
+ if parsed_data['categoryId'].present? && !d.category_id.present?
+ d.category_id = parsed_data['categoryId']
+ end
+ end
+ end
+
+ help_key = "user_activity.no_drafts"
+ if user == current_user
+ help_key += ".self"
+ else
+ help_key += ".others"
+ end
+
+ render json: {
+ drafts: serialize_data(stream, DraftSerializer),
+ no_results_help: I18n.t(help_key)
+ }
+
+ end
+
+end
diff --git a/app/models/draft.rb b/app/models/draft.rb
index b008a6c51c9..812670feaaf 100644
--- a/app/models/draft.rb
+++ b/app/models/draft.rb
@@ -43,6 +43,43 @@ class Draft < ActiveRecord::Base
end
end
+ def self.stream(opts = nil)
+ opts ||= {}
+
+ user_id = opts[:user].id
+ offset = (opts[:offset] || 0).to_i
+ limit = (opts[:limit] || 30).to_i
+
+ # JOIN of topics table based on manipulating draft_key seems imperfect
+ builder = DB.build <<~SQL
+ SELECT
+ d.*, t.title, t.id topic_id, t.archetype,
+ t.category_id, t.closed topic_closed, t.archived topic_archived,
+ pu.username, pu.name, pu.id user_id, pu.uploaded_avatar_id, pu.username_lower,
+ du.username draft_username, NULL as raw, NULL as cooked, NULL as post_number
+ FROM drafts d
+ LEFT JOIN topics t ON
+ CASE
+ WHEN d.draft_key LIKE '%' || '#{EXISTING_TOPIC}' || '%'
+ THEN CAST(replace(d.draft_key, '#{EXISTING_TOPIC}', '') AS INT)
+ ELSE 0
+ END = t.id
+ JOIN users pu on pu.id = COALESCE(t.user_id, d.user_id)
+ JOIN users du on du.id = #{user_id}
+ /*where*/
+ /*order_by*/
+ /*offset*/
+ /*limit*/
+ SQL
+
+ builder
+ .where('d.user_id = :user_id', user_id: user_id.to_i)
+ .order_by('d.updated_at desc')
+ .offset(offset)
+ .limit(limit)
+ .query
+ end
+
def self.cleanup!
DB.exec("DELETE FROM drafts where sequence < (
SELECT max(s.sequence) from draft_sequences s
diff --git a/app/serializers/draft_serializer.rb b/app/serializers/draft_serializer.rb
new file mode 100644
index 00000000000..52399d3588d
--- /dev/null
+++ b/app/serializers/draft_serializer.rb
@@ -0,0 +1,56 @@
+require_relative 'post_item_excerpt'
+
+class DraftSerializer < ApplicationSerializer
+ include PostItemExcerpt
+
+ attributes :created_at,
+ :draft_key,
+ :sequence,
+ :draft_username,
+ :avatar_template,
+ :data,
+ :topic_id,
+ :username,
+ :username_lower,
+ :name,
+ :user_id,
+ :title,
+ :slug,
+ :category_id,
+ :closed,
+ :archetype,
+ :archived
+
+ def avatar_template
+ User.avatar_template(object.username, object.uploaded_avatar_id)
+ end
+
+ def slug
+ Slug.for(object.title)
+ end
+
+ def include_slug?
+ object.title.present?
+ end
+
+ def closed
+ object.topic_closed
+ end
+
+ def archived
+ object.topic_archived
+ end
+
+ def include_closed?
+ object.topic_closed.present?
+ end
+
+ def include_archived?
+ object.topic_archived.present?
+ end
+
+ def include_category_id?
+ object.category_id.present?
+ end
+
+end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e90a58a7ec7..ad05f9c7eac 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -287,6 +287,14 @@ en:
remove: "Remove Bookmark"
confirm_clear: "Are you sure you want to clear all the bookmarks from this topic?"
+ drafts:
+ resume: "Resume Draft"
+ remove: "Remove Draft"
+ new_topic: "New topic draft"
+ new_private_message: "New private message draft"
+ topic_reply: "Draft reply"
+ post_reply: "Draft reply to #{{postNumber}}"
+
topic_count_latest:
one: "See {{count}} new or updated topic"
other: "See {{count}} new or updated topics"
@@ -546,6 +554,7 @@ en:
"12": "Sent Items"
"13": "Inbox"
"14": "Pending"
+ "15": "Drafts"
categories:
all: "all categories"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 8af8699d4f2..1d034eca822 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -776,6 +776,9 @@ en:
no_replies:
self: "You have not replied to any posts."
others: "No replies."
+ no_drafts:
+ self: "You have no drafts."
+ others: "No drafts."
topic_flag_types:
spam:
diff --git a/config/routes.rb b/config/routes.rb
index fa9a7277926..edbade0d01d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -736,6 +736,7 @@ Discourse::Application.routes.draw do
get "message-bus/poll" => "message_bus#poll"
+ resources :drafts, only: [:index]
get "draft" => "draft#show"
post "draft" => "draft#update"
delete "draft" => "draft#destroy"
diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb
index 0aef85c9914..b111d5f5112 100644
--- a/lib/guardian/user_guardian.rb
+++ b/lib/guardian/user_guardian.rb
@@ -30,6 +30,10 @@ module UserGuardian
is_me?(user) || is_admin?
end
+ def can_see_drafts?(user)
+ is_me?(user) || is_admin?
+ end
+
def can_silence_user?(user)
user && is_staff? && not(user.staff?)
end
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 9b95f66e329..72c1c8db3a5 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -2350,6 +2350,24 @@ describe Guardian do
end
end
+ describe "can_see_drafts?" do
+ it "won't allow a non-logged in user to see a user's drafts" do
+ expect(Guardian.new.can_see_drafts?(user)).to be_falsey
+ end
+
+ it "won't allow a user to see another user's drafts" do
+ expect(Guardian.new(coding_horror).can_see_drafts?(user)).to be_falsey
+ end
+
+ it "will allow user to see own drafts" do
+ expect(Guardian.new(user).can_see_drafts?(user)).to be_truthy
+ end
+
+ it "will allow an admin to see a user's drafts" do
+ expect(Guardian.new(admin).can_see_drafts?(user)).to be_truthy
+ end
+ end
+
describe "can_edit_email?" do
context 'when allowed in settings' do
before do
diff --git a/spec/models/draft_spec.rb b/spec/models/draft_spec.rb
index 06ddf3d86ea..90fc10270c3 100644
--- a/spec/models/draft_spec.rb
+++ b/spec/models/draft_spec.rb
@@ -64,6 +64,34 @@ describe Draft do
expect(Draft.count).to eq 0
end
+ describe '#stream' do
+ let(:public_post) { Fabricate(:post) }
+ let(:public_topic) { public_post.topic }
+
+ let(:stream) do
+ Draft.stream(user: @user)
+ end
+
+ it "should include the correct number of drafts in the stream" do
+ Draft.set(@user, "test", 0, "first")
+ Draft.set(@user, "test2", 0, "second")
+ expect(stream.count).to eq(2)
+ end
+
+ it "should include the right topic id in a draft reply in the stream" do
+ Draft.set(@user, "topic_#{public_topic.id}", 0, "hey")
+ draft_row = stream.first
+ expect(draft_row.topic_id).to eq(public_topic.id)
+ end
+
+ it "should include the right draft username in the stream" do
+ Draft.set(@user, "topic_#{public_topic.id}", 0, "hey")
+ draft_row = stream.first
+ expect(draft_row.draft_username).to eq(@user.username)
+ end
+
+ end
+
context 'key expiry' do
it 'nukes new topic draft after a topic is created' do
u = Fabricate(:user)
diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb
new file mode 100644
index 00000000000..bffc3900b54
--- /dev/null
+++ b/spec/requests/drafts_controller_spec.rb
@@ -0,0 +1,27 @@
+require 'rails_helper'
+
+describe DraftsController do
+ it 'requires you to be logged in' do
+ get "/drafts.json"
+ expect(response.status).to eq(403)
+ end
+
+ it 'returns correct stream length after adding a draft' do
+ user = sign_in(Fabricate(:user))
+ Draft.set(user, 'xxx', 0, '{}')
+ get "/drafts.json", params: { username: user.username }
+ expect(response.status).to eq(200)
+ parsed = JSON.parse(response.body)
+ expect(parsed["drafts"].length).to eq(1)
+ end
+
+ it 'has empty stream after deleting last draft' do
+ user = sign_in(Fabricate(:user))
+ Draft.set(user, 'xxx', 0, '{}')
+ Draft.clear(user, 'xxx', 0)
+ get "/drafts.json", params: { username: user.username }
+ expect(response.status).to eq(200)
+ parsed = JSON.parse(response.body)
+ expect(parsed["drafts"].length).to eq(0)
+ end
+end
diff --git a/test/javascripts/acceptance/user-test.js.es6 b/test/javascripts/acceptance/user-test.js.es6
index abd0f933564..b215b402e45 100644
--- a/test/javascripts/acceptance/user-test.js.es6
+++ b/test/javascripts/acceptance/user-test.js.es6
@@ -39,3 +39,18 @@ QUnit.test("Viewing Summary", async assert => {
assert.ok(exists(".badges-section .badge-card"), "badges");
assert.ok(exists(".top-categories-section .category-link"), "top categories");
});
+
+QUnit.test("Viewing Drafts", async assert => {
+ await visit("/u/eviltrout/activity/drafts");
+ assert.ok(exists(".user-stream"), "has drafts stream");
+ assert.ok(
+ $(".user-stream .user-stream-item-draft-actions").length,
+ "has draft action buttons"
+ );
+
+ await click(".user-stream button.resume-draft:eq(0)");
+ assert.ok(
+ exists(".d-editor-input"),
+ "composer is visible after resuming a draft"
+ );
+});
diff --git a/test/javascripts/fixtures/draft.js.es6 b/test/javascripts/fixtures/draft.js.es6
new file mode 100644
index 00000000000..7819b4adc65
--- /dev/null
+++ b/test/javascripts/fixtures/draft.js.es6
@@ -0,0 +1,7 @@
+export default {
+ "/draft.json": {
+ draft:
+ '{"reply":"dum de dum da ba.","action":"createTopic","title":"dum da ba dum dum","categoryId":null,"archetypeId":"regular","metaData":null,"composerTime":540879,"typingTime":3400}',
+ draft_sequence: 0
+ }
+};
diff --git a/test/javascripts/fixtures/drafts.js.es6 b/test/javascripts/fixtures/drafts.js.es6
new file mode 100644
index 00000000000..8aaa88289c3
--- /dev/null
+++ b/test/javascripts/fixtures/drafts.js.es6
@@ -0,0 +1,61 @@
+export default {
+ "/drafts.json": {
+ drafts: [
+ {
+ excerpt: "A fun new topic for testing drafts. ",
+ truncated: true,
+ created_at: "2018-07-22T22:20:14.608Z",
+ draft_key: "new_topic",
+ sequence: 26,
+ draft_username: "eviltrout",
+ avatar_template: "/user_avatar/localhost/eviltrout/{size}/2_1.png",
+ data:
+ '{"reply":"A fun new topic for testing drafts. \\n","action":"createTopic","title":"This here is a new topic, friend","categoryId":3,"archetypeId":"regular","metaData":null,"composerTime":24532,"typingTime":2500}',
+ topic_id: null,
+ username: "eviltrout",
+ name: null,
+ user_id: 1,
+ title: null,
+ category_id: 3,
+ archetype: null
+ },
+ {
+ excerpt:
+ "The last reply to this topic was 6 months ago. Your reply will bump the topic to the top of its list",
+ truncated: true,
+ created_at: "2018-07-20T19:04:32.023Z",
+ draft_key: "topic_280",
+ sequence: 0,
+ draft_username: "eviltrout",
+ avatar_template: "/letter_avatar_proxy/v2/letter/p/a87d85/{size}.png",
+ data:
+ '{"reply":"The last reply to this topic was 6 months ago. Your reply will bump the topic to the top of its list.","action":"reply","categoryId":8,"archetypeId":"regular","metaData":null,"composerTime":139499,"typingTime":6100}',
+ topic_id: 280,
+ username: "zogstrip",
+ name: "zogstrip",
+ user_id: 6,
+ title: "Django hangs if I write gibberish",
+ slug: "django-hangs-if-i-write-gibberish",
+ category_id: 8,
+ archetype: "regular"
+ },
+ {
+ excerpt: "here goes a reply to a PM.",
+ created_at: "2018-07-20T16:58:47.433Z",
+ draft_key: "topic_93",
+ sequence: 1,
+ draft_username: "eviltrout",
+ avatar_template: "/user_avatar/localhost/eviltrout/{size}/2_1.png",
+ data:
+ '{"reply":"here goes a reply to a PM.","action":"reply","categoryId":null,"postId":212,"archetypeId":"regular","whisper":false,"metaData":null,"composerTime":455711,"typingTime":5400}',
+ topic_id: 93,
+ username: "eviltrout",
+ name: null,
+ user_id: 1,
+ title: "Hello dear friend, good to see you again",
+ slug: "hello-dear-friend-good-to-see-you-again",
+ archetype: "private_message"
+ }
+ ]
+ }
+};
diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6
index a206634abc7..0ec3ec51c2c 100644
--- a/test/javascripts/helpers/create-pretender.js.es6
+++ b/test/javascripts/helpers/create-pretender.js.es6
@@ -225,7 +225,15 @@ export default function() {
return response({ category });
});
- this.get("/draft.json", () => response({}));
+ this.get("/draft.json", request => {
+ if (request.queryParams.draft_key === "new_topic") {
+ return response(fixturesByUrl["/draft.json"]);
+ }
+
+ return response({});
+ });
+
+ this.get("/drafts.json", () => response(fixturesByUrl["/drafts.json"]));
this.put("/queued_posts/:queued_post_id", function(request) {
return response({ queued_post: { id: request.params.queued_post_id } });
diff --git a/test/javascripts/models/user-drafts-test.js.es6 b/test/javascripts/models/user-drafts-test.js.es6
new file mode 100644
index 00000000000..895bdce5508
--- /dev/null
+++ b/test/javascripts/models/user-drafts-test.js.es6
@@ -0,0 +1,31 @@
+import UserDraft from "discourse/models/user-draft";
+import { NEW_TOPIC_KEY } from "discourse/models/composer";
+
+QUnit.module("model:user-drafts");
+
+QUnit.test("stream", assert => {
+ const user = Discourse.User.create({ id: 1, username: "eviltrout" });
+ const stream = user.get("userDraftsStream");
+ assert.present(stream, "a user has a drafts stream by default");
+ assert.equal(stream.get("itemsLoaded"), 0, "no items are loaded by default");
+ assert.blank(stream.get("content"), "no content by default");
+});
+
+QUnit.test("draft", assert => {
+ const drafts = [
+ UserDraft.create({
+ draft_key: "topic_1",
+ post_number: "10"
+ }),
+ UserDraft.create({
+ draft_key: NEW_TOPIC_KEY
+ })
+ ];
+
+ assert.equal(drafts.length, 2, "drafts count is right");
+ assert.equal(
+ drafts[1].get("draftType"),
+ I18n.t("drafts.new_topic"),
+ "loads correct draftType label"
+ );
+});