diff --git a/app/assets/javascripts/discourse/app/controllers/topic.js b/app/assets/javascripts/discourse/app/controllers/topic.js index b5e7e400db..9d73b1b4bf 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic.js +++ b/app/assets/javascripts/discourse/app/controllers/topic.js @@ -1424,6 +1424,12 @@ export default Controller.extend(bufferedProperty("model"), { .then(() => refresh({ id: data.id })); break; } + case "destroyed": { + postStream + .triggerDestroyedPost(data.id) + .then(() => refresh({ id: data.id })); + break; + } case "recovered": { postStream .triggerRecoveredPost(data.id) diff --git a/app/assets/javascripts/discourse/app/models/post-stream.js b/app/assets/javascripts/discourse/app/models/post-stream.js index d68c2cac41..42e5da69fe 100644 --- a/app/assets/javascripts/discourse/app/models/post-stream.js +++ b/app/assets/javascripts/discourse/app/models/post-stream.js @@ -737,6 +737,12 @@ export default RestModel.extend({ return Promise.resolve(); }, + triggerDestroyedPost(postId) { + const existing = this._identityMap[postId]; + this.removePosts([existing]); + return Promise.resolve(); + }, + triggerChangedPost(postId, updatedAt, opts) { opts = opts || {}; diff --git a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js index ee02683517..4d209f5fee 100644 --- a/app/assets/javascripts/discourse/app/models/topic-tracking-state.js +++ b/app/assets/javascripts/discourse/app/models/topic-tracking-state.js @@ -6,6 +6,7 @@ import PreloadStore from "discourse/lib/preload-store"; import Category from "discourse/models/category"; import User from "discourse/models/user"; import { deepEqual } from "discourse-common/lib/object"; +import DiscourseURL from "discourse/lib/url"; function isNew(topic) { return ( @@ -148,6 +149,17 @@ const TopicTrackingState = EmberObject.extend({ } tracker.incrementMessageCount(); }); + + this.messageBus.subscribe("/destroy", (msg) => { + tracker.incrementMessageCount(); + const currentRoute = DiscourseURL.router.currentRoute.parent; + if ( + currentRoute.name === "topic" && + parseInt(currentRoute.params.id, 10) === msg.topic_id + ) { + DiscourseURL.redirectTo("/"); + } + }); }, mutedTopics() { diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 9f8b5a66af..e018d96132 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -171,6 +171,17 @@ class TopicTrackingState MessageBus.publish("/delete", message.as_json, group_ids: group_ids) end + def self.publish_destroy(topic) + group_ids = topic.category && topic.category.secure_group_ids + + message = { + topic_id: topic.id, + message_type: "destroy" + } + + MessageBus.publish("/destroy", message.as_json, group_ids: group_ids) + end + def self.publish_read(topic_id, last_read_post_number, user_id, notification_level = nil) highest_post_number = DB.query_single("SELECT highest_post_number FROM topics WHERE id = ?", topic_id).first diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 0b569ff405..e5bff487bb 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -66,7 +66,7 @@ class PostDestroyer delete_removed_posts_after = @opts[:delete_removed_posts_after] || SiteSetting.delete_removed_posts_after - if delete_removed_posts_after < 1 || post_is_reviewable? || Guardian.new(@user).can_moderate_topic?(topic) + if delete_removed_posts_after < 1 || post_is_reviewable? || Guardian.new(@user).can_moderate_topic?(topic) || permanent? perform_delete elsif @user.id == @post.user_id mark_for_deletion(delete_removed_posts_after) @@ -140,9 +140,10 @@ class PostDestroyer # When a post is properly deleted. Well, it's still soft deleted, but it will no longer # show up in the topic + # Permanent option allows to hard delete. def perform_delete Post.transaction do - @post.trash!(@user) + permanent? ? @post.destroy! : @post.trash!(@user) if @post.topic make_previous_post_the_last_one mark_topic_changed @@ -162,7 +163,9 @@ class PostDestroyer end end - @post.topic.trash!(@user) if @post.topic && @post.is_first_post? + if @post.topic && @post.is_first_post? + permanent? ? @post.topic.destroy! : @post.topic.trash!(@user) + end update_associated_category_latest_topic update_user_counts TopicUser.update_post_action_cache(post_id: @post.id) @@ -178,8 +181,12 @@ class PostDestroyer update_imap_sync(@post, true) if @post.topic&.deleted_at feature_users_in_the_topic if @post.topic - @post.publish_change_to_clients! :deleted if @post.topic - TopicTrackingState.publish_delete(@post.topic) if @post.topic && @post.post_number == 1 + @post.publish_change_to_clients!(permanent? ? :destroyed : :deleted) if @post.topic + TopicTrackingState.send(permanent? ? :publish_destroy : :publish_delete, @post.topic) if @post.topic && @post.post_number == 1 + end + + def permanent? + @opts[:permanent] && @user == @post.user && @post.topic.private_message? end # When a user 'deletes' their own post. We just change the text. diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index 364e70aecb..be68b980f5 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -174,6 +174,7 @@ module SvgSprite "star", "step-backward", "step-forward", + "stopwatch", "stream", "sync-alt", "sync", diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index c6ac8e5dd7..be1f3aa9c2 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -961,4 +961,27 @@ describe PostDestroyer do expect(user.user_profile.reload.featured_topic).to eq(nil) end end + + describe "permanent destroy" do + fab!(:private_message_topic) { Fabricate(:private_message_topic) } + fab!(:private_post) { Fabricate(:private_message_post, topic: private_message_topic) } + fab!(:reply) { Fabricate(:private_message_post, topic: private_message_topic) } + it "destroys the post and topic if deleting first post" do + PostDestroyer.new(reply.user, reply, permanent: true).destroy + expect { reply.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(private_message_topic.reload.persisted?).to be true + + PostDestroyer.new(private_post.user, private_post, permanent: true).destroy + expect { private_post.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { private_message_topic.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'soft delete if not creator of post or not private message' do + PostDestroyer.new(moderator, reply, permanent: true).destroy + expect(reply.deleted_at).not_to eq(nil) + + PostDestroyer.new(post.user, post, permanent: true).destroy + expect(post.user_deleted).to be true + end + end end