thredded 0.10.1 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +30 -10
- data/app/assets/images/favicons/README.md +3 -0
- data/app/assets/images/favicons/amazon.png +0 -0
- data/app/assets/images/favicons/github.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_calendar_128px.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_docs_48px.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_drive_48px.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_forms_48px.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_sheets_48px.png +0 -0
- data/app/assets/images/favicons/google_branding/logo_slides_48px.png +0 -0
- data/app/assets/images/favicons/stackexchange.png +0 -0
- data/app/assets/images/favicons/twitter.png +0 -0
- data/app/assets/images/favicons/wikipedia.png +0 -0
- data/app/assets/javascripts/thredded/components/user_preferences_form.es6 +16 -1
- data/app/assets/stylesheets/thredded/_email.scss +52 -0
- data/app/assets/stylesheets/thredded/_thredded.scss +1 -0
- data/app/assets/stylesheets/thredded/base/_grid.scss +14 -1
- data/app/assets/stylesheets/thredded/base/_typography.scss +4 -0
- data/app/assets/stylesheets/thredded/base/_variables.scss +24 -1
- data/app/assets/stylesheets/thredded/components/_main-section.scss +6 -0
- data/app/assets/stylesheets/thredded/components/_messageboard.scss +4 -1
- data/app/assets/stylesheets/thredded/components/_onebox.scss +284 -0
- data/app/assets/stylesheets/thredded/components/_post.scss +10 -6
- data/app/assets/stylesheets/thredded/components/_topic-header.scss +1 -1
- data/app/assets/stylesheets/thredded/components/_topics.scss +5 -5
- data/app/assets/stylesheets/thredded/layout/_main-navigation.scss +0 -6
- data/app/assets/stylesheets/thredded/layout/_moderation.scss +1 -1
- data/app/commands/thredded/autofollow_users.rb +56 -0
- data/app/controllers/thredded/preferences_controller.rb +2 -0
- data/app/forms/thredded/user_preferences_form.rb +18 -0
- data/app/helpers/thredded/application_helper.rb +3 -3
- data/app/jobs/thredded/auto_follow_and_notify_job.rb +1 -1
- data/app/mailer_previews/thredded/base_mailer_preview.rb +19 -8
- data/app/mailers/thredded/base_mailer.rb +1 -1
- data/app/models/concerns/thredded/post_common.rb +2 -4
- data/app/models/thredded/category.rb +4 -0
- data/app/models/thredded/messageboard.rb +12 -6
- data/app/models/thredded/private_topic.rb +4 -0
- data/app/models/thredded/topic.rb +9 -5
- data/app/models/thredded/user_messageboard_preference.rb +24 -0
- data/app/models/thredded/user_preference.rb +2 -0
- data/app/models/thredded/user_topic_follow.rb +1 -1
- data/app/notifiers/thredded/email_notifier.rb +1 -15
- data/app/views/thredded/messageboard_groups/new.html.erb +15 -13
- data/app/views/thredded/messageboards/_form.html.erb +22 -22
- data/app/views/thredded/messageboards/edit.html.erb +3 -1
- data/app/views/thredded/messageboards/new.html.erb +3 -1
- data/app/views/thredded/moderation/_post.html.erb +1 -1
- data/app/views/thredded/moderation/_user_post.html.erb +1 -1
- data/app/views/thredded/moderation/activity.html.erb +3 -3
- data/app/views/thredded/moderation/history.html.erb +2 -2
- data/app/views/thredded/moderation/pending.html.erb +2 -2
- data/app/views/thredded/moderation/user.html.erb +43 -41
- data/app/views/thredded/moderation/users.html.erb +32 -30
- data/app/views/thredded/post_mailer/post_notification.html.erb +21 -12
- data/app/views/thredded/posts/_content.html.erb +1 -1
- data/app/views/thredded/posts_common/_content.html.erb +1 -3
- data/app/views/thredded/preferences/_form.html.erb +25 -8
- data/app/views/thredded/private_posts/_content.html.erb +1 -1
- data/app/views/thredded/private_topic_mailer/message_notification.html.erb +17 -14
- data/app/views/thredded/private_topics/edit.html.erb +21 -19
- data/app/views/thredded/topics/edit.html.erb +32 -30
- data/app/views/thredded/topics/new.html.erb +8 -6
- data/app/views/thredded/topics/show.html.erb +1 -1
- data/app/views/thredded/users/_post.html.erb +1 -1
- data/bin/rubocop +17 -0
- data/config/locales/en.yml +13 -1
- data/config/locales/es.yml +14 -0
- data/config/locales/pl.yml +13 -0
- data/config/locales/pt-BR.yml +12 -0
- data/config/locales/ru.yml +197 -0
- data/db/migrate/20160329231848_create_thredded.rb +2 -8
- data/db/upgrade_migrations/20161113161801_upgrade_v0_8_to_v0_9.rb +6 -5
- data/db/upgrade_migrations/20170312131417_upgrade_thredded_v0_10_to_v0_11.rb +20 -0
- data/lib/generators/thredded/install/templates/initializer.rb +12 -0
- data/lib/thredded.rb +12 -3
- data/lib/thredded/content_formatter.rb +16 -25
- data/lib/thredded/email_transformer.rb +21 -0
- data/lib/thredded/email_transformer/base.rb +47 -0
- data/lib/thredded/email_transformer/onebox.rb +20 -0
- data/lib/thredded/formatting_demo_content.rb +29 -0
- data/lib/thredded/html_pipeline/kramdown_filter.rb +5 -1
- data/lib/thredded/html_pipeline/onebox_filter.rb +136 -0
- data/lib/thredded/version.rb +1 -1
- metadata +62 -22
- data/app/commands/thredded/autofollow_mentioned_users.rb +0 -31
- data/app/commands/thredded/members_marked_notified.rb +0 -19
- data/app/models/thredded/post_notification.rb +0 -18
@@ -1,13 +1,14 @@
|
|
1
1
|
.thredded--post {
|
2
2
|
position: relative;
|
3
|
-
margin-bottom: $thredded-
|
4
|
-
@include thredded-media-
|
5
|
-
margin-bottom: $thredded-
|
3
|
+
margin-bottom: $thredded-base-spacing;
|
4
|
+
@include thredded-media-desktop-and-up {
|
5
|
+
margin-bottom: $thredded-large-spacing;
|
6
6
|
}
|
7
7
|
}
|
8
8
|
|
9
9
|
.thredded--post--dropdown--toggle {
|
10
10
|
fill: currentColor;
|
11
|
+
box-sizing: content-box;
|
11
12
|
color: $thredded-action-color;
|
12
13
|
display: inline-block;
|
13
14
|
width: 1rem;
|
@@ -15,6 +16,9 @@
|
|
15
16
|
padding: 0.875rem 0.875rem 0.875rem 1.5rem;
|
16
17
|
margin-right: -0.875rem;
|
17
18
|
-webkit-tap-highlight-color: transparent;
|
19
|
+
@include thredded-media-content-breakout {
|
20
|
+
margin-top: -0.75rem;
|
21
|
+
}
|
18
22
|
}
|
19
23
|
|
20
24
|
.thredded--post--dropdown {
|
@@ -69,8 +73,8 @@ form.button_to > .thredded--post--dropdown--actions--item {
|
|
69
73
|
&:active,
|
70
74
|
&:focus,
|
71
75
|
&:hover {
|
72
|
-
background-color: $thredded-
|
73
|
-
color: $thredded-
|
76
|
+
background-color: $thredded-button-background;
|
77
|
+
color: $thredded-button-color;
|
74
78
|
text-decoration: none;
|
75
79
|
cursor: pointer;
|
76
80
|
}
|
@@ -100,7 +104,7 @@ form.button_to > .thredded--post--dropdown--actions--item {
|
|
100
104
|
vertical-align: baseline;
|
101
105
|
width: 1.75rem; // 28px
|
102
106
|
|
103
|
-
@include thredded-media-
|
107
|
+
@include thredded-media-content-breakout {
|
104
108
|
height: 2.25rem; // 36px
|
105
109
|
left: -3rem;
|
106
110
|
position: absolute;
|
@@ -2,7 +2,7 @@
|
|
2
2
|
margin-bottom: $thredded-small-spacing;
|
3
3
|
margin-top: 0;
|
4
4
|
@include thredded-media-desktop-and-up {
|
5
|
-
margin-bottom: $thredded-
|
5
|
+
margin-bottom: $thredded-base-spacing + $thredded-small-spacing;
|
6
6
|
margin-top: $thredded-small-spacing;
|
7
7
|
@include thredded--clearfix;
|
8
8
|
}
|
@@ -2,11 +2,11 @@
|
|
2
2
|
margin-bottom: $thredded-topics-list-gutter-y;
|
3
3
|
position: relative;
|
4
4
|
|
5
|
-
@
|
6
|
-
margin-left:
|
5
|
+
@include thredded-media-content-no-breakout {
|
6
|
+
margin-left: $thredded-topics-topic-posts-counter-width;
|
7
7
|
}
|
8
8
|
@include thredded-media-mobile {
|
9
|
-
margin-right:
|
9
|
+
margin-right: $thredded-topics-topic-follow-icon-width;
|
10
10
|
}
|
11
11
|
}
|
12
12
|
|
@@ -115,7 +115,7 @@
|
|
115
115
|
font-weight: 900;
|
116
116
|
font-size: 0.8rem;
|
117
117
|
height: 2rem;
|
118
|
-
left: -
|
118
|
+
left: -$thredded-topics-topic-posts-counter-width;
|
119
119
|
line-height: 2rem;
|
120
120
|
margin-right: $thredded-base-spacing;
|
121
121
|
position: absolute;
|
@@ -150,7 +150,7 @@
|
|
150
150
|
right: -1.6rem;
|
151
151
|
top: 0;
|
152
152
|
@include thredded-media-mobile {
|
153
|
-
right: -
|
153
|
+
right: -$thredded-topics-topic-posts-counter-width;
|
154
154
|
}
|
155
155
|
}
|
156
156
|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Thredded
|
3
|
+
class AutofollowUsers
|
4
|
+
def initialize(post)
|
5
|
+
@post = post
|
6
|
+
end
|
7
|
+
|
8
|
+
def run
|
9
|
+
new_followers.each do |user, reason|
|
10
|
+
Thredded::UserTopicFollow.create_unless_exists(user.id, post.postable_id, reason)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Array<Thredded.user_class>]
|
15
|
+
def mentioned_users
|
16
|
+
@mentioned_users ||= Thredded::AtNotificationExtractor.new(post).run
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Hash<Thredded.user_class, Symbol]>] a map of users that should get subscribed to their the follow reason.
|
20
|
+
def new_followers
|
21
|
+
result = {}
|
22
|
+
auto_followers.each { |user| result[user] = :auto }
|
23
|
+
exclude_follow_on_mention_opt_outs(mentioned_users).each { |user| result[user] = :mentioned }
|
24
|
+
result.delete(post.user)
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :post
|
31
|
+
|
32
|
+
# Returns the users that have:
|
33
|
+
# UserMessageboardPreference#auto_follow_topics? && UserPreference#auto_follow_topics?
|
34
|
+
# If the `user_preference` for a given does not exist, its default value is used.
|
35
|
+
# @return [Enumerable<Thredded.user_class>]
|
36
|
+
def auto_followers
|
37
|
+
user_board_prefs = post.messageboard.user_messageboard_preferences.each_with_object({}) do |ump, h|
|
38
|
+
h[ump.user] = ump
|
39
|
+
end
|
40
|
+
Thredded.user_class.includes(:thredded_user_preference)
|
41
|
+
.select(Thredded.user_class.primary_key)
|
42
|
+
.find_each(batch_size: 50_000).select do |user|
|
43
|
+
(user_board_prefs[user] ||
|
44
|
+
Thredded::UserMessageboardPreference.new(messageboard: post.messageboard, user: user)).auto_follow_topics?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Enumerable<Thredded.user_class>]
|
49
|
+
def exclude_follow_on_mention_opt_outs(users)
|
50
|
+
users.select do |user|
|
51
|
+
user.thredded_user_preference.follow_topics_on_mention? &&
|
52
|
+
user.thredded_user_messageboard_preferences.in(post.messageboard).follow_topics_on_mention?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -27,6 +27,8 @@ module Thredded
|
|
27
27
|
|
28
28
|
def preferences_params
|
29
29
|
params.fetch(:user_preferences_form, {}).permit(
|
30
|
+
:auto_follow_topics,
|
31
|
+
:messageboard_auto_follow_topics,
|
30
32
|
:follow_topics_on_mention,
|
31
33
|
:messageboard_follow_topics_on_mention,
|
32
34
|
messageboard_notifications_for_followed_topics_attributes: %i(notifier_key id messageboard_id enabled),
|
@@ -9,12 +9,14 @@ module Thredded
|
|
9
9
|
validate :validate_children
|
10
10
|
|
11
11
|
delegate :follow_topics_on_mention, :follow_topics_on_mention=,
|
12
|
+
:auto_follow_topics, :auto_follow_topics=,
|
12
13
|
:messageboard_notifications_for_followed_topics_attributes=,
|
13
14
|
:notifications_for_followed_topics_attributes=,
|
14
15
|
:notifications_for_private_topics_attributes=,
|
15
16
|
to: :user_preference
|
16
17
|
|
17
18
|
delegate :follow_topics_on_mention, :follow_topics_on_mention=,
|
19
|
+
:auto_follow_topics, :auto_follow_topics=,
|
18
20
|
to: :user_messageboard_preference,
|
19
21
|
prefix: :messageboard
|
20
22
|
|
@@ -31,6 +33,14 @@ module Thredded
|
|
31
33
|
return false unless valid?
|
32
34
|
Thredded::UserPreference.transaction do
|
33
35
|
user_preference.save!
|
36
|
+
|
37
|
+
# Update all of the messageboards' auto_follow_topics if the global preference has changed.
|
38
|
+
if user_preference.previous_changes.include?('auto_follow_topics')
|
39
|
+
UserMessageboardPreference.where(user: @user)
|
40
|
+
.update_all(auto_follow_topics: user_preference.auto_follow_topics)
|
41
|
+
user_messageboard_preference.auto_follow_topics_will_change! if messageboard
|
42
|
+
end
|
43
|
+
|
34
44
|
user_messageboard_preference.save! if messageboard
|
35
45
|
end
|
36
46
|
true
|
@@ -55,6 +65,14 @@ module Thredded
|
|
55
65
|
end
|
56
66
|
end
|
57
67
|
|
68
|
+
def update_path
|
69
|
+
if @messageboard
|
70
|
+
Thredded::UrlsHelper.messageboard_preferences_path(@messageboard)
|
71
|
+
else
|
72
|
+
Thredded::UrlsHelper.global_preferences_path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
58
76
|
private
|
59
77
|
|
60
78
|
# @return [Thredded::UserPreference]
|
@@ -19,7 +19,7 @@ module Thredded
|
|
19
19
|
|
20
20
|
def thredded_container_classes
|
21
21
|
['thredded--main-container', content_for(:thredded_page_id)].tap do |classes|
|
22
|
-
classes << 'thredded--is-moderator'
|
22
|
+
classes << 'thredded--is-moderator' unless moderatable_messageboards_ids.empty?
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -93,11 +93,11 @@ module Thredded
|
|
93
93
|
]
|
94
94
|
end
|
95
95
|
|
96
|
-
# @param follow_reason ['manual', 'posted', 'mentioned', nil]
|
96
|
+
# @param follow_reason ['manual', 'posted', 'mentioned', 'auto', nil]
|
97
97
|
def topic_follow_reason_text(follow_reason)
|
98
98
|
if follow_reason
|
99
99
|
# rubocop:disable Metrics/LineLength
|
100
|
-
# i18n-tasks-use t('thredded.topics.following.manual') t('thredded.topics.following.posted') t('thredded.topics.following.mentioned')
|
100
|
+
# i18n-tasks-use t('thredded.topics.following.manual') t('thredded.topics.following.posted') t('thredded.topics.following.mentioned') t('thredded.topics.following.auto')
|
101
101
|
# rubocop:enable Metrics/LineLength
|
102
102
|
t("thredded.topics.following.#{follow_reason}")
|
103
103
|
else
|
@@ -11,18 +11,22 @@ module Thredded
|
|
11
11
|
|
12
12
|
def mock_content(mention_users: [])
|
13
13
|
<<-MARKDOWN
|
14
|
-
#{mention_users.map { |u| "@#{u}" } * ', '}
|
15
|
-
|
16
|
-
|
14
|
+
Hey #{mention_users.map { |u| "@#{u}" } * ', '}!
|
15
|
+
All of the basic [Markdown](https://kramdown.gettalong.org/quickref.html) formatting is supported (powered by [Kramdown](https://kramdown.gettalong.org)).
|
16
|
+
|
17
|
+
Additionally, Markdown is extended to support the following:
|
18
|
+
|
19
|
+
#{Thredded::FormattingDemoContent.parts.join("\n")}
|
20
|
+
MARKDOWN
|
17
21
|
end
|
18
22
|
|
19
23
|
def mock_topic(attr = {})
|
24
|
+
fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
|
20
25
|
Topic.new(
|
21
26
|
attr.reverse_merge(
|
22
27
|
title: 'A test topic',
|
23
28
|
slug: 'a-test-topic',
|
24
29
|
created_at: 3.days.ago,
|
25
|
-
id: 1 + rand(1334),
|
26
30
|
last_user: mock_user,
|
27
31
|
locked: [false, true].sample,
|
28
32
|
messageboard: mock_messageboard,
|
@@ -46,16 +50,16 @@ MARKDOWN
|
|
46
50
|
updated_at: Time.zone.now,
|
47
51
|
user: topic.last_user,
|
48
52
|
)
|
49
|
-
)
|
53
|
+
).tap { |m| mock_post_cache_key! m }
|
50
54
|
end
|
51
55
|
|
52
56
|
def mock_private_topic(attr = {})
|
57
|
+
fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
|
53
58
|
PrivateTopic.new(
|
54
59
|
attr.reverse_merge(
|
55
60
|
title: 'A test private topic',
|
56
61
|
slug: 'a-test-private-topic',
|
57
62
|
created_at: 3.days.ago,
|
58
|
-
id: 1 + rand(1334),
|
59
63
|
last_user: mock_user,
|
60
64
|
posts_count: 1 + rand(42),
|
61
65
|
updated_at: Time.zone.now,
|
@@ -75,17 +79,17 @@ MARKDOWN
|
|
75
79
|
updated_at: Time.zone.now,
|
76
80
|
user: private_topic.last_user,
|
77
81
|
)
|
78
|
-
)
|
82
|
+
).tap { |m| mock_post_cache_key! m }
|
79
83
|
end
|
80
84
|
|
81
85
|
def mock_messageboard(attr = {})
|
86
|
+
fail 'Do not assign ID here or a has_many association might get updated' if attr.key?(:id)
|
82
87
|
Messageboard.new(
|
83
88
|
attr.reverse_merge(
|
84
89
|
name: 'A test messageboard',
|
85
90
|
slug: 'a-test-messageboard',
|
86
91
|
description: 'Test messageboard description',
|
87
92
|
created_at: 1.month.ago,
|
88
|
-
id: 1 + rand(1334),
|
89
93
|
posts_count: rand(1337),
|
90
94
|
topics_count: rand(42),
|
91
95
|
updated_at: Time.zone.now,
|
@@ -102,5 +106,12 @@ MARKDOWN
|
|
102
106
|
)
|
103
107
|
)
|
104
108
|
end
|
109
|
+
|
110
|
+
def mock_post_cache_key!(post)
|
111
|
+
orig_key = post.cache_key
|
112
|
+
post.define_singleton_method :cache_key do
|
113
|
+
orig_key.sub(/new$/, "preview-#{Digest.hexencode(Digest::SHA2.new.digest(content))}")
|
114
|
+
end
|
115
|
+
end
|
105
116
|
end
|
106
117
|
end
|
@@ -12,8 +12,6 @@ module Thredded
|
|
12
12
|
|
13
13
|
delegate :email, to: :user, prefix: true, allow_nil: true
|
14
14
|
|
15
|
-
has_many :post_notifications, as: :post, dependent: :destroy
|
16
|
-
|
17
15
|
validates :content, presence: true
|
18
16
|
|
19
17
|
scope :order_oldest_first, -> { order(created_at: :asc, id: :asc) }
|
@@ -32,8 +30,8 @@ module Thredded
|
|
32
30
|
|
33
31
|
# @param view_context [Object] the context of the rendering view.
|
34
32
|
# @return [String] formatted and sanitized html-safe post content.
|
35
|
-
def filtered_content(view_context, users_provider: ->(names) { readers_from_user_names(names) })
|
36
|
-
Thredded::ContentFormatter.new(view_context, users_provider: users_provider).format_content(content)
|
33
|
+
def filtered_content(view_context, users_provider: ->(names) { readers_from_user_names(names) }, **options)
|
34
|
+
Thredded::ContentFormatter.new(view_context, users_provider: users_provider, **options).format_content(content)
|
37
35
|
end
|
38
36
|
|
39
37
|
def first_post_in_topic?
|
@@ -96,17 +96,23 @@ module Thredded
|
|
96
96
|
last_topic.try(:last_user)
|
97
97
|
end
|
98
98
|
|
99
|
+
def update_last_topic!
|
100
|
+
return if destroyed?
|
101
|
+
self.last_topic = topics.order_recently_posted_first.moderation_state_visible_to_all.first
|
102
|
+
save! if last_topic_id_changed?
|
103
|
+
end
|
104
|
+
|
105
|
+
def normalize_friendly_id(input)
|
106
|
+
Thredded.slugifier.call(input.to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
99
111
|
def slug_candidates
|
100
112
|
[
|
101
113
|
:name,
|
102
114
|
[:name, '-board']
|
103
115
|
]
|
104
116
|
end
|
105
|
-
|
106
|
-
def update_last_topic!
|
107
|
-
return if destroyed?
|
108
|
-
self.last_topic = topics.order_recently_posted_first.moderation_state_visible_to_all.first
|
109
|
-
save! if last_topic_id_changed?
|
110
|
-
end
|
111
117
|
end
|
112
118
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module Thredded
|
3
|
-
class Topic < ActiveRecord::Base
|
3
|
+
class Topic < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
|
4
4
|
include Thredded::TopicCommon
|
5
5
|
include Thredded::ContentModerationState
|
6
6
|
|
@@ -121,10 +121,6 @@ module Thredded
|
|
121
121
|
true
|
122
122
|
end
|
123
123
|
|
124
|
-
def should_generate_new_friendly_id?
|
125
|
-
title_changed?
|
126
|
-
end
|
127
|
-
|
128
124
|
# @return [Thredded::PostModerationRecord, nil]
|
129
125
|
def last_moderation_record
|
130
126
|
first_post.try(:last_moderation_record)
|
@@ -144,6 +140,14 @@ module Thredded
|
|
144
140
|
end
|
145
141
|
end
|
146
142
|
|
143
|
+
def should_generate_new_friendly_id?
|
144
|
+
title_changed?
|
145
|
+
end
|
146
|
+
|
147
|
+
def normalize_friendly_id(input)
|
148
|
+
Thredded.slugifier.call(input.to_s)
|
149
|
+
end
|
150
|
+
|
147
151
|
private
|
148
152
|
|
149
153
|
def slug_candidates
|