diff options
author | Thibaut Girka <thib@sitedethib.com> | 2019-09-05 11:36:41 +0200 |
---|---|---|
committer | Thibaut Girka <thib@sitedethib.com> | 2019-09-05 11:36:41 +0200 |
commit | 5088eb8388fbfcb210a518f918ae5332e6d3979e (patch) | |
tree | 20b9394200b79e7eefc234cc50b0f3650e9afc1d /app | |
parent | 0128509605ed90ee5a29d6af2347ab32bd46aeb9 (diff) | |
parent | e265b8887dbd883bc7ca04832dc67ffe46966889 (diff) |
Merge branch 'master' into glitch-soc/merge-upstream
Diffstat (limited to 'app')
37 files changed, 460 insertions, 116 deletions
diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 39aca2a4b..8bd4e5f8b 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -37,7 +37,8 @@ module Admin def set_usage_by_domain @usage_by_domain = @tag.statuses - .where(visibility: :public) + .with_public_visibility + .excluding_silenced_accounts .where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day))) .joins(:account) .group('accounts.domain') @@ -56,7 +57,7 @@ module Admin scope = scope.unreviewed if filter_params[:review] == 'unreviewed' scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed' scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review' - scope.order(score: :desc) + scope.order(max_score: :desc) end def filter_params diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 1d6e4ec19..4e89446c7 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -5,19 +5,42 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController before_action :set_body_classes before_action :set_pack + before_action :require_unconfirmed! skip_before_action :require_functional! + def new + super + + resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? + end + private def set_pack use_pack 'auth' end + def require_unconfirmed! + redirect_to edit_user_registration_path if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? + end + def set_body_classes @body_classes = 'lighter' end + def after_resending_confirmation_instructions_path_for(_resource_name) + if user_signed_in? + if current_user.confirmed? && current_user.approved? + edit_user_registration_path + else + auth_setup_path + end + else + new_user_session_path + end + end + def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.redirect_uri diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index dd0b25f3e..daacb535b 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -8,4 +8,16 @@ module InstanceHelper def site_hostname @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end + + def description_for_sign_up + prefix = begin + if @invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end + end + + safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + end end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index 3f6f187bc..ffdabe674 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -1,6 +1,7 @@ // This file will be loaded on admin pages, regardless of theme. import { delegate } from 'rails-ujs'; +import ready from '../mastodon/ready'; const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; @@ -31,7 +32,7 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => { }); }); -delegate(document, '#domain_block_severity', 'change', ({ target }) => { +const onDomainBlockSeverityChange = (target) => { const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media'); const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports'); @@ -42,4 +43,11 @@ delegate(document, '#domain_block_severity', 'change', ({ target }) => { if (rejectReportsDiv) { rejectReportsDiv.style.display = (target.value === 'suspend') ? 'none' : 'block'; } +}; + +delegate(document, '#domain_block_severity', 'change', ({ target }) => onDomainBlockSeverityChange(target)); + +ready(() => { + const input = document.getElementById('domain_block_severity'); + if (input) onDomainBlockSeverityChange(input); }); diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index f091d7893..62d613262 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -12,11 +12,11 @@ const Hashtag = ({ hashtag }) => ( #<span>{hashtag.get('name')}</span> </Permalink> - <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> + <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} /> </div> <div className='trends__item__current'> - {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} </div> <div className='trends__item__sparkline'> diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 9cd71b7c9..e8dd79af9 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 ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} target='_blank' style={{ cursor: 'pointer' }} title={attachment.get('description')}> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> @@ -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 = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />); + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />); } - if (visible) { + if (uncached) { + spoilerButton = ( + <button type='button' disabled className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span> + </button> + ); + } else if (visible) { spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />; } else { spoilerButton = ( @@ -335,7 +342,7 @@ class MediaGallery extends React.PureComponent { return ( <div className='media-gallery' style={style} ref={this.handleRef}> - <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> + <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> {spoilerButton} </div> diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index cb23a02ba..50ad74450 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -82,6 +82,43 @@ class AccountCard extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, }; + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleFollow = () => { this.props.onFollow(this.props.account); } @@ -94,6 +131,10 @@ class AccountCard extends ImmutablePureComponent { this.props.onMute(this.props.account); } + setRef = (c) => { + this.node = c; + } + render () { const { account, intl } = this.props; @@ -133,7 +174,7 @@ class AccountCard extends ImmutablePureComponent { </div> </div> - <div className='directory__card__extra'> + <div className='directory__card__extra' ref={this.setRef}> <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> </div> diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 6f07778f2..51e3ec037 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 = () => ( <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> - {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.profile_directory' defaultMessage='Profile directory' /></NavLink>} + {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} <ListPanel /> diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 617328613..9cb4b74a7 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" @@ -192,6 +200,10 @@ "id": "media_gallery.toggle_visible" }, { + "defaultMessage": "Not available", + "id": "status.uncached_media_warning" + }, + { "defaultMessage": "Sensitive content", "id": "status.sensitive_warning" }, @@ -1133,6 +1145,19 @@ { "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": [ + { "defaultMessage": "Media is marked as sensitive", "id": "compose_form.sensitive.marked" }, @@ -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" @@ -1238,6 +1271,76 @@ { "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": [ + { "defaultMessage": "Hidden domains", "id": "column.domain_blocks" }, @@ -2326,6 +2429,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" }, @@ -2441,16 +2552,16 @@ "id": "navigation_bar.lists" }, { + "defaultMessage": "Profile directory", + "id": "getting_started.directory" + }, + { "defaultMessage": "Preferences", "id": "navigation_bar.preferences" }, { "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 28ea713a3..260b43c53 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 00870a15d..89bed6de8 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 fd2180d6f..dee3c3439 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 e4564f062..c0944d417 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 f74c004e9..00d290883 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 ac99124ea..16352340b 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 1c58be8c0..cb2ac72d4 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 07bd8c494..1fdc79310 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 224d90660..4587664b8 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 9d874fe2c..42ccc6513 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 945e3a3c6..135e0a030 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 e4ce988c1..e1b92b175 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 222e17c99..17df85de3 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 067ba5c32..f1cebbcd4 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 902af376c..85da7e921 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 982dc5035..1d85aa75e 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 c3779d48c..d54a43c1e 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 90c8f9dd1..33e7c96fe 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 83384d737..e807c8d86 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 8bb44ca7f..c14fed56f 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 3c68ccd22..e6c3f7cca 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 811080eb4..dee99475a 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 40f3aa88a..e992e5563 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 b246f83a1..6e2ff31c5 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 67238fc8b..576f47a67 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 1105f2062..89dc2a75d 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 45ad3b64d..bb6610c79 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 000000000..77f0d5747 --- /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 |