FEATURE: display unpinned state, allow unpinning by clicking on pin

This commit is contained in:
Sam 2014-04-10 10:56:56 +10:00
parent 3f6764ce22
commit b9d4edd91a
12 changed files with 107 additions and 33 deletions

View File

@ -9,24 +9,54 @@
Discourse.TopicStatusComponent = Ember.Component.extend({
classNames: ['topic-statuses'],
hasDisplayableStatus: Em.computed.or('topic.closed', 'topic.pinned', 'topic.invisible', 'topic.archetypeObject.notDefault'),
shouldRerender: Discourse.View.renderIfChanged('topic.closed', 'topic.pinned', 'topic.visible'),
hasDisplayableStatus: Em.computed.or('topic.closed', 'topic.pinned', 'topic.unpinned', 'topic.invisible', 'topic.archetypeObject.notDefault'),
shouldRerender: Discourse.View.renderIfChanged('topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned'),
didInsertElement: function(){
var topic = this.get('topic');
// could be passed in a controller
if(topic.constructor.toString() !== 'Discourse.Topic') {
topic = topic.get('model');
}
this.$('a').click(function(){
// only pin unpin for now
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
}
return false;
});
},
render: function(buffer) {
if (!this.get('hasDisplayableStatus')) { return; }
var self = this,
renderIconIf = function(conditionProp, name, key) {
renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
var title = I18n.t("topic_statuses." + key + ".help");
buffer.push("<span title='" + title + "' class='topic-status'><i class='fa fa-" + name + "'></i></span>");
var startTag = actionable ? "a href='#'" : "span";
var endTag = actionable ? "a" : "span";
buffer.push("<" + startTag +
" title='" + title +"' class='topic-status'><i class='fa fa-" + name + "'></i></" + endTag + ">");
};
// Allow a plugin to add a custom icon to a topic
this.trigger('addCustomIcon', buffer);
var togglePin = function(){
};
renderIconIf('topic.closed', 'lock', 'locked');
renderIconIf('topic.pinned', 'thumb-tack', 'pinned');
renderIconIf('topic.pinned', 'thumb-tack', 'pinned', togglePin);
renderIconIf('topic.unpinned', 'thumb-tack unpinned', 'unpinned', togglePin);
renderIconIf('topic.invisible', 'eye-slash', 'invisible');
}
});

View File

@ -271,12 +271,35 @@ Discourse.Topic = Discourse.Model.extend({
// Clear the pin optimistically from the object
topic.set('pinned', false);
topic.set('unpinned', true);
Discourse.ajax("/t/" + this.get('id') + "/clear-pin", {
type: 'PUT'
}).then(null, function() {
// On error, put the pin back
topic.set('pinned', true);
topic.set('unpinned', false);
});
},
/**
Re-pins a topic with a cleared pin
@method rePin
**/
rePin: function() {
var topic = this;
// Clear the pin optimistically from the object
topic.set('pinned', true);
topic.set('unpinned', false);
Discourse.ajax("/t/" + this.get('id') + "/re-pin", {
type: 'PUT'
}).then(null, function() {
// On error, put the pin back
topic.set('pinned', true);
topic.set('unpinned', false);
});
},

View File

@ -8,6 +8,6 @@
@import "common/components/*";
@import "common/admin/*";
@import "common/input_tip";
@import "common/base/*";
/* This file doesn't actually exist, it is injected by DiscourseSassImporter. */
@import "plugins";

View File

@ -0,0 +1,3 @@
.fa-thumb-tack.unpinned {
@include fa-icon-rotate(315deg, 1);
}

View File

@ -20,6 +20,7 @@ class TopicsController < ApplicationController
:move_posts,
:merge_topic,
:clear_pin,
:re_pin,
:autoclose,
:bulk,
:reset_new,
@ -281,6 +282,13 @@ class TopicsController < ApplicationController
render nothing: true
end
def re_pin
topic = Topic.where(id: params[:topic_id].to_i).first
guardian.ensure_can_see!(topic)
topic.re_pin_for(current_user)
render nothing: true
end
def timings
PostTiming.process_timings(
current_user,

View File

@ -618,6 +618,11 @@ class Topic < ActiveRecord::Base
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
end
def re_pin_for(user)
return unless user.present?
TopicUser.change(user.id, id, cleared_pinned_at: nil)
end
def update_pinned(status, global=false)
update_column(:pinned_at, status ? Time.now : nil)
update_column(:pinned_globally, global)

View File

@ -14,6 +14,7 @@ class ListableTopicSerializer < BasicTopicSerializer
:unread,
:new_posts,
:pinned,
:unpinned,
:excerpt,
:visible,
:closed,
@ -65,7 +66,11 @@ class ListableTopicSerializer < BasicTopicSerializer
end
def pinned
PinnedCheck.new(object, object.user_data).pinned?
PinnedCheck.pinned?(object, object.user_data)
end
def unpinned
PinnedCheck.unpinned?(object, object.user_data)
end
protected

View File

@ -31,6 +31,7 @@ class TopicViewSerializer < ApplicationSerializer
:draft_sequence,
:starred,
:posted,
:unpinned,
:pinned, # Is topic pinned and viewer hasn't cleared the pin?
:pinned_at, # Ignores clear pin
:details,
@ -41,7 +42,7 @@ class TopicViewSerializer < ApplicationSerializer
:expandable_first_post
# Define a delegator for each attribute of the topic we want
attributes *topic_attributes
attributes(*topic_attributes)
topic_attributes.each do |ta|
class_eval %{def #{ta}
object.topic.#{ta}
@ -145,7 +146,11 @@ class TopicViewSerializer < ApplicationSerializer
alias_method :include_posted?, :has_topic_user?
def pinned
PinnedCheck.new(object.topic, object.topic_user).pinned?
PinnedCheck.pinned?(object.topic, object.topic_user)
end
def unpinned
PinnedCheck.unpinned?(object.topic, object.topic_user)
end
def pinned_at

View File

@ -1145,6 +1145,8 @@ en:
topic_statuses:
locked:
help: "this topic is closed; it no longer accepts new replies"
unpinned:
help: "this topic is unpinned; it will displayed in default ordering"
pinned:
help: "this topic is pinned; it will display at the top of its category"
archived:

View File

@ -315,6 +315,7 @@ Discourse::Application.routes.draw do
put "t/:slug/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/}
put "t/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/}
put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: {topic_id: /\d+/}
put "t/:topic_id/re-pin" => "topics#re_pin", constraints: {topic_id: /\d+/}
put "t/:topic_id/mute" => "topics#mute", constraints: {topic_id: /\d+/}
put "t/:topic_id/unmute" => "topics#unmute", constraints: {topic_id: /\d+/}
put "t/:topic_id/autoclose" => "topics#autoclose", constraints: {topic_id: /\d+/}

View File

@ -2,23 +2,16 @@
# taking into account anonymous users and users who have dismissed it
class PinnedCheck
def initialize(topic, topic_user=nil)
@topic, @topic_user = topic, topic_user
def self.unpinned?(topic,topic_user=nil)
topic.pinned_at &&
topic_user &&
topic_user.cleared_pinned_at &&
topic_user.cleared_pinned_at > topic.pinned_at
end
def pinned?
# If the topic isn't pinned the answer is false
return false if @topic.pinned_at.blank?
# If the user is anonymous or hasn't entered the topic, the value is always true
return true if @topic_user.blank?
# If the user hasn't cleared the pin, it's true
return true if @topic_user.cleared_pinned_at.blank?
# The final check is to see whether the cleared the pin before or after it was last pinned
@topic_user.cleared_pinned_at < @topic.pinned_at
def self.pinned?(topic, topic_user=nil)
!!topic.pinned_at &&
!unpinned?(topic,topic_user)
end
end
end

View File

@ -1,9 +1,8 @@
require 'spec_helper'
require 'pinned_check'
describe PinnedCheck do
#let(:topic) { Fabricate.build(:topic) }
let(:pinned_at) { 12.hours.ago }
let(:unpinned_topic) { Fabricate.build(:topic) }
let(:pinned_topic) { Fabricate.build(:topic, pinned_at: pinned_at) }
@ -11,11 +10,11 @@ describe PinnedCheck do
context "without a topic_user record (either anonymous or never been in the topic)" do
it "returns false if the topic is not pinned" do
PinnedCheck.new(unpinned_topic).should_not be_pinned
PinnedCheck.pinned?(unpinned_topic).should be_false
end
it "returns true if the topic is pinned" do
PinnedCheck.new(unpinned_topic).should_not be_pinned
PinnedCheck.pinned?(unpinned_topic).should be_false
end
end
@ -28,7 +27,7 @@ describe PinnedCheck do
let(:topic_user) { TopicUser.new(topic: unpinned_topic, user: user) }
it "returns false" do
PinnedCheck.new(unpinned_topic, topic_user).should_not be_pinned
PinnedCheck.pinned?(unpinned_topic, topic_user).should be_false
end
end
@ -37,17 +36,17 @@ describe PinnedCheck do
let(:topic_user) { TopicUser.new(topic: pinned_topic, user: user) }
it "is pinned if the topic_user's cleared_pinned_at is blank" do
PinnedCheck.new(pinned_topic, topic_user).should be_pinned
PinnedCheck.pinned?(pinned_topic, topic_user).should be_true
end
it "is not pinned if the topic_user's cleared_pinned_at is later than when it was pinned_at" do
topic_user.cleared_pinned_at = (pinned_at + 1.hour)
PinnedCheck.new(pinned_topic, topic_user).should_not be_pinned
PinnedCheck.pinned?(pinned_topic, topic_user).should be_false
end
it "is pinned if the topic_user's cleared_pinned_at is earlier than when it was pinned_at" do
topic_user.cleared_pinned_at = (pinned_at - 3.hours)
PinnedCheck.new(pinned_topic, topic_user).should be_pinned
PinnedCheck.pinned?(pinned_topic, topic_user).should be_true
end
end