thredded 0.15.3 → 0.15.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/javascripts/thredded/components/submit_hotkey.es6 +18 -0
- data/app/assets/stylesheets/thredded/components/_pagination.scss +1 -1
- data/app/assets/stylesheets/thredded/components/_post-form.scss +0 -1
- data/app/assets/stylesheets/thredded/components/_topics.scss +2 -28
- data/app/controllers/thredded/application_controller.rb +2 -0
- data/app/controllers/thredded/post_permalinks_controller.rb +1 -1
- data/app/controllers/thredded/post_previews_controller.rb +1 -1
- data/app/controllers/thredded/posts_controller.rb +2 -3
- data/app/controllers/thredded/private_post_permalinks_controller.rb +1 -1
- data/app/controllers/thredded/private_post_previews_controller.rb +1 -1
- data/app/controllers/thredded/private_posts_controller.rb +2 -3
- data/app/helpers/thredded/urls_helper.rb +28 -1
- data/app/models/concerns/thredded/content_moderation_state.rb +2 -2
- data/app/models/concerns/thredded/post_common.rb +1 -1
- data/app/models/concerns/thredded/topic_common.rb +1 -1
- data/app/models/thredded/messageboard.rb +0 -3
- data/app/models/thredded/post.rb +8 -0
- data/app/models/thredded/post_moderation_record.rb +2 -0
- data/app/models/thredded/private_post.rb +8 -0
- data/app/models/thredded/private_topic.rb +14 -0
- data/app/models/thredded/topic.rb +2 -2
- data/app/models/thredded/user_extender.rb +2 -2
- data/app/views/thredded/posts_common/_form.html.erb +1 -0
- data/app/views/thredded/posts_common/form/_content_field.html.erb +1 -1
- data/app/views/thredded/private_topics/_form.html.erb +2 -0
- data/app/views/thredded/private_topics/_private_topic.html.erb +0 -5
- data/app/views/thredded/topics/_form.html.erb +1 -0
- data/app/views/thredded/topics/_topic.html.erb +6 -4
- data/app/views/thredded/topics/unread.html.erb +4 -1
- data/config/i18n-tasks.yml +1 -0
- data/config/locales/de.yml +5 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +5 -0
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/locales/pl.yml +5 -0
- data/config/locales/pt-BR.yml +5 -0
- data/config/locales/ru.yml +6 -1
- data/config/locales/zh-CN.yml +5 -0
- data/lib/generators/thredded/install/templates/initializer.rb +17 -11
- data/lib/thredded.rb +111 -48
- data/lib/thredded/content_formatter.rb +2 -2
- data/lib/thredded/database_seeder.rb +6 -6
- data/lib/thredded/db_tools.rb +2 -2
- data/lib/thredded/errors.rb +13 -1
- data/lib/thredded/html_pipeline/onebox_filter.rb +2 -2
- data/lib/thredded/version.rb +1 -1
- data/vendor/assets/javascripts/autosize.min.js +2 -6
- data/vendor/assets/javascripts/textcomplete.min.js +2 -2
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4fb30f1cf973b0e6d8e26feb9006cbf20b1405cddfcaa2deb4e06ccd1d3a6524
|
4
|
+
data.tar.gz: cd3fa99b4797609c4e840ed02a3117ccf7288d1c706ef879ac6ee3d32546404a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef7c3bd8c922fc954a55e00625ffe267b0a164be7e4f36eb434de72a70f974571ae72cde41d799643ce893ef3f55aff7b5daebc70119cfe7462027d150ddf897
|
7
|
+
data.tar.gz: e005eb547cbbb181d9f118ff54e9d48427a28c6b3b66dd2e19289e8b5468ceeb059b35e80d9d62b896af964ea416184ac3ea4c4fc8d189675a9b2d5dd9d23616
|
data/README.md
CHANGED
@@ -95,7 +95,7 @@ Then, see the rest of this Readme for more information about using and customizi
|
|
95
95
|
Add the gem to your Gemfile:
|
96
96
|
|
97
97
|
```ruby
|
98
|
-
gem 'thredded', '~> 0.15.
|
98
|
+
gem 'thredded', '~> 0.15.4'
|
99
99
|
```
|
100
100
|
|
101
101
|
Add the Thredded [initializer] to your parent app by running the install generator.
|
@@ -0,0 +1,18 @@
|
|
1
|
+
(function() {
|
2
|
+
const Thredded = window.Thredded;
|
3
|
+
Thredded.isSubmitHotkey = (evt) => {
|
4
|
+
// Ctrl+Enter.
|
5
|
+
return evt.ctrlKey && (evt.keyCode === 13 || evt.keyCode === 10 /* http://crbug.com/79407 */);
|
6
|
+
};
|
7
|
+
|
8
|
+
document.addEventListener('keypress', (evt) => {
|
9
|
+
if (Thredded.isSubmitHotkey(evt)) {
|
10
|
+
const submitButton = document.querySelector('[data-thredded-submit-hotkey] [type="submit"]');
|
11
|
+
if (!submitButton) return;
|
12
|
+
evt.preventDefault();
|
13
|
+
// Focus first for better visual feedback.
|
14
|
+
submitButton.focus();
|
15
|
+
submitButton.click();
|
16
|
+
}
|
17
|
+
});
|
18
|
+
})();
|
@@ -1,6 +1,7 @@
|
|
1
1
|
.thredded--pagination {
|
2
2
|
border-top: $thredded-base-border;
|
3
3
|
padding-top: $thredded-base-spacing;
|
4
|
+
padding-bottom: $thredded-base-spacing;
|
4
5
|
text-align: center;
|
5
6
|
|
6
7
|
> span {
|
@@ -27,7 +28,6 @@
|
|
27
28
|
.thredded--pagination-top > .thredded--pagination {
|
28
29
|
border-bottom: $thredded-base-border;
|
29
30
|
margin-bottom: $thredded-base-spacing;
|
30
|
-
padding-bottom: $thredded-base-spacing;
|
31
31
|
}
|
32
32
|
|
33
33
|
.thredded--pagination-bottom > .thredded--pagination {
|
@@ -55,8 +55,8 @@
|
|
55
55
|
}
|
56
56
|
}
|
57
57
|
|
58
|
-
.thredded--topics--
|
59
|
-
.thredded--topics--
|
58
|
+
.thredded--topics--updated-by,
|
59
|
+
.thredded--topics--messageboard {
|
60
60
|
color: $thredded-secondary-text-color;
|
61
61
|
font-size: $thredded-font-size-small;
|
62
62
|
font-style: normal;
|
@@ -69,22 +69,6 @@
|
|
69
69
|
color: $thredded-action-color;
|
70
70
|
}
|
71
71
|
}
|
72
|
-
|
73
|
-
abbr {
|
74
|
-
&::after {
|
75
|
-
content: ' by ';
|
76
|
-
}
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
.thredded--topics--updated-by {
|
81
|
-
margin-right: $thredded-small-spacing;
|
82
|
-
|
83
|
-
abbr {
|
84
|
-
&::before {
|
85
|
-
content: 'updated ';
|
86
|
-
}
|
87
|
-
}
|
88
72
|
}
|
89
73
|
|
90
74
|
.thredded--topics--participants--participant {
|
@@ -93,16 +77,6 @@
|
|
93
77
|
}
|
94
78
|
}
|
95
79
|
|
96
|
-
.thredded--topics--started-by {
|
97
|
-
display: none;
|
98
|
-
|
99
|
-
abbr {
|
100
|
-
&::before {
|
101
|
-
content: 'started ';
|
102
|
-
}
|
103
|
-
}
|
104
|
-
}
|
105
|
-
|
106
80
|
.thredded--topics--moderation-state {
|
107
81
|
padding: 0.3em 0.5em;
|
108
82
|
font-size: $thredded-font-size-small;
|
@@ -20,7 +20,9 @@ module Thredded
|
|
20
20
|
|
21
21
|
rescue_from Thredded::Errors::MessageboardNotFound,
|
22
22
|
Thredded::Errors::PrivateTopicNotFound,
|
23
|
+
Thredded::Errors::PrivatePostNotFound,
|
23
24
|
Thredded::Errors::TopicNotFound,
|
25
|
+
Thredded::Errors::PostNotFound,
|
24
26
|
Thredded::Errors::UserNotFound do |exception|
|
25
27
|
@error = exception
|
26
28
|
@message = exception.message
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Thredded
|
4
4
|
class PostPermalinksController < Thredded::ApplicationController
|
5
5
|
def show
|
6
|
-
post = Thredded::Post.find(params[:id])
|
6
|
+
post = Thredded::Post.find!(params[:id])
|
7
7
|
authorize post, :read?
|
8
8
|
redirect_to post_url(post, user: thredded_current_user), status: :found
|
9
9
|
end
|
@@ -81,12 +81,11 @@ module Thredded
|
|
81
81
|
def parent_topic
|
82
82
|
Thredded::Topic
|
83
83
|
.where(messageboard: messageboard)
|
84
|
-
.
|
85
|
-
.find(params[:topic_id])
|
84
|
+
.friendly_find!(params[:topic_id])
|
86
85
|
end
|
87
86
|
|
88
87
|
def post
|
89
|
-
@post ||= Thredded::Post.find(params[:id])
|
88
|
+
@post ||= Thredded::Post.find!(params[:id])
|
90
89
|
end
|
91
90
|
|
92
91
|
def current_page
|
@@ -4,7 +4,7 @@ module Thredded
|
|
4
4
|
class PrivatePostPermalinksController < Thredded::ApplicationController
|
5
5
|
before_action :thredded_require_login!
|
6
6
|
def show
|
7
|
-
private_post = Thredded::PrivatePost.find(params[:id])
|
7
|
+
private_post = Thredded::PrivatePost.find!(params[:id])
|
8
8
|
authorize private_post, :read?
|
9
9
|
redirect_to post_url(private_post, user: thredded_current_user), status: :found
|
10
10
|
end
|
@@ -13,7 +13,7 @@ module Thredded
|
|
13
13
|
|
14
14
|
# Preview an update to an existing post
|
15
15
|
def update
|
16
|
-
@private_post = Thredded::PrivatePost.find(params[:private_post_id])
|
16
|
+
@private_post = Thredded::PrivatePost.find!(params[:private_post_id])
|
17
17
|
@private_post.assign_attributes(private_post_params)
|
18
18
|
render_preview
|
19
19
|
end
|
@@ -80,12 +80,11 @@ module Thredded
|
|
80
80
|
def parent_topic
|
81
81
|
Thredded::PrivateTopic
|
82
82
|
.includes(:private_users)
|
83
|
-
.
|
84
|
-
.find(params[:private_topic_id])
|
83
|
+
.friendly_find!(params[:private_topic_id])
|
85
84
|
end
|
86
85
|
|
87
86
|
def post
|
88
|
-
@post ||= Thredded::PrivatePost.find(params[:id])
|
87
|
+
@post ||= Thredded::PrivatePost.find!(params[:id])
|
89
88
|
end
|
90
89
|
|
91
90
|
def current_page
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Thredded
|
4
|
-
module UrlsHelper
|
4
|
+
module UrlsHelper # rubocop:disable Metrics/ModuleLength
|
5
5
|
class << self
|
6
6
|
include Thredded::Engine.routes.url_helpers
|
7
7
|
include Thredded::UrlsHelper
|
@@ -141,5 +141,32 @@ module Thredded
|
|
141
141
|
post_permalink_path(post)
|
142
142
|
end
|
143
143
|
end
|
144
|
+
|
145
|
+
# @param [Thredded.user_class] current_user
|
146
|
+
# @param [Thredded.user_class] to
|
147
|
+
# @param [Boolean] use_existing Whether to use the existing thread if any.
|
148
|
+
# @return [String] a path to a new or existing private message thread for the given users.
|
149
|
+
def send_private_message_path(current_user:, to:, use_existing: true)
|
150
|
+
existing_topic = use_existing &&
|
151
|
+
Thredded::PrivateTopic.has_exact_participants([current_user, to])
|
152
|
+
.order_recently_posted_first.first
|
153
|
+
if existing_topic
|
154
|
+
page = 1 + (existing_topic.posts_count - 1) / Thredded::PrivatePost.default_per_page
|
155
|
+
Thredded::UrlsHelper.private_topic_path(
|
156
|
+
existing_topic,
|
157
|
+
page: (page if page > 1),
|
158
|
+
autofocus_new_post_content: true,
|
159
|
+
anchor: 'post_content'
|
160
|
+
)
|
161
|
+
else
|
162
|
+
Thredded::UrlsHelper.new_private_topic_path(
|
163
|
+
private_topic: {
|
164
|
+
user_names: to.send(Thredded.user_name_column),
|
165
|
+
title: [current_user, to].map(&Thredded.user_display_name_method).join(' • ')
|
166
|
+
},
|
167
|
+
autofocus_new_post_content: true,
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
144
171
|
end
|
145
172
|
end
|
@@ -13,7 +13,7 @@ module Thredded
|
|
13
13
|
|
14
14
|
scope :moderation_state_visible_to_all, -> { where(visible_to_all_arel_node) }
|
15
15
|
|
16
|
-
scope :moderation_state_visible_to_user, (
|
16
|
+
scope :moderation_state_visible_to_user, ->(user) {
|
17
17
|
visible = visible_to_all_arel_node
|
18
18
|
# @type [Arel::Table]
|
19
19
|
table = arel_table
|
@@ -27,7 +27,7 @@ module Thredded
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
where(visible)
|
30
|
-
|
30
|
+
}
|
31
31
|
|
32
32
|
# @return [Arel::Nodes::Node]
|
33
33
|
# @api private
|
@@ -4,7 +4,7 @@ module Thredded
|
|
4
4
|
module TopicCommon
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
included do
|
7
|
-
paginates_per
|
7
|
+
paginates_per Thredded.topics_per_page if respond_to?(:paginates_per)
|
8
8
|
|
9
9
|
belongs_to :last_user,
|
10
10
|
class_name: Thredded.user_class_name,
|
@@ -58,8 +58,6 @@ module Thredded
|
|
58
58
|
**(Thredded.rails_gte_51? ? { optional: true } : {})
|
59
59
|
|
60
60
|
has_many :post_moderation_records, inverse_of: :messageboard, dependent: :delete_all
|
61
|
-
|
62
|
-
# rubocop:disable Style/Lambda
|
63
61
|
scope :top_level_messageboards, -> { where(group: nil) }
|
64
62
|
scope :by_messageboard_group, ->(group) { where(group: group.id) }
|
65
63
|
scope :ordered, ->(order = Thredded.messageboards_order) {
|
@@ -83,7 +81,6 @@ module Thredded
|
|
83
81
|
scope :ordered_by_topics_count_desc, ->() {
|
84
82
|
order(topics_count: :desc)
|
85
83
|
}
|
86
|
-
# rubocop:enable Style/Lambda
|
87
84
|
|
88
85
|
# Finds the messageboard by its slug or ID, or raises Thredded::Errors::MessageboardNotFound.
|
89
86
|
# @param slug_or_id [String]
|
data/app/models/thredded/post.rb
CHANGED
@@ -37,6 +37,14 @@ module Thredded
|
|
37
37
|
|
38
38
|
after_commit :auto_follow_and_notify, on: %i[create update]
|
39
39
|
|
40
|
+
# Finds the post by its ID, or raises {Thredded::Errors::PostNotFound}.
|
41
|
+
# @param id [String, Number]
|
42
|
+
# @return [Thredded::Post]
|
43
|
+
# @raise [Thredded::Errors::PostNotFound] if the post with the given ID does not exist.
|
44
|
+
def self.find!(id)
|
45
|
+
find_by(id: id) || fail(Thredded::Errors::PostNotFound)
|
46
|
+
end
|
47
|
+
|
40
48
|
# @param [Integer] per_page
|
41
49
|
# @param [Thredded.user_class] user
|
42
50
|
def page(per_page: self.class.default_per_page, user:)
|
@@ -21,6 +21,14 @@ module Thredded
|
|
21
21
|
after_commit :update_parent_last_user_and_timestamp, on: %i[create destroy]
|
22
22
|
after_commit :notify_users, on: [:create]
|
23
23
|
|
24
|
+
# Finds the post by its ID, or raises {Thredded::Errors::PrivatePostNotFound}.
|
25
|
+
# @param id [String, Number]
|
26
|
+
# @return [Thredded::PrivatePost]
|
27
|
+
# @raise [Thredded::Errors::PrivatePostNotFound] if the post with the given ID does not exist.
|
28
|
+
def self.find!(id)
|
29
|
+
find_by(id: id) || fail(Thredded::Errors::PrivatePostNotFound)
|
30
|
+
end
|
31
|
+
|
24
32
|
# @param [Integer] per_page
|
25
33
|
def page(per_page: self.class.default_per_page)
|
26
34
|
calculate_page(postable.posts, per_page)
|
@@ -38,6 +38,20 @@ module Thredded
|
|
38
38
|
inverse_of: :postable,
|
39
39
|
dependent: :destroy
|
40
40
|
|
41
|
+
# Private topics with that have exactly the given participants.
|
42
|
+
scope :has_exact_participants, ->(users) {
|
43
|
+
private_users = Thredded::PrivateUser.arel_table
|
44
|
+
joins(:private_users)
|
45
|
+
.group(arel_table[:id])
|
46
|
+
.having(
|
47
|
+
Arel::Nodes::And.new(
|
48
|
+
users.map do |user|
|
49
|
+
Arel::Nodes::Count.new([private_users[:user_id].eq(user.id).or(Arel.sql('NULL'))]).eq(1)
|
50
|
+
end
|
51
|
+
).and(Arel::Nodes::Count.new([private_users[:user_id].not_in(users.map(&:id)).or(Arel.sql('NULL'))]).eq(0))
|
52
|
+
)
|
53
|
+
}
|
54
|
+
|
41
55
|
validates_each :users do |model, _attr, users|
|
42
56
|
# Must include the creator + at least one other user
|
43
57
|
if users.length < 2
|
@@ -14,7 +14,7 @@ module Thredded
|
|
14
14
|
scope :search_query, ->(query) { ::Thredded::TopicsSearch.new(query, self).search }
|
15
15
|
|
16
16
|
scope :order_sticky_first, -> { order(sticky: :desc) }
|
17
|
-
scope :order_followed_first,
|
17
|
+
scope :order_followed_first, ->(user) {
|
18
18
|
user_follows = UserTopicFollow.arel_table
|
19
19
|
joins(arel_table.join(user_follows, Arel::Nodes::OuterJoin)
|
20
20
|
.on(user_follows[:topic_id].eq(arel_table[:id])
|
@@ -22,7 +22,7 @@ module Thredded
|
|
22
22
|
.order(Arel::Nodes::Ascending.new(user_follows[:id].eq(nil)))
|
23
23
|
}
|
24
24
|
|
25
|
-
scope :followed_by,
|
25
|
+
scope :followed_by, ->(user) {
|
26
26
|
joins(:user_follows)
|
27
27
|
.where(thredded_user_topic_follows: { user_id: user.id })
|
28
28
|
}
|
@@ -46,12 +46,12 @@ module Thredded
|
|
46
46
|
opt.has_many :thredded_post_moderated_records, foreign_key: 'moderator_id', inverse_of: :moderator
|
47
47
|
end
|
48
48
|
|
49
|
-
scope :left_join_thredded_user_details,
|
49
|
+
scope :left_join_thredded_user_details, -> {
|
50
50
|
users = arel_table
|
51
51
|
user_details = Thredded::UserDetail.arel_table
|
52
52
|
joins(users.join(user_details, Arel::Nodes::OuterJoin)
|
53
53
|
.on(users[:id].eq(user_details[:user_id])).join_sources)
|
54
|
-
|
54
|
+
}
|
55
55
|
end
|
56
56
|
|
57
57
|
def thredded_user_preference
|
@@ -7,6 +7,7 @@
|
|
7
7
|
'data-thredded-post-form' => true,
|
8
8
|
'data-autocomplete-url' => autocomplete_users_path,
|
9
9
|
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
|
10
|
+
'data-thredded-submit-hotkey' => true,
|
10
11
|
} do |form| %>
|
11
12
|
<ul class="thredded--form-list">
|
12
13
|
<%= render 'thredded/posts_common/form/content',
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<%= form.label :content, content_label %>
|
3
3
|
<%= view_hooks.post_form.content_text_area.render self, form: form, content_label: content_label do %>
|
4
4
|
<%= render 'thredded/posts_common/form/before_content', form: form %>
|
5
|
-
<%= form.text_area :content, {rows: 5, required: true} %>
|
5
|
+
<%= form.text_area :content, {rows: 5, required: true, autofocus: !!params[:autofocus_new_post_content]} %>
|
6
6
|
<%= render 'thredded/posts_common/form/after_content', form: form %>
|
7
7
|
<% end %>
|
8
8
|
</li>
|
@@ -6,6 +6,7 @@
|
|
6
6
|
# TODO: only autocomplete users in this private topic
|
7
7
|
'data-autocomplete-url' => autocomplete_users_path,
|
8
8
|
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
|
9
|
+
'data-thredded-submit-hotkey' => true,
|
9
10
|
} do |form| %>
|
10
11
|
|
11
12
|
<ul class="thredded--form-list on-top">
|
@@ -17,6 +18,7 @@
|
|
17
18
|
<li>
|
18
19
|
<%= form.label :user_names, t('thredded.private_topics.form.users_label') %>
|
19
20
|
<%= form.text_area :user_names,
|
21
|
+
required: true,
|
20
22
|
placeholder: t('thredded.private_topics.form.users_placeholder'),
|
21
23
|
'data-thredded-users-select' => true,
|
22
24
|
'data-autocomplete-url' => autocomplete_users_path,
|
@@ -14,10 +14,5 @@
|
|
14
14
|
collection: [private_topic.last_user, *(private_topic.users - [private_topic.last_user])] %>
|
15
15
|
</span>
|
16
16
|
</cite>
|
17
|
-
|
18
|
-
<cite class="thredded--topics--started-by">
|
19
|
-
<%= time_ago private_topic.created_at %>
|
20
|
-
<%= user_link private_topic.user %>
|
21
|
-
</cite>
|
22
17
|
<% end %>
|
23
18
|
|
@@ -5,6 +5,7 @@
|
|
5
5
|
'data-thredded-topic-form' => true,
|
6
6
|
'data-autocomplete-url' => autocomplete_users_path,
|
7
7
|
'data-autocomplete-min-length' => Thredded.autocomplete_min_length,
|
8
|
+
'data-thredded-submit-hotkey' => true,
|
8
9
|
} do |form| %>
|
9
10
|
<ul class="thredded--form-list on-top">
|
10
11
|
<li class="title">
|