Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
1725f6e5bc
|
@ -9,24 +9,54 @@
|
||||||
Discourse.TopicStatusComponent = Ember.Component.extend({
|
Discourse.TopicStatusComponent = Ember.Component.extend({
|
||||||
classNames: ['topic-statuses'],
|
classNames: ['topic-statuses'],
|
||||||
|
|
||||||
hasDisplayableStatus: Em.computed.or('topic.closed', 'topic.pinned', 'topic.invisible', 'topic.archetypeObject.notDefault'),
|
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'),
|
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) {
|
render: function(buffer) {
|
||||||
if (!this.get('hasDisplayableStatus')) { return; }
|
if (!this.get('hasDisplayableStatus')) { return; }
|
||||||
|
|
||||||
var self = this,
|
var self = this,
|
||||||
renderIconIf = function(conditionProp, name, key) {
|
renderIconIf = function(conditionProp, name, key, actionable) {
|
||||||
if (!self.get(conditionProp)) { return; }
|
if (!self.get(conditionProp)) { return; }
|
||||||
var title = I18n.t("topic_statuses." + key + ".help");
|
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
|
// Allow a plugin to add a custom icon to a topic
|
||||||
this.trigger('addCustomIcon', buffer);
|
this.trigger('addCustomIcon', buffer);
|
||||||
|
|
||||||
|
var togglePin = function(){
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
renderIconIf('topic.closed', 'lock', 'locked');
|
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');
|
renderIconIf('topic.invisible', 'eye-slash', 'invisible');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -271,12 +271,35 @@ Discourse.Topic = Discourse.Model.extend({
|
||||||
|
|
||||||
// Clear the pin optimistically from the object
|
// Clear the pin optimistically from the object
|
||||||
topic.set('pinned', false);
|
topic.set('pinned', false);
|
||||||
|
topic.set('unpinned', true);
|
||||||
|
|
||||||
Discourse.ajax("/t/" + this.get('id') + "/clear-pin", {
|
Discourse.ajax("/t/" + this.get('id') + "/clear-pin", {
|
||||||
type: 'PUT'
|
type: 'PUT'
|
||||||
}).then(null, function() {
|
}).then(null, function() {
|
||||||
// On error, put the pin back
|
// On error, put the pin back
|
||||||
topic.set('pinned', true);
|
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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
@import "common/components/*";
|
@import "common/components/*";
|
||||||
@import "common/admin/*";
|
@import "common/admin/*";
|
||||||
@import "common/input_tip";
|
@import "common/input_tip";
|
||||||
|
@import "common/base/*";
|
||||||
/* This file doesn't actually exist, it is injected by DiscourseSassImporter. */
|
/* This file doesn't actually exist, it is injected by DiscourseSassImporter. */
|
||||||
@import "plugins";
|
@import "plugins";
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.fa-thumb-tack.unpinned {
|
||||||
|
@include fa-icon-rotate(315deg, 1);
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ class TopicsController < ApplicationController
|
||||||
:move_posts,
|
:move_posts,
|
||||||
:merge_topic,
|
:merge_topic,
|
||||||
:clear_pin,
|
:clear_pin,
|
||||||
|
:re_pin,
|
||||||
:autoclose,
|
:autoclose,
|
||||||
:bulk,
|
:bulk,
|
||||||
:reset_new,
|
:reset_new,
|
||||||
|
@ -281,6 +282,13 @@ class TopicsController < ApplicationController
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
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
|
def timings
|
||||||
PostTiming.process_timings(
|
PostTiming.process_timings(
|
||||||
current_user,
|
current_user,
|
||||||
|
|
|
@ -618,6 +618,11 @@ class Topic < ActiveRecord::Base
|
||||||
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
|
TopicUser.change(user.id, id, cleared_pinned_at: Time.now)
|
||||||
end
|
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)
|
def update_pinned(status, global=false)
|
||||||
update_column(:pinned_at, status ? Time.now : nil)
|
update_column(:pinned_at, status ? Time.now : nil)
|
||||||
update_column(:pinned_globally, global)
|
update_column(:pinned_globally, global)
|
||||||
|
|
|
@ -14,6 +14,7 @@ class ListableTopicSerializer < BasicTopicSerializer
|
||||||
:unread,
|
:unread,
|
||||||
:new_posts,
|
:new_posts,
|
||||||
:pinned,
|
:pinned,
|
||||||
|
:unpinned,
|
||||||
:excerpt,
|
:excerpt,
|
||||||
:visible,
|
:visible,
|
||||||
:closed,
|
:closed,
|
||||||
|
@ -65,7 +66,11 @@ class ListableTopicSerializer < BasicTopicSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned
|
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
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
|
@ -31,6 +31,7 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:draft_sequence,
|
:draft_sequence,
|
||||||
:starred,
|
:starred,
|
||||||
:posted,
|
:posted,
|
||||||
|
:unpinned,
|
||||||
:pinned, # Is topic pinned and viewer hasn't cleared the pin?
|
:pinned, # Is topic pinned and viewer hasn't cleared the pin?
|
||||||
:pinned_at, # Ignores clear pin
|
:pinned_at, # Ignores clear pin
|
||||||
:details,
|
:details,
|
||||||
|
@ -41,7 +42,7 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:expandable_first_post
|
:expandable_first_post
|
||||||
|
|
||||||
# Define a delegator for each attribute of the topic we want
|
# Define a delegator for each attribute of the topic we want
|
||||||
attributes *topic_attributes
|
attributes(*topic_attributes)
|
||||||
topic_attributes.each do |ta|
|
topic_attributes.each do |ta|
|
||||||
class_eval %{def #{ta}
|
class_eval %{def #{ta}
|
||||||
object.topic.#{ta}
|
object.topic.#{ta}
|
||||||
|
@ -145,7 +146,11 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
alias_method :include_posted?, :has_topic_user?
|
alias_method :include_posted?, :has_topic_user?
|
||||||
|
|
||||||
def pinned
|
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
|
end
|
||||||
|
|
||||||
def pinned_at
|
def pinned_at
|
||||||
|
|
|
@ -1145,6 +1145,8 @@ en:
|
||||||
topic_statuses:
|
topic_statuses:
|
||||||
locked:
|
locked:
|
||||||
help: "this topic is closed; it no longer accepts new replies"
|
help: "this topic is closed; it no longer accepts new replies"
|
||||||
|
unpinned:
|
||||||
|
help: "this topic is unpinned; it will displayed in default ordering"
|
||||||
pinned:
|
pinned:
|
||||||
help: "this topic is pinned; it will display at the top of its category"
|
help: "this topic is pinned; it will display at the top of its category"
|
||||||
archived:
|
archived:
|
||||||
|
|
|
@ -315,6 +315,7 @@ Discourse::Application.routes.draw do
|
||||||
put "t/:slug/:topic_id/status" => "topics#status", constraints: {topic_id: /\d+/}
|
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/status" => "topics#status", constraints: {topic_id: /\d+/}
|
||||||
put "t/:topic_id/clear-pin" => "topics#clear_pin", 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/mute" => "topics#mute", constraints: {topic_id: /\d+/}
|
||||||
put "t/:topic_id/unmute" => "topics#unmute", 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+/}
|
put "t/:topic_id/autoclose" => "topics#autoclose", constraints: {topic_id: /\d+/}
|
||||||
|
|
|
@ -2,23 +2,16 @@
|
||||||
# taking into account anonymous users and users who have dismissed it
|
# taking into account anonymous users and users who have dismissed it
|
||||||
class PinnedCheck
|
class PinnedCheck
|
||||||
|
|
||||||
def initialize(topic, topic_user=nil)
|
def self.unpinned?(topic,topic_user=nil)
|
||||||
@topic, @topic_user = topic, topic_user
|
topic.pinned_at &&
|
||||||
|
topic_user &&
|
||||||
|
topic_user.cleared_pinned_at &&
|
||||||
|
topic_user.cleared_pinned_at > topic.pinned_at
|
||||||
end
|
end
|
||||||
|
|
||||||
def pinned?
|
def self.pinned?(topic, topic_user=nil)
|
||||||
|
!!topic.pinned_at &&
|
||||||
# If the topic isn't pinned the answer is false
|
!unpinned?(topic,topic_user)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
|
@ -1,9 +1,8 @@
|
||||||
|
require 'spec_helper'
|
||||||
require 'pinned_check'
|
require 'pinned_check'
|
||||||
|
|
||||||
describe PinnedCheck do
|
describe PinnedCheck do
|
||||||
|
|
||||||
#let(:topic) { Fabricate.build(:topic) }
|
|
||||||
|
|
||||||
let(:pinned_at) { 12.hours.ago }
|
let(:pinned_at) { 12.hours.ago }
|
||||||
let(:unpinned_topic) { Fabricate.build(:topic) }
|
let(:unpinned_topic) { Fabricate.build(:topic) }
|
||||||
let(:pinned_topic) { Fabricate.build(:topic, pinned_at: pinned_at) }
|
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
|
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
|
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
|
end
|
||||||
|
|
||||||
it "returns true if the topic is pinned" do
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -28,7 +27,7 @@ describe PinnedCheck do
|
||||||
let(:topic_user) { TopicUser.new(topic: unpinned_topic, user: user) }
|
let(:topic_user) { TopicUser.new(topic: unpinned_topic, user: user) }
|
||||||
|
|
||||||
it "returns false" do
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -37,17 +36,17 @@ describe PinnedCheck do
|
||||||
let(:topic_user) { TopicUser.new(topic: pinned_topic, user: user) }
|
let(:topic_user) { TopicUser.new(topic: pinned_topic, user: user) }
|
||||||
|
|
||||||
it "is pinned if the topic_user's cleared_pinned_at is blank" do
|
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
|
end
|
||||||
|
|
||||||
it "is not pinned if the topic_user's cleared_pinned_at is later than when it was pinned_at" do
|
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)
|
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
|
end
|
||||||
|
|
||||||
it "is pinned if the topic_user's cleared_pinned_at is earlier than when it was pinned_at" do
|
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)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue