diff options
Diffstat (limited to 'app')
60 files changed, 491 insertions, 151 deletions
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 3a4382850..442e99089 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -53,11 +53,12 @@ class AccountsController < ApplicationController private def show_pinned_statuses? - [replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none? + [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses default_statuses.tap do |statuses| + statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? statuses.merge!(no_replies_scope) unless replies_requested? end @@ -79,12 +80,15 @@ class AccountsController < ApplicationController Status.without_replies end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id) + end + def set_account @account = Account.find_local!(params[:username]) end def older_url - ::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}") pagination_url(max_id: @statuses.last.id) end @@ -93,7 +97,9 @@ class AccountsController < ApplicationController end def pagination_url(max_id: nil, min_id: nil) - if media_requested? + if tag_requested? + short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) + elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) @@ -110,6 +116,10 @@ class AccountsController < ApplicationController request.path.ends_with?('/with_replies') end + def tag_requested? + request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + end + def filtered_status_page(params) if params[:min_id].present? filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 6c2a5c141..6fdc827cb 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) + statuses.merge!(hashtag_scope) if params[:tagged].present? statuses end @@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController Status.without_reblogs end + def hashtag_scope + Status.tagged_with(Tag.find_by(name: params[:tagged])&.id) + end + def pagination_params(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 1e420b3e7..4e45445df 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :set_pack + before_action :set_body_classes include Localized @@ -16,6 +17,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio private + def set_body_classes + @body_classes = 'admin' + end + def store_current_location store_location_for(:user, request.url) end diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb new file mode 100644 index 000000000..3a3241425 --- /dev/null +++ b/app/controllers/settings/featured_tags_controller.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class Settings::FeaturedTagsController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_featured_tags, only: :index + before_action :set_featured_tag, except: [:index, :create] + before_action :set_most_used_tags, only: :index + + def index + @featured_tag = FeaturedTag.new + end + + def create + @featured_tag = current_account.featured_tags.new(featured_tag_params) + @featured_tag.reset_data + + if @featured_tag.save + redirect_to settings_featured_tags_path + else + set_featured_tags + set_most_used_tags + + render :index + end + end + + def destroy + @featured_tag.destroy! + redirect_to settings_featured_tags_path + end + + private + + def set_featured_tag + @featured_tag = current_account.featured_tags.find(params[:id]) + end + + def set_featured_tags + @featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?) + end + + def set_most_used_tags + @most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) + end + + def featured_tag_params + params.require(:featured_tag).permit(:name) + end +end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 1a0b73d16..76d599f08 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -29,6 +29,6 @@ class Settings::ProfilesController < Settings::BaseController end def set_account - @account = current_user.account + @account = current_account end end diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 780ea64b4..d74db6000 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -2,6 +2,7 @@ # Intentionally does not inherit from BaseController class Settings::SessionsController < ApplicationController + before_action :authenticate_user! before_action :set_session, only: :destroy def destroy diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js index 3f5d7ef46..50cd48a9e 100644 --- a/app/javascript/mastodon/actions/alerts.js +++ b/app/javascript/mastodon/actions/alerts.js @@ -22,7 +22,7 @@ export function clearAlert() { }; }; -export function showAlert(title, message) { +export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) { return { type: ALERT_SHOW, title, @@ -44,6 +44,6 @@ export function showAlertForError(error) { return showAlert(title, message); } else { console.error(error); - return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); + return showAlert(); } } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index a4352faab..0be2a5cd4 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; import { importFetchedAccounts } from './importer'; import { updateTimeline } from './timelines'; import { showAlertForError } from './alerts'; +import { showAlert } from './alerts'; +import { defineMessages } from 'react-intl'; let cancelFetchComposeSuggestionsAccounts; @@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; +const messages = defineMessages({ + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, +}); + export function changeCompose(text) { return { type: COMPOSE_CHANGE, @@ -184,20 +190,32 @@ export function submitComposeFail(error) { export function uploadCompose(files) { return function (dispatch, getState) { - if (getState().getIn(['compose', 'media_attachments']).size > 3) { + const uploadLimit = 4; + const media = getState().getIn(['compose', 'media_attachments']); + const total = Array.from(files).reduce((a, v) => a + v.size, 0); + const progress = new Array(files.length).fill(0); + + if (files.length + media.size > uploadLimit) { + dispatch(showAlert(undefined, messages.uploadErrorLimit)); return; } - dispatch(uploadComposeRequest()); - resizeImage(files[0]).then(file => { - const data = new FormData(); - data.append('file', file); - - return api(getState).post('/api/v1/media', data, { - onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), - }).then(({ data }) => dispatch(uploadComposeSuccess(data))); - }).catch(error => dispatch(uploadComposeFail(error))); + for (const [i, f] of Array.from(files).entries()) { + if (media.size + i > 3) break; + + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + + return api(getState).post('/api/v1/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ data }) => dispatch(uploadComposeSuccess(data))); + }).catch(error => dispatch(uploadComposeFail(error))); + }; }; }; diff --git a/app/javascript/mastodon/components/intersection_observer_article.js b/app/javascript/mastodon/components/intersection_observer_article.js index de2203a4b..e453730ba 100644 --- a/app/javascript/mastodon/components/intersection_observer_article.js +++ b/app/javascript/mastodon/components/intersection_observer_article.js @@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component { } updateStateAfterIntersection = (prevState) => { - if (prevState.isIntersecting && !this.entry.isIntersecting) { + if (prevState.isIntersecting !== false && !this.entry.isIntersecting) { scheduleIdleTask(this.hideIfNotIntersecting); } return { diff --git a/app/javascript/mastodon/containers/compose_container.js b/app/javascript/mastodon/containers/compose_container.js index 5ee1d2f14..7bc7bbaa4 100644 --- a/app/javascript/mastodon/containers/compose_container.js +++ b/app/javascript/mastodon/containers/compose_container.js @@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; +import { fetchCustomEmojis } from '../actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); @@ -17,6 +18,8 @@ if (initialState) { store.dispatch(hydrateStore(initialState)); } +store.dispatch(fetchCustomEmojis()); + export default class TimelineContainer extends React.PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index b6fe770ea..db55ad70b 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -63,7 +63,7 @@ class UploadButton extends ImmutablePureComponent { key={resetFileKey} ref={this.setRef} type='file' - multiple={false} + multiple accept={acceptContentTypes.toArray().join(',')} onChange={this.handleChange} disabled={disabled} diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index e277a73c7..e1f84de27 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -160,7 +160,7 @@ class GettingStarted extends ImmutablePureComponent { {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {multiColumn && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> - <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /></a> · </li> + <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js index e712b2f7d..754477bb9 100644 --- a/app/javascript/mastodon/features/introduction/index.js +++ b/app/javascript/mastodon/features/introduction/index.js @@ -89,7 +89,7 @@ const FrameInteractions = ({ onNext }) => ( </div> <div className='introduction__action'> - <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button> + <button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish toot-orial!' /></button> </div> </div> ); diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index d640033eb..2b7d9c56f 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -124,7 +124,7 @@ class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} + emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />} shouldUpdateScroll={shouldUpdateScroll} /> </Column> diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js index bc6b18664..2e41f784d 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.js +++ b/app/javascript/mastodon/features/ui/components/report_modal.js @@ -97,7 +97,7 @@ class ReportModal extends ImmutablePureComponent { <div className='report-modal__container'> <div className='report-modal__comment'> - <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' /></p> + <p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p> <textarea className='setting-text light' diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index f01c2bf24..93e45678f 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -263,7 +263,7 @@ class UI extends React.PureComponent { this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length === 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 60f481076..eb3a7bbde 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -15,6 +15,15 @@ { "descriptors": [ { + "defaultMessage": "File upload limit exceeded.", + "id": "upload_error.limit" + } + ], + "path": "app/javascript/mastodon/actions/compose.json" + }, + { + "descriptors": [ + { "defaultMessage": "{name} mentioned you", "id": "notification.mention" }, @@ -1275,7 +1284,7 @@ "id": "getting_started.security" }, { - "defaultMessage": "About this instance", + "defaultMessage": "About this server", "id": "navigation_bar.info" }, { @@ -1448,7 +1457,7 @@ "id": "introduction.interactions.favourite.text" }, { - "defaultMessage": "Finish tutorial!", + "defaultMessage": "Finish toot-orial!", "id": "introduction.interactions.action" } ], @@ -1828,7 +1837,7 @@ "id": "column.public" }, { - "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "defaultMessage": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "id": "empty_column.public" } ], @@ -2188,7 +2197,7 @@ "id": "report.target" }, { - "defaultMessage": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "defaultMessage": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "id": "report.hint" }, { @@ -2298,4 +2307,4 @@ ], "path": "app/javascript/mastodon/features/video/index.json" } -] \ No newline at end of file +] diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 3bb157aeb..b079f256d 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -132,7 +132,7 @@ "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.mutes": "You haven't muted any users yet.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", - "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "getting_started.developers": "Developers", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "Favourites", "navigation_bar.filters": "Muted words", "navigation_bar.follow_requests": "Follow requests", - "navigation_bar.info": "About this instance", + "navigation_bar.info": "About this server", "navigation_bar.keyboard_shortcuts": "Hotkeys", "navigation_bar.lists": "Lists", "navigation_bar.misc": "Misc", @@ -281,7 +281,7 @@ "reply_indicator.cancel": "Cancel", "report.forward": "Forward to {target}", "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", - "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", @@ -347,6 +347,7 @@ "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "upload_area.title": "Drag & drop to upload", "upload_button.label": "Add media (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "File upload limit exceeded.", "upload_form.description": "Describe for the visually impaired", "upload_form.focus": "Change preview", "upload_form.undo": "Delete", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index e0cba7764..4bcc11d70 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -132,7 +132,7 @@ "empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。", "empty_column.mutes": "まだ誰もミュートしていません。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", - "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のインスタンスのユーザーをフォローしたりしていっぱいにしましょう", + "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう", "follow_request.authorize": "許可", "follow_request.reject": "拒否", "getting_started.developers": "開発", @@ -228,7 +228,7 @@ "navigation_bar.favourites": "お気に入り", "navigation_bar.filters": "フィルター設定", "navigation_bar.follow_requests": "フォローリクエスト", - "navigation_bar.info": "このインスタンスについて", + "navigation_bar.info": "このサーバーについて", "navigation_bar.keyboard_shortcuts": "ホットキー", "navigation_bar.lists": "リスト", "navigation_bar.logout": "ログアウト", @@ -280,8 +280,8 @@ "relative_time.seconds": "{number}秒前", "reply_indicator.cancel": "キャンセル", "report.forward": "{target} に転送する", - "report.forward_hint": "このアカウントは別のインスタンスに所属しています。通報内容を匿名で転送しますか?", - "report.hint": "通報内容はあなたのインスタンスのモデレーターへ送信されます。通報理由を入力してください。:", + "report.forward_hint": "このアカウントは別のサーバーに所属しています。通報内容を匿名で転送しますか?", + "report.hint": "通報内容はあなたのサーバーのモデレーターへ送信されます。通報理由を入力してください。:", "report.placeholder": "追加コメント", "report.submit": "通報する", "report.target": "{target}さんを通報する", @@ -347,6 +347,7 @@ "ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。", "upload_area.title": "ドラッグ&ドロップでアップロード", "upload_button.label": "メディアを追加 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "アップロードできる上限を超えています。", "upload_form.description": "視覚障害者のための説明", "upload_form.focus": "焦点", "upload_form.undo": "削除", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 9f78c430f..e9e0c768d 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -347,6 +347,7 @@ "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.", "upload_area.title": "Przeciągnij i upuść aby wysłać", "upload_button.label": "Dodaj zawartość multimedialną (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.limit": "Przekroczono limit plików do wysłania.", "upload_form.description": "Wprowadź opis dla niewidomych i niedowidzących", "upload_form.focus": "Dopasuj podgląd", "upload_form.undo": "Usuń", diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js index d1608094f..bbdbc865e 100644 --- a/app/javascript/mastodon/utils/resize_image.js +++ b/app/javascript/mastodon/utils/resize_image.js @@ -31,7 +31,7 @@ const loadImage = inputFile => new Promise((resolve, reject) => { }); const getOrientation = (img, type = 'image/png') => new Promise(resolve => { - if (type !== 'image/jpeg') { + if (!['image/jpeg', 'image/webp'].includes(type)) { resolve(1); return; } diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 63a5c61b8..f4f458cf4 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -288,3 +288,7 @@ border-bottom: 0; } } + +.directory__tag .trends__item__current { + width: auto; +} diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 4e969601b..4dbbaa1e8 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -153,10 +153,15 @@ $content-width: 840px; font-weight: 500; } - .directory__tag a { + .directory__tag > a, + .directory__tag > div { box-shadow: none; } + .directory__tag .table-action-link .fa { + color: inherit; + } + .directory__tag h4 { font-size: 18px; font-weight: 700; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8c1115e76..e29abf4f3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -638,7 +638,6 @@ font-weight: 400; overflow: hidden; text-overflow: ellipsis; - white-space: pre-wrap; padding-top: 2px; color: $primary-text-color; @@ -662,6 +661,7 @@ p { margin-bottom: 20px; + white-space: pre-wrap; &:last-child { margin-bottom: 0; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index c97337e4e..1eaf30c5b 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -269,7 +269,8 @@ box-sizing: border-box; margin-bottom: 10px; - a { + & > a, + & > div { display: flex; align-items: center; justify-content: space-between; @@ -279,7 +280,9 @@ text-decoration: none; color: inherit; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + } + & > a { &:hover, &:active, &:focus { @@ -287,7 +290,7 @@ } } - &.active a { + &.active > a { background: $ui-highlight-color; cursor: default; } diff --git a/app/lib/activity_tracker.rb b/app/lib/activity_tracker.rb index 5b4972674..ae3c11b6a 100644 --- a/app/lib/activity_tracker.rb +++ b/app/lib/activity_tracker.rb @@ -4,6 +4,8 @@ class ActivityTracker EXPIRE_AFTER = 90.days.seconds class << self + include Redisable + def increment(prefix) key = [prefix, current_week].join(':') @@ -20,10 +22,6 @@ class ActivityTracker private - def redis - Redis.current - end - def current_week Time.zone.today.cweek end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 87318fb1c..919678618 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -2,6 +2,7 @@ class ActivityPub::Activity include JsonLdHelper + include Redisable def initialize(json, account, **options) @json = json @@ -70,10 +71,6 @@ class ActivityPub::Activity @object_uri ||= value_or_id(@object) end - def redis - Redis.current - end - def distribute(status) crawl_links(status) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index a1b186f1c..4bc75dae8 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -4,6 +4,7 @@ require 'singleton' class FeedManager include Singleton + include Redisable MAX_ITEMS = 400 @@ -35,7 +36,7 @@ class FeedManager def unpush_from_home(account, status) return false unless remove_from_feed(:home, account.id, status) - Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -54,7 +55,7 @@ class FeedManager def unpush_from_list(list, status) return false unless remove_from_feed(:list, list.id, status) - Redis.current.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) + redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) true end @@ -143,10 +144,6 @@ class FeedManager private - def redis - Redis.current - end - def push_update_required?(timeline_id) redis.exists("subscribed:#{timeline_id}") end diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 05fd9eeb1..0653214f5 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -99,7 +99,7 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = Extractor.extract_entities_with_indices(html, extract_url_without_protocol: false) + entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) if accounts.is_a?(Hash) options = accounts @@ -199,6 +199,53 @@ class Formatter result.flatten.join end + UNICODE_ESCAPE_BLACKLIST_RE = /\p{Z}|\p{P}/ + + def utf8_friendly_extractor(text, options = {}) + old_to_new_index = [0] + + escaped = text.chars.map do |c| + output = begin + if c.ord.to_s(16).length > 2 && UNICODE_ESCAPE_BLACKLIST_RE.match(c).nil? + CGI.escape(c) + else + c + end + end + + old_to_new_index << old_to_new_index.last + output.length + + output + end.join + + # Note: I couldn't obtain list_slug with @user/list-name format + # for mention so this requires additional check + special = Extractor.extract_urls_with_indices(escaped, options).map do |extract| + # exactly one of :url, :hashtag, :screen_name, :cashtag keys is present + key = (extract.keys & [:url, :hashtag, :screen_name, :cashtag]).first + + new_indices = [ + old_to_new_index.find_index(extract[:indices].first), + old_to_new_index.find_index(extract[:indices].last), + ] + + has_prefix_char = [:hashtag, :screen_name, :cashtag].include?(key) + value_indices = [ + new_indices.first + (has_prefix_char ? 1 : 0), # account for #, @ or $ + new_indices.last - 1, + ] + + next extract.merge( + :indices => new_indices, + key => text[value_indices.first..value_indices.last] + ) + end + + standard = Extractor.extract_entities_with_indices(text, options) + + Extractor.remove_overlapping_entities(special + standard) + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener' } diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb index c5933f3ad..db70f1998 100644 --- a/app/lib/ostatus/activity/base.rb +++ b/app/lib/ostatus/activity/base.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class OStatus::Activity::Base + include Redisable + def initialize(xml, account = nil, **options) @xml = xml @account = account @@ -66,8 +68,4 @@ class OStatus::Activity::Base Status.find_by(uri: uri) end end - - def redis - Redis.current - end end diff --git a/app/lib/potential_friendship_tracker.rb b/app/lib/potential_friendship_tracker.rb index 017a9748d..188aa4a27 100644 --- a/app/lib/potential_friendship_tracker.rb +++ b/app/lib/potential_friendship_tracker.rb @@ -11,6 +11,8 @@ class PotentialFriendshipTracker }.freeze class << self + include Redisable + def record(account_id, target_account_id, action) return if account_id == target_account_id @@ -31,11 +33,5 @@ class PotentialFriendshipTracker return [] if account_ids.empty? Account.searchable.where(id: account_ids) end - - private - - def redis - Redis.current - end end end diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb index e352000c3..7c0d60379 100644 --- a/app/models/account_domain_block.rb +++ b/app/models/account_domain_block.rb @@ -12,6 +12,7 @@ class AccountDomainBlock < ApplicationRecord include Paginable + include DomainNormalizable belongs_to :account validates :domain, presence: true, uniqueness: { scope: :account_id } diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 4e730451a..3ab8a0daa 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -56,5 +56,6 @@ module AccountAssociations # Hashtags has_and_belongs_to_many :tags + has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account end end diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb index 2d5ebfca3..5fff3ef5d 100644 --- a/app/models/concerns/account_avatar.rb +++ b/app/models/concerns/account_avatar.rb @@ -3,7 +3,7 @@ module AccountAvatar extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes class_methods do diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb index 067e166eb..a748fdff7 100644 --- a/app/models/concerns/account_header.rb +++ b/app/models/concerns/account_header.rb @@ -3,7 +3,7 @@ module AccountHeader extend ActiveSupport::Concern - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 2.megabytes MAX_PIXELS = 750_000 # 1500x500px diff --git a/app/models/concerns/domain_normalizable.rb b/app/models/concerns/domain_normalizable.rb index dff3e5414..fb84058fc 100644 --- a/app/models/concerns/domain_normalizable.rb +++ b/app/models/concerns/domain_normalizable.rb @@ -10,6 +10,6 @@ module DomainNormalizable private def normalize_domain - self.domain = TagManager.instance.normalize_domain(domain) + self.domain = TagManager.instance.normalize_domain(domain&.strip) end end diff --git a/app/models/concerns/redisable.rb b/app/models/concerns/redisable.rb new file mode 100644 index 000000000..c6cf97359 --- /dev/null +++ b/app/models/concerns/redisable.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Redisable + extend ActiveSupport::Concern + + private + + def redis + Redis.current + end +end diff --git a/app/models/export.rb b/app/models/export.rb index a2520e9c2..fc4bb6964 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'csv' class Export diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb new file mode 100644 index 000000000..b5a10ad2d --- /dev/null +++ b/app/models/featured_tag.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: featured_tags +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# tag_id :bigint(8) +# statuses_count :bigint(8) default(0), not null +# last_status_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class FeaturedTag < ApplicationRecord + belongs_to :account, inverse_of: :featured_tags, required: true + belongs_to :tag, inverse_of: :featured_tags, required: true + + delegate :name, to: :tag, allow_nil: true + + validates :name, presence: true + validate :validate_featured_tags_limit, on: :create + + def name=(str) + self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s) + end + + def increment(timestamp) + update(statuses_count: statuses_count + 1, last_status_at: timestamp) + end + + def decrement(deleted_status_id) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) + end + + def reset_data + self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at + end + + private + + def validate_featured_tags_limit + errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10 + end +end diff --git a/app/models/feed.rb b/app/models/feed.rb index 5bce88f25..0e8943ff8 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Feed + include Redisable + def initialize(type, id) @type = type @id = id @@ -27,8 +29,4 @@ class Feed def key FeedManager.instance.key(@type, @id) end - - def redis - Redis.current - end end diff --git a/app/models/import.rb b/app/models/import.rb index 55e970b0d..a7a0d8065 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,20 +13,30 @@ # data_file_size :integer # data_updated_at :datetime # account_id :bigint(8) not null +# overwrite :boolean default(FALSE), not null # class Import < ApplicationRecord - FILE_TYPES = ['text/plain', 'text/csv'].freeze + FILE_TYPES = %w(text/plain text/csv).freeze + MODES = %i(merge overwrite).freeze self.inheritance_column = false belongs_to :account - enum type: [:following, :blocking, :muting] + enum type: [:following, :blocking, :muting, :domain_blocking] validates :type, presence: true has_attached_file :data validates_attachment_content_type :data, content_type: FILE_TYPES validates_attachment_presence :data + + def mode + overwrite? ? :overwrite : :merge + end + + def mode=(str) + self.overwrite = str.to_sym == :overwrite + end end diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 601b14223..81397a18e 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -25,11 +25,11 @@ class MediaAttachment < ApplicationRecord enum type: [:image, :gifv, :video, :audio, :unknown] - IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'].freeze + IMAGE_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].freeze VIDEO_FILE_EXTENSIONS = ['.webm', '.mp4', '.m4v', '.mov'].freeze AUDIO_FILE_EXTENSIONS = ['.mp3', '.m4a', '.wav', '.ogg'].freeze - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/mp4', 'audio/vnd.wav', 'audio/wav', 'audio/x-wav', 'audio/x-wave', 'audio/ogg',].freeze @@ -105,8 +105,8 @@ class MediaAttachment < ApplicationRecord convert_options: { all: '-quality 90 -strip' } validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES + AUDIO_MIME_TYPES - validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video? - validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video? + validates_attachment_size :file, less_than: IMAGE_LIMIT, unless: :video_or_gifv? + validates_attachment_size :file, less_than: VIDEO_LIMIT, if: :video_or_gifv? remotable_attachment :file, VIDEO_LIMIT include Attachmentable @@ -129,6 +129,10 @@ class MediaAttachment < ApplicationRecord file.blank? && remote_url.present? end + def video_or_gifv? + video? || gifv? + end + def to_param shortcode end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index a792b352b..f26ea0c74 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -25,7 +25,7 @@ # class PreviewCard < ApplicationRecord - IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze + IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze LIMIT = 1.megabytes self.inheritance_column = false diff --git a/app/models/tag.rb b/app/models/tag.rb index 99830ae92..4373e967b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -14,6 +14,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :accounts has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' + has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' @@ -23,6 +24,7 @@ class Tag < ApplicationRecord scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } delegate :accounts_count, :accounts_count=, diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 3a8be2164..148535c21 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -7,6 +7,8 @@ class TrendingTags THRESHOLD = 5 class << self + include Redisable + def record_use!(tag, account, at_time = Time.now.utc) return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot? @@ -59,9 +61,5 @@ class TrendingTags @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) end - - def redis - Redis.current - end end end diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 859ef0d14..cc8b9a4d4 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -52,6 +52,14 @@ class ManifestSerializer < ActiveModel::Serializer end def share_target - { url_template: 'share?title={title}&text={text}&url={url}' } + { + url_template: 'share?title={title}&text={text}&url={url}', + action: 'share', + params: { + title: 'title', + text: 'text', + url: 'url', + }, + } end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 487456f3a..5e3308428 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -212,7 +212,7 @@ class ActivityPub::ProcessAccountService < BaseService end def clear_tombstones! - Tombstone.delete_all(account_id: @account.id) + Tombstone.where(account_id: @account.id).delete_all end def protocol_changed? diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 61c408926..d78f506c6 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -2,6 +2,7 @@ class BatchedRemoveStatusService < BaseService include StreamEntryRenderer + include Redisable # Delete given statuses and reblogs of them # Dispatch PuSH updates of the deleted statuses, but only local ones @@ -120,10 +121,6 @@ class BatchedRemoveStatusService < BaseService end end - def redis - Redis.current - end - def build_xml(stream_entry) return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id) diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 9d36a1449..92d8c864a 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class FollowService < BaseService + include Redisable + # Follow a remote user, notify remote user about the follow # @param [Account] source_account From which to follow # @param [String, Account] uri User URI to follow in the form of username@domain (or account record) @@ -67,10 +69,6 @@ class FollowService < BaseService follow end - def redis - Redis.current - end - def build_follow_request_xml(follow_request) OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.follow_request_salmon(follow_request)) end diff --git a/app/services/import_service.rb b/app/services/import_service.rb new file mode 100644 index 000000000..3f558626e --- /dev/null +++ b/app/services/import_service.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'csv' + +class ImportService < BaseService + ROWS_PROCESSING_LIMIT = 20_000 + + def call(import) + @import = import + @account = @import.account + @data = CSV.new(import_data).reject(&:blank?) + + case @import.type + when 'following' + import_follows! + when 'blocking' + import_blocks! + when 'muting' + import_mutes! + when 'domain_blocking' + import_domain_blocks! + end + end + + private + + def import_follows! + import_relationships!('follow', 'unfollow', @account.following, follow_limit) + end + + def import_blocks! + import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT) + end + + def import_mutes! + import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT) + end + + def import_domain_blocks! + items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + @account.domain_blocks.find_each do |domain_block| + if presence_hash[domain_block.domain] + items.delete(domain_block.domain) + else + @account.unblock_domain!(domain_block.domain) + end + end + end + + items.each do |domain| + @account.block_domain!(domain) + end + + AfterAccountDomainBlockWorker.push_bulk(items) do |domain| + [@account.id, domain] + end + end + + def import_relationships!(action, undo_action, overwrite_scope, limit) + items = @data.take(limit).map { |row| row.first.strip } + + if @import.overwrite? + presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true } + + overwrite_scope.find_each do |target_account| + if presence_hash[target_account.acct] + items.delete(target_account.acct) + else + Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action) + end + end + end + + Import::RelationshipWorker.push_bulk(items) do |acct| + [@account.id, acct, action] + end + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def follow_limit + FollowLimitValidator.limit_for_account(@account) + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 5d431c42a..cfb266fbb 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PostStatusService < BaseService + include Redisable + MIN_SCHEDULE_OFFSET = 5.minutes.freeze # Post a text status update, fetch and notify remote users mentioned @@ -115,10 +117,6 @@ class PostStatusService < BaseService ProcessHashtagsService.new end - def redis - Redis.current - end - def scheduled? @scheduled_at.present? end diff --git a/app/services/process_hashtags_service.rb b/app/services/process_hashtags_service.rb index cf7471c98..d5ec076a8 100644 --- a/app/services/process_hashtags_service.rb +++ b/app/services/process_hashtags_service.rb @@ -2,12 +2,22 @@ class ProcessHashtagsService < BaseService def call(status, tags = []) - tags = Extractor.extract_hashtags(status.text) if status.local? + tags = Extractor.extract_hashtags(status.text) if status.local? + records = [] tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| tag = Tag.where(name: name).first_or_create(name: name) + status.tags << tag + records << tag + TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility? end + + return unless status.public_visibility? || status.unlisted_visibility? + + status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag| + featured_tag.increment(status.created_at) + end end end diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb index 4bee86c8a..99c8e6cbb 100644 --- a/app/services/remove_status_service.rb +++ b/app/services/remove_status_service.rb @@ -2,6 +2,7 @@ class RemoveStatusService < BaseService include StreamEntryRenderer + include Redisable def call(status, **options) @payload = Oj.dump(event: :delete, payload: status.id.to_s) @@ -56,7 +57,7 @@ class RemoveStatusService < BaseService def remove_from_affected @mentions.map(&:account).select(&:local?).each do |account| - Redis.current.publish("timeline:#{account.id}", @payload) + redis.publish("timeline:#{account.id}", @payload) end end @@ -131,26 +132,30 @@ class RemoveStatusService < BaseService end def remove_from_hashtags + @account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag| + featured_tag.decrement(@status.id) + end + return unless @status.public_visibility? @tags.each do |hashtag| - Redis.current.publish("timeline:hashtag:#{hashtag}", @payload) - Redis.current.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? + redis.publish("timeline:hashtag:#{hashtag}", @payload) + redis.publish("timeline:hashtag:#{hashtag}:local", @payload) if @status.local? end end def remove_from_public return unless @status.public_visibility? - Redis.current.publish('timeline:public', @payload) - Redis.current.publish('timeline:public:local', @payload) if @status.local? + redis.publish('timeline:public', @payload) + redis.publish('timeline:public:local', @payload) if @status.local? end def remove_from_media return unless @status.public_visibility? - Redis.current.publish('timeline:public:media', @payload) - Redis.current.publish('timeline:public:local:media', @payload) if @status.local? + redis.publish('timeline:public:media', @payload) + redis.publish('timeline:public:local:media', @payload) if @status.local? end def remove_from_direct @@ -159,8 +164,4 @@ class RemoveStatusService < BaseService end Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local? end - - def redis - Redis.current - end end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 0ee9dd7de..0da69728f 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -63,4 +63,17 @@ - @endorsed_accounts.each do |account| = account_link_to account + - @account.featured_tags.order(statuses_count: :desc).each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + = link_to short_account_tag_path(@account, featured_tag.tag) do + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true + = render 'application/sidebar' diff --git a/app/views/admin/change_emails/show.html.haml b/app/views/admin/change_emails/show.html.haml index 6febef9b1..6ff0d785e 100644 --- a/app/views/admin/change_emails/show.html.haml +++ b/app/views/admin/change_emails/show.html.haml @@ -3,7 +3,7 @@ = simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f| .fields-group - = f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email') + = f.input :email, wrapper: :with_label, hint: false, disabled: true, label: t('admin.accounts.change_email.current_email') .fields-group = f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email') diff --git a/app/views/settings/featured_tags/index.html.haml b/app/views/settings/featured_tags/index.html.haml new file mode 100644 index 000000000..5f69517f3 --- /dev/null +++ b/app/views/settings/featured_tags/index.html.haml @@ -0,0 +1,27 @@ +- content_for :page_title do + = t('settings.featured_tags') + += simple_form_for @featured_tag, url: settings_featured_tags_path do |f| + = render 'shared/error_messages', object: @featured_tag + + .fields-group + = f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ') + + .actions + = f.button :button, t('featured_tags.add_new'), type: :submit + +%hr.spacer/ + +- @featured_tags.each do |featured_tag| + .directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil } + %div + %h4 + = fa_icon 'hashtag' + = featured_tag.name + %small + - if featured_tag.last_status_at.nil? + = t('accounts.nothing_here') + - else + %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at + = table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } + .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true diff --git a/app/views/settings/imports/show.html.haml b/app/views/settings/imports/show.html.haml index 4512fc714..7bb4beb01 100644 --- a/app/views/settings/imports/show.html.haml +++ b/app/views/settings/imports/show.html.haml @@ -5,8 +5,11 @@ .field-group = f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface') - .field-group - = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data') + .fields-group.fields-row__column.fields-row__column-6 + = f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' .actions = f.button :button, t('imports.upload'), type: :submit diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb index 1dd8bf8fb..e9db20a46 100644 --- a/app/workers/import/relationship_worker.rb +++ b/app/workers/import/relationship_worker.rb @@ -13,11 +13,17 @@ class Import::RelationshipWorker case relationship when 'follow' - FollowService.new.call(from_account, target_account.acct) + FollowService.new.call(from_account, target_account) + when 'unfollow' + UnfollowService.new.call(from_account, target_account) when 'block' BlockService.new.call(from_account, target_account) + when 'unblock' + UnblockService.new.call(from_account, target_account) when 'mute' MuteService.new.call(from_account, target_account) + when 'unmute' + UnmuteService.new.call(from_account, target_account) end rescue ActiveRecord::RecordNotFound true diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb index aeb221cf6..dfa71b29e 100644 --- a/app/workers/import_worker.rb +++ b/app/workers/import_worker.rb @@ -1,44 +1,14 @@ # frozen_string_literal: true -require 'csv' - class ImportWorker include Sidekiq::Worker sidekiq_options queue: 'pull', retry: false - attr_reader :import - def perform(import_id) - @import = Import.find(import_id) - - Import::RelationshipWorker.push_bulk(import_rows) do |row| - [@import.account_id, row.first, relationship_type] - end - - @import.destroy - end - - private - - def import_contents - Paperclip.io_adapters.for(@import.data).read - end - - def relationship_type - case @import.type - when 'following' - 'follow' - when 'blocking' - 'block' - when 'muting' - 'mute' - end - end - - def import_rows - rows = CSV.new(import_contents).reject(&:blank?) - rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following' - rows + import = Import.find(import_id) + ImportService.new.call(import) + ensure + import&.destroy end end diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb index cd2273418..bf5e20757 100644 --- a/app/workers/scheduler/feed_cleanup_scheduler.rb +++ b/app/workers/scheduler/feed_cleanup_scheduler.rb @@ -2,6 +2,7 @@ class Scheduler::FeedCleanupScheduler include Sidekiq::Worker + include Redisable sidekiq_options unique: :until_executed, retry: 0 @@ -57,8 +58,4 @@ class Scheduler::FeedCleanupScheduler def feed_manager FeedManager.instance end - - def redis - Redis.current - end end |