diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 56229fd05c..6ee784b921 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -47,7 +47,7 @@ class AccountsController < ApplicationController end def default_statuses - @account.statuses.where(visibility: [:public, :unlisted]) + @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted]) end def only_media_scope diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 8e0f9de2ee..e65dbb7d38 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -32,7 +32,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_replies @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses - @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) + @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted, :public_unlisted]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 00d069cdfb..37032e6ba4 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'fastimage' + module Admin class CustomEmojisController < BaseController def index @@ -18,7 +20,11 @@ module Admin def create authorize :custom_emoji, :create? + image_size = FastImage.size(params[:custom_emoji][:image]) + @custom_emoji = CustomEmoji.new(resource_params) + @custom_emoji.image_width = image_size[0] + @custom_emoji.image_height = image_size[1] if @custom_emoji.save log_action :create, @custom_emoji diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 210fbbbb49..9d2d817d09 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -3,7 +3,6 @@ class Api::BaseController < ApplicationController DEFAULT_STATUSES_LIMIT = 20 DEFAULT_ACCOUNTS_LIMIT = 40 - DEFAULT_EMOJI_REACTION_LIMIT = 10 include RateLimitHeaders include AccessTokenTrackingConcern diff --git a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb index 88a3ac4644..1b56033bc4 100644 --- a/app/controllers/api/v1/statuses/emoji_reactions_controller.rb +++ b/app/controllers/api/v1/statuses/emoji_reactions_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:emoji_reactions' } before_action :require_user! - before_action :set_status, only: %i(create update) + before_action :set_status, only: %i(create update destroy) before_action :set_status_without_authorize, only: [:destroy] def create @@ -18,7 +18,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController end def destroy - emoji = params[:emoji] + emoji = params[:emoji] || params[:id] if emoji shortcode, domain = emoji.split('@') @@ -42,7 +42,7 @@ class Api::V1::Statuses::EmojiReactionsController < Api::BaseController def create_private(emoji) count = EmojiReaction.where(account: current_account, status: @status).count - if count >= DEFAULT_EMOJI_REACTION_LIMIT + if count >= EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT bad_request return end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 4b545f9826..988304a84d 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def paginated_statuses - Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id( + Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted, :public_unlisted]).paginate_by_max_id( limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 08020a65a2..7a2b9b9ed0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -122,6 +122,8 @@ module ApplicationHelper fa_icon('globe', title: I18n.t('statuses.visibilities.public')) elsif status.unlisted_visibility? fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) + elsif status.public_unlisted_visibility? + fa_icon('cloud', title: I18n.t('statuses.visibilities.public_unlisted')) elsif status.private_visibility? || status.limited_visibility? fa_icon('lock', title: I18n.t('statuses.visibilities.private')) elsif status.direct_visibility? @@ -199,7 +201,7 @@ module ApplicationHelper text: [params[:title], params[:text], params[:url]].compact.join(' '), } - permit_visibilities = %w(public unlisted private direct) + permit_visibilities = %w(public unlisted public_unlisted private direct) default_privacy = current_account&.user&.setting_default_privacy permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present? state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility] diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d1e3fddafe..2b009da391 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -98,6 +98,8 @@ module StatusesHelper fa_icon 'globe fw' when 'unlisted' fa_icon 'unlock fw' + when 'public_unlisted' + fa_icon 'cloud fw' when 'private' fa_icon 'lock fw' when 'direct' diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 7d598ef17b..78cbd22eae 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -55,6 +55,7 @@ export const defaultMediaVisibility = (status) => { const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, @@ -506,6 +507,7 @@ class Status extends ImmutablePureComponent { const visibilityIconInfo = { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 282892809c..0a1aeb859b 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -248,8 +248,8 @@ class StatusActionBar extends ImmutablePureComponent { const { signedIn, permissions } = this.context.identity; const anonymousAccess = !signedIn; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); + const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -369,13 +369,17 @@ class StatusActionBar extends ImmutablePureComponent { ); + const emojiPickerButton = ( + + ); + return (
- + {shareButton} diff --git a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx index 6e624a6295..e3b26c9f3c 100644 --- a/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx +++ b/app/javascript/mastodon/components/status_emoji_reactions_bar.jsx @@ -73,7 +73,7 @@ class StatusEmojiReactionsBar extends React.PureComponent { render () { const { emojiReactions } = this.props; - const emojiButtons = Array.from(emojiReactions).map((emoji, index) => ( + const emojiButtons = Array.from(emojiReactions).filter(emoji => emoji.get('count') != 0).map((emoji, index) => ( {intl.formatMessage(messages.publish)}; } else { - publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + publishText = (this.props.privacy !== 'unlisted' && this.props.privacy !== 'public_unlisted') ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); } return ( diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx index ffd1094cd3..c939b9e5cb 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx @@ -12,6 +12,8 @@ const messages = defineMessages({ public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, + public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, + public_unlisted_long: { id: 'privacy.public_unlisted.long', defaultMessage: 'Visible for all without GTL' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, @@ -218,6 +220,7 @@ class PrivacyDropdown extends React.PureComponent { this.options = [ { icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, + { icon: 'cloud', value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, ]; diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index 0ee6d06c7f..10e18e504f 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -154,7 +154,7 @@ class Footer extends ImmutablePureComponent { render () { const { status, intl, withOpenButton } = this.props; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility')); const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; let replyIcon, replyTitle; diff --git a/app/javascript/mastodon/features/report/components/status_check_box.jsx b/app/javascript/mastodon/features/report/components/status_check_box.jsx index 5366da90be..ba44d4a0ed 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.jsx +++ b/app/javascript/mastodon/features/report/components/status_check_box.jsx @@ -13,6 +13,7 @@ import Icon from 'mastodon/components/icon'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); @@ -43,6 +44,7 @@ class StatusCheckBox extends React.PureComponent { const visibilityIconInfo = { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index dc87e9751f..e0f4cee26f 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -190,8 +190,8 @@ class ActionBar extends React.PureComponent { const { status, relationship, intl } = this.props; const { signedIn, permissions } = this.context.identity; - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); + const publicStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'private'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); const account = status.get('account'); const writtenByMe = status.getIn(['account', 'id']) === me; @@ -267,6 +267,10 @@ class ActionBar extends React.PureComponent {
); + const emojiPickerButton = ( + + ); + let replyIcon; if (status.get('in_reply_to_id', null) === null) { replyIcon = 'reply'; @@ -293,7 +297,7 @@ class ActionBar extends React.PureComponent {
-
+
{shareButton} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 61c600ffc1..213bdf4533 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -22,6 +22,7 @@ import EditedTimestamp from 'mastodon/components/edited_timestamp'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -209,6 +210,7 @@ class DetailedStatus extends ImmutablePureComponent { const visibilityIconInfo = { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx index d6a6cea31e..f9b36907c6 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/boost_modal.jsx @@ -20,6 +20,7 @@ const messages = defineMessages({ reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, }); @@ -87,6 +88,7 @@ class BoostModal extends ImmutablePureComponent { const visibilityIconInfo = { 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) }, 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) }, + 'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) }, 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ae2d40f062..8a564fa9d6 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -460,6 +460,8 @@ "privacy.private.short": "Followers only", "privacy.public.long": "Visible for all", "privacy.public.short": "Public", + "privacy.public_unlisted.long": "Visible for all without GTL", + "privacy.public_unlisted.short": "Public unlisted", "privacy.unlisted.long": "Visible for all, but opted-out of discovery features", "privacy.unlisted.short": "Unlisted", "privacy_policy.last_updated": "Last updated {date}", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 12a35c22ba..7879a690a8 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -458,9 +458,11 @@ "privacy.direct.short": "指定された相手のみ", "privacy.private.long": "フォロワーのみ閲覧可", "privacy.private.short": "フォロワーのみ", - "privacy.public.long": "誰でも閲覧可", + "privacy.public.long": "誰でも閲覧可、全てのTL", "privacy.public.short": "公開", - "privacy.unlisted.long": "誰でも閲覧可、サイレント", + "privacy.public_unlisted.long": "誰でも閲覧可、ローカル+ホームTL", + "privacy.public_unlisted.short": "ローカル公開", + "privacy.unlisted.long": "誰でも閲覧可、ホームTL", "privacy.unlisted.short": "未収載", "privacy_policy.last_updated": "{date}に更新", "privacy_policy.title": "プライバシーポリシー", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 842b7af518..9cd81a7bf0 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -218,7 +218,7 @@ const insertEmoji = (state, position, emojiData, needsSpace) => { }; const privacyPreference = (a, b) => { - const order = ['public', 'unlisted', 'private', 'direct']; + const order = ['public', 'public_unlisted', 'unlisted', 'private', 'direct']; return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 538044340c..fdc9c54c8c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -802,7 +802,8 @@ body > [data-popper-placement] { vertical-align: middle; object-fit: contain; margin: -0.2ex 0.15em 0.2ex; - width: 16px; + min-width: 16px; + max-width: min(8em, 100%); height: 16px; img { @@ -878,7 +879,8 @@ body > [data-popper-placement] { } .emojione { - width: 20px; + min-width: 20px; + max-width: min(8em, 100%); height: 20px; margin: -3px 0 0; } @@ -959,7 +961,8 @@ body > [data-popper-placement] { overflow-y: auto; .emojione { - width: 20px; + min-width: 20px; + max-width: min(8em, 100%); height: 20px; margin: -3px 0 0; } @@ -1334,7 +1337,8 @@ body > [data-popper-placement] { line-height: 24px; .emojione { - width: 24px; + min-width: 24px; + max-width: min(8em, 100%); height: 24px; margin: -1px 0 0; } @@ -4122,6 +4126,10 @@ a.status-card.compact:hover { text-align: center; } +.detailed-status__button .emoji-button { + padding: 0; +} + .column-settings__outer { background: lighten($ui-base-color, 8%); padding: 15px; @@ -5871,7 +5879,8 @@ a.status-card.compact:hover { line-height: 24px; .emojione { - width: 24px; + min-width: 24px; + max-width: min(8em, 100%); height: 24px; margin: -1px 0 0; } @@ -7222,7 +7231,8 @@ noscript { } .emojione { - width: 22px; + min-width: 22px; + max-width: min(8em, 100%); height: 22px; } diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss index 0d7a7df2ee..b427e56c44 100644 --- a/app/javascript/styles/mastodon/emoji_picker.scss +++ b/app/javascript/styles/mastodon/emoji_picker.scss @@ -166,6 +166,11 @@ .emoji-mart-category .emoji-mart-emoji { cursor: pointer; + &.emoji-mart-emoji-custom img { + width: auto !important; + min-width: 22px; + } + span { z-index: 1; position: relative; diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss index f5d4cde6c6..729825fc07 100644 --- a/app/javascript/styles/mastodon/rich_text.scss +++ b/app/javascript/styles/mastodon/rich_text.scss @@ -85,6 +85,13 @@ sup { vertical-align: super; } + +} + +.status__content__text { + a.kmy-dangerous-link { + color: red !important; + } } .reply-indicator__content { diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index b644b38f15..726c6ae1e5 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -299,7 +299,8 @@ a.table-action-link { margin-right: 10px; .emojione { - width: 32px; + min-width: 32px; + max-width: min(8em, 100%); height: 32px; } } diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index ef7bfc6de4..3d3ea1bbbd 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -36,7 +36,8 @@ font-weight: 400; .emojione { - width: 20px; + min-width: 20px; + max-width: min(8em, 100%); height: 20px; margin: -3px 0 0; margin-left: 0.075em; diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index 603816fcb2..9fd08280d9 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -38,7 +38,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity end end - return if EmojiReaction.where(account: @account, status: @original_status).count >= 10 + return if EmojiReaction.where(account: @account, status: @original_status).count >= EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT reaction = @original_status.emoji_reactions.create!(account: @account, name: shortcode, custom_emoji: emoji, uri: @json['id']) write_stream(reaction) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 87ea178887..4358172ae4 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -84,7 +84,7 @@ class ActivityPub::TagManager case status.visibility when 'public' [COLLECTIONS[:public]] - when 'unlisted', 'private' + when 'unlisted', 'public_unlisted', 'private' [account_followers_url(status.account)] when 'direct', 'limited' if status.account.silenced? @@ -120,7 +120,7 @@ class ActivityPub::TagManager case status.visibility when 'public' cc << account_followers_url(status.account) - when 'unlisted' + when 'unlisted', 'public_unlisted' cc << COLLECTIONS[:public] end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 7dda6b1853..b7993cd6fd 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -109,7 +109,7 @@ class FeedManager def merge_into_home(from_account, into_account) timeline_key = key(:home, into_account.id) aggregate = into_account.user&.aggregates_reblogs? - query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -135,7 +135,7 @@ class FeedManager def merge_into_list(from_account, list) timeline_key = key(:list, list.id) aggregate = list.account.user&.aggregates_reblogs? - query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) + query = from_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4) if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4 oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true).first.last.to_i @@ -253,7 +253,7 @@ class FeedManager next if last_status_score < oldest_home_score end - statuses = target_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) + statuses = target_account.statuses.where(visibility: [:public, :unlisted, :public_unlisted, :private]).includes(:preloadable_poll, :media_attachments, :account, reblog: :account).limit(limit) crutches = build_crutches(account.id, statuses) statuses.each do |status| diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 36fb0e80fb..cbcc0e6b61 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -87,7 +87,7 @@ class StatusReachFinder end def distributable? - @status.public_visibility? || @status.unlisted_visibility? + @status.public_visibility? || @status.unlisted_visibility? || @status.public_unlisted_visibility? end def unsafe? diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb index 91c0ac1273..980e783ff9 100644 --- a/app/lib/text_formatter.rb +++ b/app/lib/text_formatter.rb @@ -44,7 +44,6 @@ class TextFormatter end # line first letter for blockquote - p 'DEBUG ' + html.gsub(/^gt;/, '>') html = markdownify(html.gsub(/^>/, '>')) # html = simple_format(html, {}, sanitize: false).delete("\n") if multiline? @@ -202,6 +201,10 @@ class TextFormatter text.include?(':') ? nil : '' + text + '' end + def image(link, title, alt_text) + nil + end + private def process_program_code(code) diff --git a/app/models/account_statuses_filter.rb b/app/models/account_statuses_filter.rb index 211f414787..068ab329e9 100644 --- a/app/models/account_statuses_filter.rb +++ b/app/models/account_statuses_filter.rb @@ -35,7 +35,7 @@ class AccountStatusesFilter if suspended? Status.none elsif anonymous? - account.statuses.where(visibility: %i(public unlisted)) + account.statuses.where(visibility: %i(public unlisted public_unlisted)) elsif author? account.statuses.all # NOTE: #merge! does not work without the #all elsif blocked? @@ -48,7 +48,7 @@ class AccountStatusesFilter def filtered_scope scope = account.statuses.left_outer_joins(:mentions) - scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) + scope.merge!(scope.where(visibility: follower? ? %i(public unlisted public_unlisted private) : %i(public unlisted public_unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id])) scope.merge!(filtered_reblogs_scope) if reblogs_may_occur? scope diff --git a/app/models/admin/status_filter.rb b/app/models/admin/status_filter.rb index 4d439e9a1c..8f8b538137 100644 --- a/app/models/admin/status_filter.rb +++ b/app/models/admin/status_filter.rb @@ -14,7 +14,7 @@ class Admin::StatusFilter end def results - scope = @account.statuses.where(visibility: [:public, :unlisted]) + scope = @account.statuses.where(visibility: [:public, :unlisted, :public_unlisted]) params.each do |key, value| next if %w(page report_id).include?(key.to_s) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 339f5ae70c..5d30d03fef 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -57,7 +57,7 @@ class Announcement < ApplicationRecord @statuses ||= if status_ids.nil? [] else - Status.where(id: status_ids, visibility: [:public, :unlisted]) + Status.where(id: status_ids, visibility: [:public, :unlisted, :public_unlisted]) end end diff --git a/app/models/concerns/status_threading_concern.rb b/app/models/concerns/status_threading_concern.rb index 8b628beea2..15118c10f7 100644 --- a/app/models/concerns/status_threading_concern.rb +++ b/app/models/concerns/status_threading_concern.rb @@ -12,7 +12,7 @@ module StatusThreadingConcern end def self_replies(limit) - account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted]).reorder(id: :asc).limit(limit) + account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :public_unlisted]).reorder(id: :asc).limit(limit) end private diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 0ebb28dc23..7e9eb91ca5 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -19,6 +19,8 @@ # visible_in_picker :boolean default(TRUE), not null # category_id :bigint(8) # image_storage_schema_version :integer +# image_width :integer +# image_height :integer # class CustomEmoji < ApplicationRecord diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index b66751ffed..44e880d9fc 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -17,6 +17,9 @@ class EmojiReaction < ApplicationRecord include Paginable + EMOJI_REACTION_LIMIT = 32767 + EMOJI_REACTION_PER_ACCOUNT_LIMIT = 5 + update_index('statuses', :status) belongs_to :account, inverse_of: :emoji_reactions diff --git a/app/models/featured_tag.rb b/app/models/featured_tag.rb index 587dcf9912..1bc47af7f2 100644 --- a/app/models/featured_tag.rb +++ b/app/models/featured_tag.rb @@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord 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) + update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at) end private @@ -59,8 +59,8 @@ class FeaturedTag < ApplicationRecord 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 + self.statuses_count = account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).count + self.last_status_at = account.statuses.where(visibility: %i(public unlisted public_unlisted)).tagged_with(tag).select(:created_at).first&.created_at end def validate_featured_tags_limit diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index 1cfd9a500c..51a9058a84 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -25,6 +25,7 @@ class PublicFeed scope.merge!(without_reblogs_scope) unless with_reblogs? scope.merge!(local_only_scope) if local_only? scope.merge!(remote_only_scope) if remote_only? + scope.merge!(global_timeline_only_scope) if global_timeline? scope.merge!(account_filters_scope) if account? scope.merge!(media_only_scope) if media_only? scope.merge!(language_scope) if account&.chosen_languages.present? @@ -52,6 +53,10 @@ class PublicFeed options[:remote] end + def global_timeline? + !options[:remote] && !options[:local] + end + def account? account.present? end @@ -72,6 +77,10 @@ class PublicFeed Status.remote end + def global_timeline_only_scope + Status.with_global_timeline_visibility.joins(:account).merge(Account.without_suspended.without_silenced) + end + def without_replies_scope Status.without_replies end diff --git a/app/models/status.rb b/app/models/status.rb index 3309f9c8db..05791b9f7d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -51,7 +51,7 @@ class Status < ApplicationRecord update_index('statuses', :proper) - enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, _suffix: :visibility + enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10 }, _suffix: :visibility belongs_to :application, class_name: 'Doorkeeper::Application', optional: true @@ -99,7 +99,8 @@ class Status < ApplicationRecord scope :with_accounts, ->(ids) { where(id: ids).includes(:account) } scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } - scope :with_public_visibility, -> { where(visibility: :public) } + scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted]) } + scope :with_global_timeline_visibility, -> { where(visibility: [:public]) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) } @@ -232,7 +233,7 @@ class Status < ApplicationRecord end def distributable? - public_visibility? || unlisted_visibility? + public_visibility? || unlisted_visibility? || public_unlisted_visibility? end def translatable? @@ -306,8 +307,8 @@ class Status < ApplicationRecord if account.present? emoji_reactions.each do |emoji_reaction| emoji_reaction['me'] = emoji_reaction['account_ids'].include?(account.id.to_s) - emoji_reaction['count'] = emoji_reaction['account_ids'].size emoji_reaction['account_ids'] -= account.excluded_from_timeline_account_ids.map(&:to_s) + emoji_reaction['count'] = emoji_reaction['account_ids'].size end end end diff --git a/app/policies/admin/status_policy.rb b/app/policies/admin/status_policy.rb index ffaa30f13d..181c16653f 100644 --- a/app/policies/admin/status_policy.rb +++ b/app/policies/admin/status_policy.rb @@ -12,7 +12,7 @@ class Admin::StatusPolicy < ApplicationPolicy end def show? - role.can?(:manage_reports, :manage_users) && (record.public_visibility? || record.unlisted_visibility? || record.reported?) + role.can?(:manage_reports, :manage_users) && (record.public_visibility? || record.unlisted_visibility? || record.public_unlisted_visibility? || record.reported?) end def destroy? diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 6088dbcffb..22d5e3d95d 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -17,7 +17,7 @@ class StatusRelationshipsPresenter statuses = statuses.compact status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact conversation_ids = statuses.filter_map(&:conversation_id).uniq - pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) } + pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && %w(public unlisted public_unlisted private).include?(s.visibility) } @filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {}) @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) diff --git a/app/serializers/rest/custom_emoji_serializer.rb b/app/serializers/rest/custom_emoji_serializer.rb index aff58e4d94..a4d2556849 100644 --- a/app/serializers/rest/custom_emoji_serializer.rb +++ b/app/serializers/rest/custom_emoji_serializer.rb @@ -3,7 +3,7 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer include RoutingHelper - attributes :shortcode, :url, :static_url, :visible_in_picker + attributes :shortcode, :url, :static_url, :visible_in_picker, :width, :height attribute :category, if: :category_loaded? @@ -22,4 +22,12 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer def category_loaded? object.association(:category).loaded? && object.category.present? end + + def width + object.image_width + end + + def height + object.image_height + end end diff --git a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb index d4230de61a..48c70a4808 100644 --- a/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb +++ b/app/serializers/rest/emoji_reactions_grouped_by_name_serializer.rb @@ -9,6 +9,8 @@ class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer attribute :url, if: :custom_emoji? attribute :static_url, if: :custom_emoji? attribute :domain, if: :custom_emoji? + attribute :width, if: :custom_emoji? + attribute :height, if: :custom_emoji? attribute :account_ids, if: :account_ids? def current_user? @@ -34,4 +36,12 @@ class REST::EmojiReactionsGroupedByNameSerializer < ActiveModel::Serializer def domain object.custom_emoji.domain end + + def width + object.custom_emoji.image_width + end + + def height + object.custom_emoji.image_height + end end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index f36591939a..eb98750605 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -77,6 +77,11 @@ class REST::InstanceSerializer < ActiveModel::Serializer translation: { enabled: TranslationService.configured?, }, + + emoji_reactions: { + max_reactions: EmojiReaction::EMOJI_REACTION_LIMIT, + max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, + }, } end @@ -92,6 +97,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer def fedibird_capabilities capabilities = [ :emoji_reaction, + :visibility_public_unlisted, + :enable_wide_emoji, + :enable_wide_emoji_reaction, ] capabilities << :profile_search unless Chewy.enabled? diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 0c4bd29af7..d3c96a77e2 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -141,7 +141,7 @@ class REST::StatusSerializer < ActiveModel::Serializer current_user? && current_user.account_id == object.account_id && !object.reblog? && - %w(public unlisted private).include?(object.visibility) + %w(public unlisted public_unlisted private).include?(object.visibility) end def source_requested? diff --git a/app/serializers/rest/v1/instance_serializer.rb b/app/serializers/rest/v1/instance_serializer.rb index a96d2adf53..338c79383e 100644 --- a/app/serializers/rest/v1/instance_serializer.rb +++ b/app/serializers/rest/v1/instance_serializer.rb @@ -83,6 +83,11 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer min_expiration: PollValidator::MIN_EXPIRATION, max_expiration: PollValidator::MAX_EXPIRATION, }, + + emoji_reactions: { + max_reactions: EmojiReaction::EMOJI_REACTION_LIMIT, + max_reactions_per_account: EmojiReaction::EMOJI_REACTION_PER_ACCOUNT_LIMIT, + }, } end @@ -102,6 +107,9 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer def fedibird_capabilities capabilities = [ :emoji_reaction, + :visibility_public_unlisted, + :enable_wide_emoji, + :enable_wide_emoji_reaction, ] capabilities << :profile_search unless Chewy.enabled? diff --git a/app/services/concerns/account_limitable.rb b/app/services/concerns/account_limitable.rb index 8a55d857c9..9788eda91e 100644 --- a/app/services/concerns/account_limitable.rb +++ b/app/services/concerns/account_limitable.rb @@ -3,7 +3,7 @@ module AccountLimitable def scope_status(status) case status.visibility.to_sym - when :public, :unlisted + when :public, :unlisted, :public_unlisted #scope_local.merge(scope_list_following_account(status.account)) scope_local when :private diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 2554756a5d..9f7a8441cc 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -17,8 +17,13 @@ class FanOutOnWriteService < BaseService warm_payload_cache! fan_out_to_local_recipients! - fan_out_to_public_recipients! if broadcastable? - fan_out_to_public_streams! if broadcastable? + if broadcastable? + fan_out_to_public_recipients! + fan_out_to_public_streams! + elsif broadcastable_unlisted? + fan_out_to_public_recipients! + fan_out_to_public_unlisted_streams! + end end private @@ -41,7 +46,7 @@ class FanOutOnWriteService < BaseService notify_about_update! if update? case @status.visibility.to_sym - when :public, :unlisted, :private + when :public, :unlisted, :public_unlisted, :private deliver_to_all_followers! deliver_to_lists! when :limited @@ -61,6 +66,11 @@ class FanOutOnWriteService < BaseService broadcast_to_public_streams! end + def fan_out_to_public_unlisted_streams! + broadcast_to_hashtag_streams! + broadcast_to_public_unlisted_streams! + end + def deliver_to_self! FeedManager.instance.push_to_home(@account, @status, update: update?) if @account.local? end @@ -132,6 +142,16 @@ class FanOutOnWriteService < BaseService end end + def broadcast_to_public_unlisted_streams! + return if @status.reply? && @status.in_reply_to_account_id != @account.id + + redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', anonymous_payload) + + if @status.with_media? + redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', anonymous_payload) + end + end + def deliver_to_conversation! AccountConversation.add_status(@account, @status) unless update? end @@ -158,4 +178,8 @@ class FanOutOnWriteService < BaseService def broadcastable? @status.public_visibility? && !@status.reblog? && !@account.silenced? end + + def broadcastable_unlisted? + @status.public_unlisted_visibility? && !@status.reblog? && !@account.silenced? + end end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index ea27f374e7..d1e750c9c4 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -65,7 +65,7 @@ class PostStatusService < BaseService @sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @visibility = @options[:visibility] || @account.user&.setting_default_privacy - @visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced? + @visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted) && @account.silenced? @scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = nil if scheduled_in_the_past? rescue ArgumentError diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 0ce525b071..b673c5a029 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -62,7 +62,7 @@ class ReportService < BaseService # If the account making reports is remote, it is likely anonymized so we have to relax the requirements for attaching statuses. domain = @source_account.domain.to_s.downcase has_followers = @target_account.followers.where(Account.arel_table[:domain].lower.eq(domain)).exists? - visibility = has_followers ? %i(public unlisted private) : %i(public unlisted) + visibility = has_followers ? %i(public unlisted public_unlisted private) : %i(public unlisted public_unlisted) scope = @target_account.statuses.with_discarded scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain))) # Allow missing posts to not drop reports that include e.g. a deleted post diff --git a/app/workers/feed_any_json_worker.rb b/app/workers/feed_any_json_worker.rb index cf9bdca17d..00c24e820a 100644 --- a/app/workers/feed_any_json_worker.rb +++ b/app/workers/feed_any_json_worker.rb @@ -16,8 +16,8 @@ class FeedAnyJsonWorker redis.publish("timeline:#{account.id}", payload_json) if redis.exists?("subscribed:timeline:#{account.id}") end - if status.visibility.to_sym != :public && status.visibility.to_sym != :unlisted && status.account_id != my_account_id && - redis.exists?("subscribed:timeline:#{status.account_id}") + if status.visibility.to_sym != :public && status.visibility.to_sym != :unlisted && status.visibility.to_sym != :public_unlisted && status.account_id != my_account_id && + redis.exists?("subscribed:timeline:#{status.account_id}") redis.publish("timeline:#{status.account_id}", payload_json) end end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index d503395a9c..73c02f6b5b 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1521,9 +1521,11 @@ ja: private: フォロワー限定 private_long: フォロワーにのみ表示されます public: 公開 - public_long: 誰でも見ることができ、かつ公開タイムラインに表示されます + public_long: 誰でも見ることができ、かつ連合・ローカルタイムラインに表示されます + public_unlisted: ローカル公開 + public_unlisted_long: 誰でも見ることができますが、連合タイムラインには表示されません unlisted: 未収載 - unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません + unlisted_long: 誰でも見ることができますが、連合・ローカルタイムラインには表示されません statuses_cleanup: enabled: 古い投稿を自動的に削除する enabled_hint: 設定した期間を過ぎた投稿は、以下の例外に該当しない限り、自動的に削除されます diff --git a/config/routes.rb b/config/routes.rb index 82ec5a5f4b..3ee7a8069d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -463,7 +463,7 @@ Rails.application.routes.draw do post :translate, to: 'translations#create' - resources :emoji_reactions, only: [:create, :update], constraints: { id: /[^\/]+/ } + resources :emoji_reactions, only: [:create, :update, :destroy], constraints: { id: /[^\/]+/ } post :emoji_unreaction, to: 'emoji_reactions#destroy' end diff --git a/db/migrate/20230308061833_add_image_size_to_custom_emojis.rb b/db/migrate/20230308061833_add_image_size_to_custom_emojis.rb new file mode 100644 index 0000000000..ff048c6903 --- /dev/null +++ b/db/migrate/20230308061833_add_image_size_to_custom_emojis.rb @@ -0,0 +1,6 @@ +class AddImageSizeToCustomEmojis < ActiveRecord::Migration[6.1] + def change + add_column :custom_emojis, :image_width, :integer + add_column :custom_emojis, :image_height, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index cd085429f2..93a923bbe2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_23_102416) do +ActiveRecord::Schema.define(version: 2023_03_08_061833) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -338,6 +338,8 @@ ActiveRecord::Schema.define(version: 2023_02_23_102416) do t.boolean "visible_in_picker", default: true, null: false t.bigint "category_id" t.integer "image_storage_schema_version" + t.integer "image_width" + t.integer "image_height" t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true end diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 9cc500c36e..fdcb0c450b 100644 --- a/lib/sanitize_ext/sanitize_config.rb +++ b/lib/sanitize_ext/sanitize_config.rb @@ -50,6 +50,26 @@ class Sanitize current_node.replace(Nokogiri::XML::Text.new(current_node.text, current_node.document)) unless LINK_PROTOCOLS.include?(scheme) end + PHISHING_SCAM_HREF_TRANSFORMER = lambda do |env| + return unless env[:node_name] == 'a' + + current_node = env[:node] + href = current_node['href'] + text = current_node.text + cls = current_node['class'] || '' + + scheme = if current_node['href'] =~ Sanitize::REGEX_PROTOCOL + Regexp.last_match(1).downcase + else + :relative + end + + if LINK_PROTOCOLS.include?(scheme) && href != text && href != 'https://' + text && !text.start_with?('#') && !text.start_with?('@') + current_node['class'] = cls + ' kmy-dangerous-link' + current_node.before(Nokogiri::XML::Text.new('⚠', current_node.document)) + end + end + UNSUPPORTED_ELEMENTS_TRANSFORMER = lambda do |env| return unless %w(h1 h2 h3 h4 h5 h6).include?(env[:node_name]) @@ -82,6 +102,7 @@ class Sanitize CLASS_WHITELIST_TRANSFORMER, UNSUPPORTED_ELEMENTS_TRANSFORMER, UNSUPPORTED_HREF_TRANSFORMER, + PHISHING_SCAM_HREF_TRANSFORMER, ] )