diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index 9cd71b7c99c..e8dd79af9e0 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -159,7 +159,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') {
return (
@@ -315,15 +315,22 @@ class MediaGallery extends React.PureComponent {
style.height = height;
}
- const size = media.take(4).size;
+ const size = media.take(4).size;
+ const uncached = media.every(attachment => attachment.get('type') === 'unknown');
if (this.isStandaloneEligible()) {
children =
);
}
- if (visible) {
+ if (uncached) {
+ spoilerButton = (
+
;
} else {
spoilerButton = (
@@ -335,7 +342,7 @@ class MediaGallery extends React.PureComponent {
return (
+
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 6f07778f244..51e3ec037d1 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -18,7 +18,7 @@ const NavigationPanel = () => (
- {profile_directory &&
}
+ {profile_directory &&
}
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 617328613ce..9cb4b74a75c 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -8,6 +8,14 @@
{
"defaultMessage": "An unexpected error occurred.",
"id": "alert.unexpected.message"
+ },
+ {
+ "defaultMessage": "Rate limited",
+ "id": "alert.rate_limited.title"
+ },
+ {
+ "defaultMessage": "Please retry after {retry_time, time, medium}.",
+ "id": "alert.rate_limited.message"
}
],
"path": "app/javascript/mastodon/actions/alerts.json"
@@ -191,6 +199,10 @@
"defaultMessage": "Toggle visibility",
"id": "media_gallery.toggle_visible"
},
+ {
+ "defaultMessage": "Not available",
+ "id": "status.uncached_media_warning"
+ },
{
"defaultMessage": "Sensitive content",
"id": "status.sensitive_warning"
@@ -1130,6 +1142,19 @@
],
"path": "app/javascript/mastodon/features/compose/components/upload.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Are you sure you want to log out?",
+ "id": "confirmations.logout.message"
+ },
+ {
+ "defaultMessage": "Log out",
+ "id": "confirmations.logout.confirm"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json"
+ },
{
"descriptors": [
{
@@ -1218,6 +1243,14 @@
{
"defaultMessage": "Compose new toot",
"id": "navigation_bar.compose"
+ },
+ {
+ "defaultMessage": "Are you sure you want to log out?",
+ "id": "confirmations.logout.message"
+ },
+ {
+ "defaultMessage": "Log out",
+ "id": "confirmations.logout.confirm"
}
],
"path": "app/javascript/mastodon/features/compose/index.json"
@@ -1235,6 +1268,76 @@
],
"path": "app/javascript/mastodon/features/direct_timeline/index.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Follow",
+ "id": "account.follow"
+ },
+ {
+ "defaultMessage": "Unfollow",
+ "id": "account.unfollow"
+ },
+ {
+ "defaultMessage": "Awaiting approval",
+ "id": "account.requested"
+ },
+ {
+ "defaultMessage": "Unblock @{name}",
+ "id": "account.unblock"
+ },
+ {
+ "defaultMessage": "Unmute @{name}",
+ "id": "account.unmute"
+ },
+ {
+ "defaultMessage": "Are you sure you want to unfollow {name}?",
+ "id": "confirmations.unfollow.message"
+ },
+ {
+ "defaultMessage": "Toots",
+ "id": "account.posts"
+ },
+ {
+ "defaultMessage": "Followers",
+ "id": "account.followers"
+ },
+ {
+ "defaultMessage": "Never",
+ "id": "account.never_active"
+ },
+ {
+ "defaultMessage": "Last active",
+ "id": "account.last_status"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/directory/components/account_card.json"
+ },
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Browse profiles",
+ "id": "column.directory"
+ },
+ {
+ "defaultMessage": "Recently active",
+ "id": "directory.recently_active"
+ },
+ {
+ "defaultMessage": "New arrivals",
+ "id": "directory.new_arrivals"
+ },
+ {
+ "defaultMessage": "From {domain} only",
+ "id": "directory.local"
+ },
+ {
+ "defaultMessage": "From known fediverse",
+ "id": "directory.federated"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/directory/index.json"
+ },
{
"descriptors": [
{
@@ -2325,6 +2428,14 @@
},
{
"descriptors": [
+ {
+ "defaultMessage": "Are you sure you want to log out?",
+ "id": "confirmations.logout.message"
+ },
+ {
+ "defaultMessage": "Log out",
+ "id": "confirmations.logout.confirm"
+ },
{
"defaultMessage": "Invite people",
"id": "getting_started.invite"
@@ -2440,6 +2551,10 @@
"defaultMessage": "Lists",
"id": "navigation_bar.lists"
},
+ {
+ "defaultMessage": "Profile directory",
+ "id": "getting_started.directory"
+ },
{
"defaultMessage": "Preferences",
"id": "navigation_bar.preferences"
@@ -2447,10 +2562,6 @@
{
"defaultMessage": "Follows and followers",
"id": "navigation_bar.follows_and_followers"
- },
- {
- "defaultMessage": "Profile directory",
- "id": "navigation_bar.profile_directory"
}
],
"path": "app/javascript/mastodon/features/ui/components/navigation_panel.json"
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 28ea713a322..260b43c53fd 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -16,6 +16,7 @@
"account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you",
"account.hide_reblogs": "Hide boosts from @{name}",
+ "account.last_status": "Last active",
"account.link_verified_on": "Ownership of this link was checked on {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media",
@@ -24,6 +25,7 @@
"account.mute": "Mute @{name}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.muted": "Muted",
+ "account.never_active": "Never",
"account.posts": "Toots",
"account.posts_with_replies": "Toots and replies",
"account.report": "Report @{name}",
@@ -36,6 +38,8 @@
"account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
+ "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
+ "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.",
"alert.unexpected.title": "Oops!",
"autosuggest_hashtag.per_week": "{count} per week",
@@ -49,6 +53,7 @@
"column.blocks": "Blocked users",
"column.community": "Local timeline",
"column.direct": "Direct messages",
+ "column.directory": "Browse profiles",
"column.domain_blocks": "Hidden domains",
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
@@ -99,6 +104,8 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
+ "confirmations.logout.confirm": "Log out",
+ "confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft",
@@ -107,6 +114,10 @@
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
+ "directory.federated": "From known fediverse",
+ "directory.local": "From {domain} only",
+ "directory.new_arrivals": "New arrivals",
+ "directory.recently_active": "Recently active",
"embed.instructions": "Embed this status on your website by copying the code below.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@@ -254,7 +265,6 @@
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences",
- "navigation_bar.profile_directory": "Profile directory",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security",
"notification.favourite": "{name} favourited your status",
@@ -361,6 +371,7 @@
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",
"status.show_thread": "Show thread",
+ "status.uncached_media_warning": "Not available",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"suggestions.dismiss": "Dismiss suggestion",
diff --git a/app/javascript/mastodon/rtl.js b/app/javascript/mastodon/rtl.js
index 00870a15d67..89bed6de888 100644
--- a/app/javascript/mastodon/rtl.js
+++ b/app/javascript/mastodon/rtl.js
@@ -20,6 +20,7 @@ export function isRtl(text) {
text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, '');
text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, '');
text = text.replace(/\s+/g, '');
+ text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
const matches = text.match(rtlChars);
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index fd2180d6f49..dee3c343973 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -507,6 +507,7 @@
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
}
strong {
@@ -515,8 +516,10 @@
&__uses {
flex: 0 0 auto;
- width: 80px;
text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
}
@@ -3449,6 +3452,10 @@ a.status-card.compact:hover {
height: auto;
}
+ &--click-thru {
+ pointer-events: none;
+ }
+
&--hidden {
display: none;
}
@@ -3477,6 +3484,12 @@ a.status-card.compact:hover {
background: rgba($base-overlay-background, 0.8);
}
}
+
+ &:disabled {
+ .spoiler-button__overlay__label {
+ background: rgba($base-overlay-background, 0.5);
+ }
+ }
}
}
diff --git a/app/javascript/styles/mastodon/dashboard.scss b/app/javascript/styles/mastodon/dashboard.scss
index e4564f062cd..c0944d417dd 100644
--- a/app/javascript/styles/mastodon/dashboard.scss
+++ b/app/javascript/styles/mastodon/dashboard.scss
@@ -15,6 +15,8 @@
padding: 20px;
background: lighten($ui-base-color, 4%);
border-radius: 4px;
+ box-sizing: border-box;
+ height: 100%;
}
& > a {
diff --git a/app/javascript/styles/mastodon/footer.scss b/app/javascript/styles/mastodon/footer.scss
index f74c004e995..00d2908832f 100644
--- a/app/javascript/styles/mastodon/footer.scss
+++ b/app/javascript/styles/mastodon/footer.scss
@@ -128,7 +128,7 @@
&:hover,
&:focus,
&:active {
- svg path {
+ svg {
fill: lighten($ui-base-color, 38%);
}
}
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index ac99124ea86..16352340bf6 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -112,6 +112,15 @@ code {
padding: 0.2em 0.4em;
background: darken($ui-base-color, 12%);
}
+
+ li {
+ list-style: disc;
+ margin-left: 18px;
+ }
+ }
+
+ ul.hint {
+ margin-bottom: 15px;
}
span.hint {
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 1c58be8c0f1..cb2ac72d454 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -32,22 +32,23 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
end
def serializable_hash(options = nil)
+ named_contexts = {}
+ context_extensions = {}
options = serialization_options(options)
- serialized_hash = serializer.serializable_hash(options)
+ serialized_hash = serializer.serializable_hash(options.merge(named_contexts: named_contexts, context_extensions: context_extensions))
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options)
- { '@context' => serialized_context }.merge(serialized_hash)
+ { '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
end
private
- def serialized_context
+ def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
- serializer_options = serializer.send(:instance_options) || {}
- named_contexts = [:activitystreams] + serializer._named_contexts.keys + serializer_options.fetch(:named_contexts, {}).keys
- context_extensions = serializer._context_extensions.keys + serializer_options.fetch(:context_extensions, {}).keys
+ named_contexts = [:activitystreams] + named_contexts_map.keys
+ context_extensions = context_extensions_map.keys
named_contexts.each do |key|
context_array << NAMED_CONTEXT_MAP[key]
diff --git a/app/lib/activitypub/serializer.rb b/app/lib/activitypub/serializer.rb
index 07bd8c49461..1fdc7931041 100644
--- a/app/lib/activitypub/serializer.rb
+++ b/app/lib/activitypub/serializer.rb
@@ -27,4 +27,12 @@ class ActivityPub::Serializer < ActiveModel::Serializer
_context_extensions[extension_name] = true
end
end
+
+ def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
+ unless adapter_options&.fetch(:named_contexts, nil).nil?
+ adapter_options[:named_contexts].merge!(_named_contexts)
+ adapter_options[:context_extensions].merge!(_context_extensions)
+ end
+ super(adapter_options, options, adapter_instance)
+ end
end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 224d9066004..4587664b8b4 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -78,7 +78,7 @@ class FeedManager
reblog_key = key(type, account_id, 'reblogs')
# Remove any items past the MAX_ITEMS'th entry in our feed
- redis.zremrangebyrank(timeline_key, '0', (-(FeedManager::MAX_ITEMS + 1)).to_s)
+ redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
# tracking anything after it for deduplication purposes.
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 9d874fe2cad..42ccc6513d1 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -191,6 +191,9 @@ class Request
end
end
+ socks = []
+ addr_by_socket = {}
+
addresses.each do |address|
begin
check_private_address(address)
@@ -200,30 +203,45 @@ class Request
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
- begin
- sock.connect_nonblock(sockaddr)
- rescue IO::WaitWritable
- if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect])
- begin
- sock.connect_nonblock(sockaddr)
- rescue Errno::EISCONN
- # Yippee!
- rescue
- sock.close
- raise
- end
- else
- sock.close
- raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
- end
- end
+ sock.connect_nonblock(sockaddr)
+ # If that hasn't raised an exception, we somehow managed to connect
+ # immediately, close pending sockets and return immediately
+ socks.each(&:close)
return sock
+ rescue IO::WaitWritable
+ socks << sock
+ addr_by_socket[sock] = sockaddr
rescue => e
outer_e = e
end
end
+ until socks.empty?
+ _, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect])
+
+ if available_socks.nil?
+ socks.each(&:close)
+ raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
+ end
+
+ available_socks.each do |sock|
+ socks.delete(sock)
+
+ begin
+ sock.connect_nonblock(addr_by_socket[sock])
+ rescue Errno::EISCONN
+ rescue => e
+ sock.close
+ outer_e = e
+ next
+ end
+
+ socks.each(&:close)
+ return sock
+ end
+ end
+
if outer_e
raise outer_e
else
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 945e3a3c621..135e0a0306f 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -7,14 +7,14 @@
# name :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
-# score :integer
# usable :boolean
# trendable :boolean
# listable :boolean
# reviewed_at :datetime
# requested_review_at :datetime
# last_status_at :datetime
-# last_trend_at :datetime
+# max_score :float
+# max_score_at :datetime
#
class Tag < ApplicationRecord
diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb
index e4ce988c183..e1b92b17571 100644
--- a/app/models/trending_tags.rb
+++ b/app/models/trending_tags.rb
@@ -7,6 +7,8 @@ class TrendingTags
THRESHOLD = 5
LIMIT = 10
REVIEW_THRESHOLD = 3
+ MAX_SCORE_COOLDOWN = 3.days.freeze
+ MAX_SCORE_HALFLIFE = 6.hours.freeze
class << self
include Redisable
@@ -16,14 +18,75 @@ class TrendingTags
increment_historical_use!(tag.id, at_time)
increment_unique_use!(tag.id, account.id, at_time)
- increment_vote!(tag, at_time)
+ increment_use!(tag.id, at_time)
tag.update(last_status_at: Time.now.utc) if tag.last_status_at.nil? || tag.last_status_at < 12.hours.ago
- tag.update(last_trend_at: Time.now.utc) if trending?(tag) && (tag.last_trend_at.nil? || tag.last_trend_at < 12.hours.ago)
+ end
+
+ def update!(at_time = Time.now.utc)
+ tag_ids = redis.smembers("#{KEY}:used:#{at_time.beginning_of_day.to_i}") + redis.zrange(KEY, 0, -1)
+ tags = Tag.where(id: tag_ids.uniq)
+
+ # First pass to calculate scores and update the set
+
+ tags.each do |tag|
+ expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
+ expected = 1.0 if expected.zero?
+ observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
+ max_time = tag.max_score_at
+ max_score = tag.max_score
+ max_score = 0 if max_time.nil? || max_time < (at_time - MAX_SCORE_COOLDOWN)
+
+ score = begin
+ if expected > observed || observed < THRESHOLD
+ 0
+ else
+ ((observed - expected)**2) / expected
+ end
+ end
+
+ if score > max_score
+ max_score = score
+ max_time = at_time
+
+ # Not interested in triggering any callbacks for this
+ tag.update_columns(max_score: max_score, max_score_at: max_time)
+ end
+
+ decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / MAX_SCORE_HALFLIFE.to_f))
+
+ if decaying_score.zero?
+ redis.zrem(KEY, tag.id)
+ else
+ redis.zadd(KEY, decaying_score, tag.id)
+ end
+ end
+
+ users_for_review = User.staff.includes(:account).to_a.select(&:allows_trending_tag_emails?)
+
+ # Second pass to notify about previously unreviewed trends
+
+ tags.each do |tag|
+ current_rank = redis.zrevrank(KEY, tag.id)
+ needs_review_notification = tag.requires_review? && !tag.requested_review?
+ rank_passes_threshold = current_rank.present? && current_rank <= REVIEW_THRESHOLD
+
+ next unless !tag.trendable? && rank_passes_threshold && needs_review_notification
+
+ tag.touch(:requested_review_at)
+
+ users_for_review.each do |user|
+ AdminMailer.new_trending_tag(user.account, tag).deliver_later!
+ end
+ end
+
+ # Trim older items
+
+ redis.zremrangebyrank(KEY, 0, -(LIMIT + 1))
end
def get(limit, filtered: true)
- tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, LIMIT - 1).map(&:to_i)
+ tag_ids = redis.zrevrange(KEY, 0, LIMIT - 1).map(&:to_i)
tags = Tag.where(id: tag_ids)
tags = tags.where(trendable: true) if filtered
@@ -33,8 +96,8 @@ class TrendingTags
end
def trending?(tag)
- rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id)
- rank.present? && rank <= LIMIT
+ rank = redis.zrevrank(KEY, tag.id)
+ rank.present? && rank < LIMIT
end
private
@@ -51,31 +114,10 @@ class TrendingTags
redis.expire(key, EXPIRE_HISTORY_AFTER)
end
- def increment_vote!(tag, at_time)
- key = "#{KEY}:#{at_time.beginning_of_day.to_i}"
- expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f
- expected = 1.0 if expected.zero?
- observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f
-
- if expected > observed || observed < THRESHOLD
- redis.zrem(key, tag.id)
- else
- score = ((observed - expected)**2) / expected
- old_rank = redis.zrevrank(key, tag.id)
-
- redis.zadd(key, score, tag.id)
- request_review!(tag) if (old_rank.nil? || old_rank > REVIEW_THRESHOLD) && redis.zrevrank(key, tag.id) <= REVIEW_THRESHOLD && !tag.trendable? && tag.requires_review? && !tag.requested_review?
- end
-
- redis.expire(key, EXPIRE_TRENDS_AFTER)
- end
-
- def request_review!(tag)
- return unless Setting.trends
-
- tag.touch(:requested_review_at)
-
- User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? }
+ def increment_use!(tag_id, at_time)
+ key = "#{KEY}:used:#{at_time.beginning_of_day.to_i}"
+ redis.sadd(key, tag_id)
+ redis.expire(key, EXPIRE_HISTORY_AFTER)
end
end
end
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 222e17c9944..17df85de31a 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -6,7 +6,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context :security
context_extensions :manually_approves_followers, :featured, :also_known_as,
- :moved_to, :property_value, :hashtag, :emoji, :identity_proof,
+ :moved_to, :property_value, :identity_proof,
:discoverable
attributes :id, :type, :following, :followers,
@@ -138,6 +138,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
end
class TagSerializer < ActivityPub::Serializer
+ context_extensions :hashtag
+
include RoutingHelper
attributes :type, :href, :name
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index 067ba5c3282..f1cebbcd4de 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
class ActivityPub::NoteSerializer < ActivityPub::Serializer
- context_extensions :atom_uri, :conversation, :sensitive,
- :hashtag, :emoji, :focal_point, :blurhash
+ context_extensions :atom_uri, :conversation, :sensitive
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
@@ -152,6 +151,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
class MediaAttachmentSerializer < ActivityPub::Serializer
+ context_extensions :blurhash, :focal_point
+
include RoutingHelper
attributes :type, :media_type, :url, :name, :blurhash
@@ -199,6 +200,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
class TagSerializer < ActivityPub::Serializer
+ context_extensions :hashtag
+
include RoutingHelper
attributes :type, :href, :name
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 902af376c83..85da7e92107 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -61,6 +61,7 @@ class SuspendAccountService < BaseService
return if !@account.local? || @account.user.nil?
if @options[:including_user]
+ @options[:destroy] = true if !@account.user_confirmed? || @account.user_pending?
@account.user.destroy
else
@account.user.disable!
diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml
index 982dc50354e..1d85aa75e01 100644
--- a/app/views/admin/instances/index.html.haml
+++ b/app/views/admin/instances/index.html.haml
@@ -44,15 +44,16 @@
- if !instance.domain_block.noop?
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
- first_item = false
- - if instance.domain_block.reject_media?
- - unless first_item
- •
- = t('admin.domain_blocks.rejecting_media')
- - first_item = false
- - if instance.domain_block.reject_reports?
- - unless first_item
- •
- = t('admin.domain_blocks.rejecting_reports')
+ - unless instance.domain_block.suspend?
+ - if instance.domain_block.reject_media?
+ - unless first_item
+ •
+ = t('admin.domain_blocks.rejecting_media')
+ - first_item = false
+ - if instance.domain_block.reject_reports?
+ - unless first_item
+ •
+ = t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted')
- else
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index c3779d48c46..d54a43c1e8f 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -38,8 +38,10 @@
.table-wrapper
%table.table
%tbody
+ - total = @usage_by_domain.sum(&:last).to_f
+
- @usage_by_domain.each do |(domain, count)|
%tr
%th= domain || site_hostname
- %td= number_to_percentage((count / @tag.history[0][:uses].to_f) * 100)
+ %td= number_to_percentage((count / total) * 100, precision: 1)
%td= number_with_delimiter count
diff --git a/app/views/application/_sidebar.html.haml b/app/views/application/_sidebar.html.haml
index 90c8f9dd1af..33e7c96fe75 100644
--- a/app/views/application/_sidebar.html.haml
+++ b/app/views/application/_sidebar.html.haml
@@ -5,7 +5,7 @@
.hero-widget__text
%p= @instance_presenter.site_short_description.html_safe.presence || @instance_presenter.site_description.html_safe.presence || t('about.generic_description', domain: site_hostname)
-- if Setting.trends
+- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
- trends = TrendingTags.get(3)
- unless trends.empty?
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index 83384d73794..e807c8d8676 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -2,7 +2,7 @@
= t('auth.register')
- content_for :header_tags do
- = render partial: 'shared/og'
+ = render partial: 'shared/og', locals: { description: description_for_sign_up }
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
= render 'shared/error_messages', object: resource
diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml
index 8bb44ca7f79..c14fed56f8b 100644
--- a/app/views/auth/setup/show.html.haml
+++ b/app/views/auth/setup/show.html.haml
@@ -17,7 +17,4 @@
.simple_form
%p.hint= t('auth.setup.email_settings_hint_html', email: content_tag(:strong, @user.email))
-.form-footer
- %ul.no-list
- %li= link_to t('settings.account_settings'), edit_user_registration_path
- %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }
+.form-footer= render 'auth/shared/links'
diff --git a/app/views/auth/shared/_links.html.haml b/app/views/auth/shared/_links.html.haml
index 3c68ccd2220..e6c3f7cca6d 100644
--- a/app/views/auth/shared/_links.html.haml
+++ b/app/views/auth/shared/_links.html.haml
@@ -1,12 +1,18 @@
%ul.no-list
- - if controller_name != 'sessions'
- %li= link_to t('auth.login'), new_session_path(resource_name)
+ - if user_signed_in?
+ %li= link_to t('settings.account_settings'), edit_user_registration_path
+ - else
+ - if controller_name != 'sessions'
+ %li= link_to t('auth.login'), new_user_session_path
- - if devise_mapping.registerable? && controller_name != 'registrations'
- %li= link_to t('auth.register'), available_sign_up_path
+ - if controller_name != 'registrations'
+ %li= link_to t('auth.register'), available_sign_up_path
- - if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
- %li= link_to t('auth.forgot_password'), new_password_path(resource_name)
+ - if controller_name != 'passwords' && controller_name != 'registrations'
+ %li= link_to t('auth.forgot_password'), new_user_password_path
- - if devise_mapping.confirmable? && controller_name != 'confirmations'
- %li= link_to t('auth.didnt_get_confirmation'), new_confirmation_path(resource_name)
+ - if controller_name != 'confirmations'
+ %li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path
+
+ - if user_signed_in? && controller_name != 'setup'
+ %li= link_to t('auth.logout'), destroy_user_session_path, data: { method: :delete }
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index 811080eb4a4..dee99475a21 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -49,7 +49,7 @@
- if account.last_status_at.present?
%time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
- else
- = t('invites.expires_in_prompt')
+ = t('accounts.never_active')
%small= t('accounts.last_active')
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 40f3aa88a73..e992e5563dc 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -36,7 +36,10 @@
- if status.media_attachments.size > 0
%p
- status.media_attachments.each do |a|
- = link_to medium_url(a), medium_url(a)
+ - if status.local?
+ = link_to medium_url(a), medium_url(a)
+ - else
+ = link_to a.remote_url, a.remote_url
%p.status-footer
= link_to l(status.created_at), web_url("statuses/#{status.id}")
diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml
index b246f83a16e..6e2ff31c577 100644
--- a/app/views/settings/deletes/show.html.haml
+++ b/app/views/settings/deletes/show.html.haml
@@ -2,15 +2,25 @@
= t('settings.delete')
= simple_form_for @confirmation, url: settings_delete_path, method: :delete do |f|
- .warning
- %strong
- = fa_icon('warning')
- = t('deletes.warning_title')
- = t('deletes.warning_html')
+ %p.hint= t('deletes.warning.before')
- %p.hint= t('deletes.description_html')
+ %ul.hint
+ - if current_user.confirmed? && current_user.approved?
+ %li.warning-hint= t('deletes.warning.irreversible')
+ %li.warning-hint= t('deletes.warning.username_unavailable')
+ %li.warning-hint= t('deletes.warning.data_removal')
+ %li.warning-hint= t('deletes.warning.caches')
+ - else
+ %li.positive-hint= t('deletes.warning.email_change_html', path: edit_user_registration_path)
+ %li.positive-hint= t('deletes.warning.email_reconfirmation_html', path: new_user_confirmation_path)
+ %li.positive-hint= t('deletes.warning.email_contact_html', email: Setting.site_contact_email)
+ %li.positive-hint= t('deletes.warning.username_available')
- = f.input :password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, hint: t('deletes.confirm_password')
+ %p.hint= t('deletes.warning.more_details_html', terms_path: terms_path)
+
+ %hr.spacer/
+
+ = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password')
.actions
= f.button :button, t('deletes.proceed'), type: :submit, class: 'negative'
diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml
index 67238fc8bf8..576f47a676a 100644
--- a/app/views/shared/_og.html.haml
+++ b/app/views/shared/_og.html.haml
@@ -1,5 +1,5 @@
-- thumbnail = @instance_presenter.thumbnail
-- description = strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
+- thumbnail = @instance_presenter.thumbnail
+- description ||= strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html'))
%meta{ name: 'description', content: description }/
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index 1105f2062f6..89dc2a75dbf 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -42,11 +42,11 @@
- unless @warning.text.blank?
= Formatter.instance.linkify(@warning.text)
- - unless @statuses&.empty?
+ - if !@statuses.nil? && !@statuses.empty?
%p
%strong= t('user_mailer.warning.statuses')
-- unless @statuses&.empty?
+- if !@statuses.nil? && !@statuses.empty?
- @statuses.each_with_index do |status, i|
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
diff --git a/app/views/user_mailer/warning.text.erb b/app/views/user_mailer/warning.text.erb
index 45ad3b64d4b..bb6610c79bd 100644
--- a/app/views/user_mailer/warning.text.erb
+++ b/app/views/user_mailer/warning.text.erb
@@ -7,7 +7,7 @@
<% end %>
<%= @warning.text %>
-<% unless @statuses&.empty? %>
+<% if !@statuses.nil? && !@statuses.empty? %>
<%= t('user_mailer.warning.statuses') %>
<% @statuses.each do |status| %>
diff --git a/app/workers/scheduler/trending_tags_scheduler.rb b/app/workers/scheduler/trending_tags_scheduler.rb
new file mode 100644
index 00000000000..77f0d574757
--- /dev/null
+++ b/app/workers/scheduler/trending_tags_scheduler.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Scheduler::TrendingTagsScheduler
+ include Sidekiq::Worker
+
+ sidekiq_options unique: :until_executed, retry: 0
+
+ def perform
+ TrendingTags.update! if Setting.trends
+ end
+end
diff --git a/config/deploy.rb b/config/deploy.rb
index f0db50788c2..c4133e79469 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-lock '3.11.0'
+lock '3.11.1'
set :repo_url, ENV.fetch('REPO', 'https://github.com/tootsuite/mastodon.git')
set :branch, ENV.fetch('BRANCH', 'master')
diff --git a/config/environments/production.rb b/config/environments/production.rb
index ccf325bf237..00571a35aae 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -83,7 +83,10 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
# E-mails
- config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost') }
+ config.action_mailer.default_options = {
+ from: ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost'),
+ reply_to: ENV['SMTP_REPLY_TO']
+ }
config.action_mailer.smtp_settings = {
:port => ENV['SMTP_PORT'],
diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb
index 329a5fb2c3a..0e69e1d96c4 100644
--- a/config/initializers/active_model_serializers.rb
+++ b/config/initializers/active_model_serializers.rb
@@ -3,22 +3,3 @@ ActiveModelSerializers.config.tap do |config|
end
ActiveSupport::Notifications.unsubscribe(ActiveModelSerializers::Logging::RENDER_EVENT)
-
-class ActiveModel::Serializer::Reflection
- # We monkey-patch this method so that when we include associations in a serializer,
- # the nested serializers can send information about used contexts upwards back to
- # the root. We do this via instance_options because the nesting can be dynamic.
- def build_association(parent_serializer, parent_serializer_options, include_slice = {})
- serializer = options[:serializer]
-
- parent_serializer_options.merge!(named_contexts: serializer._named_contexts, context_extensions: serializer._context_extensions) if serializer.respond_to?(:_named_contexts)
-
- association_options = {
- parent_serializer: parent_serializer,
- parent_serializer_options: parent_serializer_options,
- include_slice: include_slice,
- }
-
- ActiveModel::Serializer::Association.new(self, association_options)
- end
-end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 98783da45fd..56f0fd2cffc 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -58,6 +58,7 @@ en:
media: Media
moved_html: "%{name} has moved to %{new_profile_link}:"
network_hidden: This information is not available
+ never_active: Never
nothing_here: There is nothing here!
people_followed_by: People whom %{name} follows
people_who_follow: People who follow %{name}
@@ -581,6 +582,10 @@ en:
checkbox_agreement_without_rules_html: I agree to the
terms of service
delete_account: Delete account
delete_account_html: If you wish to delete your account, you can
proceed here. You will be asked for confirmation.
+ description:
+ prefix_invited_by_user: "@%{name} invites you to join this server of Mastodon!"
+ prefix_sign_up: Sign up on Mastodon today!
+ suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more!
didnt_get_confirmation: Didn't receive confirmation instructions?
forgot_password: Forgot your password?
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
@@ -634,13 +639,21 @@ en:
x_months: "%{count}mo"
x_seconds: "%{count}s"
deletes:
- bad_password_msg: Nice try, hackers! Incorrect password
+ bad_password_msg: The password you entered was incorrect
confirm_password: Enter your current password to verify your identity
- description_html: This will
permanently, irreversibly remove content from your account and deactivate it. Your username will remain reserved to prevent future impersonations.
proceed: Delete account
success_msg: Your account was successfully deleted
- warning_html: Only deletion of content from this particular server is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases.
- warning_title: Disseminated content availability
+ warning:
+ before: 'Before proceeding, please read these notes carefully:'
+ caches: Content that has been cached by other servers may persist
+ data_removal: Your posts and other data will be permanently removed
+ email_change_html: You can
change your e-mail address without deleting your account
+ email_contact_html: If it still doesn't arrive, you can e-mail
%{email} for help
+ email_reconfirmation_html: If you are not receiving the confirmation e-mail, you can
request it again
+ irreversible: You will not be able to restore or reactivate your account
+ more_details_html: For more details, see the
privacy policy.
+ username_available: Your username will become available again
+ username_unavailable: Your username will remain unavailable
directories:
directory: Profile directory
explanation: Discover users based on their interests
diff --git a/config/puma.rb b/config/puma.rb
index 6a96867d54d..224be790365 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,3 +1,5 @@
+persistent_timeout ENV.fetch('PERSISTENT_TIMEOUT') { 20 }.to_i
+
threads_count = ENV.fetch('MAX_THREADS') { 5 }.to_i
threads threads_count, threads_count
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
index 6ebe450b007..5de25de234b 100644
--- a/config/sidekiq.yml
+++ b/config/sidekiq.yml
@@ -9,6 +9,9 @@
scheduled_statuses_scheduler:
every: '5m'
class: Scheduler::ScheduledStatusesScheduler
+ trending_tags_scheduler:
+ every: '5m'
+ class: Scheduler::TrendingTagsScheduler
media_cleanup_scheduler:
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
class: Scheduler::MediaCleanupScheduler
diff --git a/db/migrate/20190901035623_add_max_score_to_tags.rb b/db/migrate/20190901035623_add_max_score_to_tags.rb
new file mode 100644
index 00000000000..f936e987182
--- /dev/null
+++ b/db/migrate/20190901035623_add_max_score_to_tags.rb
@@ -0,0 +1,6 @@
+class AddMaxScoreToTags < ActiveRecord::Migration[5.2]
+ def change
+ add_column :tags, :max_score, :float
+ add_column :tags, :max_score_at, :datetime
+ end
+end
diff --git a/db/post_migrate/20190901040524_remove_score_from_tags.rb b/db/post_migrate/20190901040524_remove_score_from_tags.rb
new file mode 100644
index 00000000000..a1112700b5d
--- /dev/null
+++ b/db/post_migrate/20190901040524_remove_score_from_tags.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class RemoveScoreFromTags < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def change
+ safety_assured do
+ remove_column :tags, :score, :int
+ remove_column :tags, :last_trend_at, :datetime
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 328506b5028..f15f33bea2c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_08_23_221802) do
+ActiveRecord::Schema.define(version: 2019_09_01_040524) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -677,14 +677,14 @@ ActiveRecord::Schema.define(version: 2019_08_23_221802) do
t.string "name", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.integer "score"
t.boolean "usable"
t.boolean "trendable"
t.boolean "listable"
t.datetime "reviewed_at"
t.datetime "requested_review_at"
t.datetime "last_status_at"
- t.datetime "last_trend_at"
+ t.float "max_score"
+ t.datetime "max_score_at"
t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
end
diff --git a/package.json b/package.json
index 11dbc57a723..895245eedf4 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,7 @@
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
"@babel/plugin-transform-react-jsx-self": "^7.2.0",
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
- "@babel/plugin-transform-runtime": "^7.4.4",
+ "@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.5.4",
@@ -172,7 +172,7 @@
"websocket.js": "^0.1.12"
},
"devDependencies": {
- "babel-eslint": "^10.0.2",
+ "babel-eslint": "^10.0.3",
"babel-jest": "^24.8.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb
index fbfc585cf97..42da2986066 100644
--- a/spec/lib/activitypub/activity/update_spec.rb
+++ b/spec/lib/activitypub/activity/update_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe ActivityPub::Activity::Update do
end
let(:actor_json) do
- ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, key_transform: :camel_lower).as_json
+ ActiveModelSerializers::SerializableResource.new(modified_sender, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter).as_json
end
let(:json) do
diff --git a/spec/models/trending_tags_spec.rb b/spec/models/trending_tags_spec.rb
new file mode 100644
index 00000000000..b6122c99481
--- /dev/null
+++ b/spec/models/trending_tags_spec.rb
@@ -0,0 +1,68 @@
+require 'rails_helper'
+
+RSpec.describe TrendingTags do
+ describe '.record_use!' do
+ pending
+ end
+
+ describe '.update!' do
+ let!(:at_time) { Time.now.utc }
+ let!(:tag1) { Fabricate(:tag, name: 'Catstodon') }
+ let!(:tag2) { Fabricate(:tag, name: 'DogsOfMastodon') }
+ let!(:tag3) { Fabricate(:tag, name: 'OCs') }
+
+ before do
+ allow(Redis.current).to receive(:pfcount) do |key|
+ case key
+ when "activity:tags:#{tag1.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
+ 2
+ when "activity:tags:#{tag1.id}:#{at_time.beginning_of_day.to_i}:accounts"
+ 16
+ when "activity:tags:#{tag2.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
+ 0
+ when "activity:tags:#{tag2.id}:#{at_time.beginning_of_day.to_i}:accounts"
+ 4
+ when "activity:tags:#{tag3.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts"
+ 13
+ end
+ end
+
+ Redis.current.zadd('trending_tags', 0.9, tag3.id)
+ Redis.current.sadd("trending_tags:used:#{at_time.beginning_of_day.to_i}", [tag1.id, tag2.id])
+
+ tag3.update(max_score: 0.9, max_score_at: (at_time - 1.day).beginning_of_day + 12.hours)
+
+ described_class.update!(at_time)
+ end
+
+ it 'calculates and re-calculates scores' do
+ expect(described_class.get(10, filtered: false)).to eq [tag1, tag3]
+ end
+
+ it 'omits hashtags below threshold' do
+ expect(described_class.get(10, filtered: false)).to_not include(tag2)
+ end
+
+ it 'decays scores' do
+ expect(Redis.current.zscore('trending_tags', tag3.id)).to be < 0.9
+ end
+ end
+
+ describe '.trending?' do
+ let(:tag) { Fabricate(:tag) }
+
+ before do
+ 10.times { |i| Redis.current.zadd('trending_tags', i + 1, Fabricate(:tag).id) }
+ end
+
+ it 'returns true if the hashtag is within limit' do
+ Redis.current.zadd('trending_tags', 11, tag.id)
+ expect(described_class.trending?(tag)).to be true
+ end
+
+ it 'returns false if the hashtag is outside the limit' do
+ Redis.current.zadd('trending_tags', 0, tag.id)
+ expect(described_class.trending?(tag)).to be false
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index ab20731ff5d..4d601731d88 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,14 +2,7 @@
# yarn lockfile v1
-"@babel/code-frame@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
- integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
- dependencies:
- "@babel/highlight" "^7.0.0"
-
-"@babel/code-frame@^7.5.5":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
@@ -291,12 +284,7 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
-"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5":
- version "7.4.5"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872"
- integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==
-
-"@babel/parser@^7.5.5":
+"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4", "@babel/parser@^7.4.4", "@babel/parser@^7.4.5", "@babel/parser@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
@@ -657,10 +645,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-runtime@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz#a50f5d16e9c3a4ac18a1a9f9803c107c380bce08"
- integrity sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==
+"@babel/plugin-transform-runtime@^7.5.5":
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.5.5.tgz#a6331afbfc59189d2135b2e09474457a8e3d28bc"
+ integrity sha512-6Xmeidsun5rkwnGfMOp6/z9nSzWpHFNVr2Jx7kwoq4mVatQfQx5S56drBgEHF+XQbKOdIaOiMIINvp/kAwMN+w==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
@@ -819,22 +807,7 @@
"@babel/parser" "^7.4.4"
"@babel/types" "^7.4.4"
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5":
- version "7.4.5"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216"
- integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==
- dependencies:
- "@babel/code-frame" "^7.0.0"
- "@babel/generator" "^7.4.4"
- "@babel/helper-function-name" "^7.1.0"
- "@babel/helper-split-export-declaration" "^7.4.4"
- "@babel/parser" "^7.4.5"
- "@babel/types" "^7.4.4"
- debug "^4.1.0"
- globals "^11.1.0"
- lodash "^4.17.11"
-
-"@babel/traverse@^7.5.5":
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4", "@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5", "@babel/traverse@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
@@ -1737,17 +1710,17 @@ axobject-query@^2.0.2:
dependencies:
ast-types-flow "0.0.7"
-babel-eslint@^10.0.2:
- version "10.0.2"
- resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.2.tgz#182d5ac204579ff0881684b040560fdcc1558456"
- integrity sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==
+babel-eslint@^10.0.3:
+ version "10.0.3"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
+ integrity sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/parser" "^7.0.0"
"@babel/traverse" "^7.0.0"
"@babel/types" "^7.0.0"
- eslint-scope "3.7.1"
eslint-visitor-keys "^1.0.0"
+ resolve "^1.12.0"
babel-jest@^24.8.0:
version "24.8.0"
@@ -3816,14 +3789,6 @@ eslint-plugin-react@~7.14.3:
prop-types "^15.7.2"
resolve "^1.10.1"
-eslint-scope@3.7.1:
- version "3.7.1"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
- integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
- dependencies:
- esrecurse "^4.1.0"
- estraverse "^4.1.1"
-
eslint-scope@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
@@ -9027,10 +8992,10 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
- version "1.11.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
- integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
+resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
+ integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
dependencies:
path-parse "^1.0.6"