Browse Source

删除orig 修改细节 匿名可配置

pull/4/head
欧醚 3 years ago
parent
commit
3cc3992d1d
29 changed files with 14 additions and 5457 deletions
  1. +6
    -5
      app/controllers/api/v1/statuses_controller.rb
  2. +0
    -126
      app/controllers/api/v1/statuses_controller.rb.orig
  3. +0
    -218
      app/helpers/statuses_helper.rb.orig
  4. +0
    -153
      app/javascript/mastodon/components/icon_button.js.orig
  5. +0
    -631
      app/javascript/mastodon/components/status.js.orig
  6. +0
    -349
      app/javascript/mastodon/components/status_action_bar.js.orig
  7. +0
    -286
      app/javascript/mastodon/containers/status_container.js.orig
  8. +0
    -183
      app/javascript/mastodon/features/getting_started/index.js.orig
  9. +0
    -294
      app/javascript/mastodon/features/status/components/action_bar.js.orig
  10. +0
    -271
      app/javascript/mastodon/features/status/components/detailed_status.js.orig
  11. +0
    -673
      app/javascript/mastodon/features/status/index.js.orig
  12. +0
    -247
      app/javascript/mastodon/features/ui/components/columns_area.js.orig
  13. +0
    -41
      app/javascript/mastodon/features/ui/components/navigation_panel.js.orig
  14. +0
    -475
      app/javascript/mastodon/locales/zh-HK.json.orig
  15. +0
    -523
      app/models/status.rb.orig
  16. +1
    -1
      app/serializers/initial_state_serializer.rb
  17. +0
    -182
      app/services/fetch_link_card_service.rb.orig
  18. +0
    -39
      app/views/about/_registration.html.haml.orig
  19. +0
    -91
      app/views/about/show.html.haml.orig
  20. +0
    -91
      app/views/accounts/show.html.haml.orig
  21. +0
    -87
      app/views/statuses/_detailed_status.html.haml.orig
  22. +0
    -81
      app/views/statuses/_simple_status.html.haml.orig
  23. +0
    -22
      app/views/tags/show.html.haml.orig
  24. +0
    -9
      config/initializers/2_whitelist_mode.rb.orig
  25. +0
    -1
      config/initializers/blacklists.rb
  26. +0
    -12
      config/initializers/blacklists.rb.orig
  27. +7
    -0
      config/initializers/new_features.rb
  28. +0
    -158
      config/locales/doorkeeper.zh-TW.yml.orig
  29. +0
    -207
      package.json.orig

+ 6
- 5
app/controllers/api/v1/statuses_controller.rb View File

@ -25,8 +25,8 @@ class Api::V1::StatusesController < Api::BaseController
def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
treeId = ENV['TREE_ADDRESS'].split('/')[-1].to_i
depth = @status.id == treeId ? 1 : ((!ancestors_results.empty? && ancestors_results[0].id == treeId) ? 2 : nil)
treeId = Rails.configuration.x.tree_address.split('/')[-1].to_i
depth = (@status.id == treeId || (!ancestors_results.empty? && ancestors_results[0].id == treeId)) ? 1 : nil
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account, nil, nil, depth)
loaded_ancestors = cache_collection(ancestors_results, Status)
@ -50,9 +50,10 @@ class Api::V1::StatusesController < Api::BaseController
end
def create
masked = status_params[:status].end_with?('[mask]')
sender = masked ? Account.find_local('mask_bot') : current_user.account
st_text = masked ? ("$#{to_cn(7919**(current_account.id + 1000 * Time.new.day) % 1000000007)}:\n" + status_params[:status][0..4900]) : status_params[:status]
p Rails.configuration.x.anon_tag
anon = Rails.configuration.x.anon_acc && status_params[:status].end_with?(Rails.configuration.x.anon_tag)
sender = anon ? Account.find(Rails.configuration.x.anon_acc) : current_user.account
st_text = anon ? ("$#{to_cn(7919**(current_account.id + 1000 * Time.new.day) % 1000000007)}:\n" + status_params[:status][0..4990]) : status_params[:status]
@status = PostStatusService.new.call(sender,
text: st_text,

+ 0
- 126
app/controllers/api/v1/statuses_controller.rb.orig View File

@ -1,126 +0,0 @@
# frozen_string_literal: true
class Api::V1::StatusesController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
override_rate_limit_headers :create, family: :statuses
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
# conversations as quasi-unlimited, it would be too much work to render more
# than this anyway
CONTEXT_LIMIT = 4_096
def show
@status = cache_collection([@status], Status).first
render json: @status, serializer: REST::StatusSerializer
end
def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
treeId = ENV['TREE_ADDRESS'].split('/')[-1].to_i
depth = @status.id == treeId ? 1 : ((!ancestors_results.empty? && ancestors_results[0].id == treeId) ? 2 : nil)
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account, nil, nil, depth)
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
end
def to_cn(n)
case Time.new.wday
when 0, 3
"秦汉魏晋隋唐宋元明清"[n % 10] + [n % 20873, n % 20899].map{|i| i+0x4e00}.pack('U*')
when 1, 4, 6
"甲乙丙丁戊己庚辛壬癸"[n % 10] + [n % 20873, n % 20899].map{|i| i+0x4e00}.pack('U*')
else
"鼠牛虎兔龙蛇马羊猴鸡狗猪" [n % 12] + [n % 20873, n % 20899].map{|i| i+0x4e00}.pack('U*')
end
end
def create
<<<<<<< HEAD
thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
masked = status_params[:status].end_with?('[mask]')
sender = masked ? Account.find_local('mask_bot') : current_account
st_text = masked ? ("$#{to_cn(7919**(current_account.id + 1000 * Time.new.day) % 1000000007)}:\n" + status_params[:status][0..4900]) : status_params[:status]
@status = PostStatusService.new.call(sender,
text: st_text,
thread: thread,
=======
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
thread: @thread,
>>>>>>> master
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
def destroy
@status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy?
@status.discard
RemovalWorker.perform_async(@status.id, redraft: true)
render json: @status, serializer: REST::StatusSerializer, source_requested: true
end
private
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_thread
@thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
rescue ActiveRecord::RecordNotFound
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end
def status_params
params.permit(
:status,
:in_reply_to_id,
:sensitive,
:spoiler_text,
:visibility,
:scheduled_at,
media_ids: [],
poll: [
:multiple,
:hide_totals,
:expires_in,
options: [],
]
)
end
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end

+ 0
- 218
app/helpers/statuses_helper.rb.orig View File

@ -1,218 +0,0 @@
# frozen_string_literal: true
module StatusesHelper
EMBEDDED_CONTROLLER = 'statuses'
EMBEDDED_ACTION = 'embed'
<<<<<<< HEAD
def display_name(account, **options)
if options[:custom_emojify]
Formatter.instance.format_display_name(account, options)
else
account.display_name.presence || account.username
end
end
def account_action_button(account)
if user_signed_in?
if account.id == current_user.account_id
link_to settings_profile_url, class: 'button logo-button' do
safe_join([svg_logo, t('settings.edit_profile')])
end
elsif current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do
safe_join([svg_logo, t('accounts.unfollow')])
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do
safe_join([svg_logo, t('accounts.follow')])
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do
safe_join([svg_logo, t('accounts.follow')])
end
end
end
def minimal_account_action_button(account)
if user_signed_in?
return if account.id == current_user.account_id
if current_account.following?(account) || current_account.requested?(account)
link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
fa_icon('user-times fw')
end
elsif !(account.memorial? || account.moved?)
link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
elsif !(account.memorial? || account.moved?)
link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
fa_icon('user-plus fw')
end
end
end
def svg_logo
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end
def svg_logo_full
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 180 80')
end
def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all
content_tag(:div, class: 'roles') do
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end
end
=======
>>>>>>> master
def link_to_more(url)
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
end
def nothing_here(extra_classes = '')
content_tag(:div, class: "nothing-here #{extra_classes}") do
t('accounts.nothing_here')
end
end
def media_summary(status)
attachments = { image: 0, video: 0, audio: 0 }
status.media_attachments.each do |media|
if media.video?
attachments[:video] += 1
elsif media.audio?
attachments[:audio] += 1
else
attachments[:image] += 1
end
end
text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ')
return if text.blank?
I18n.t('statuses.attached.description', attached: text)
end
def status_text_summary(status)
return if status.spoiler_text.blank?
I18n.t('statuses.content_warning', warning: status.spoiler_text)
end
def poll_summary(status)
return unless status.preloadable_poll
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
end
def status_description(status)
components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')]
if status.spoiler_text.blank?
components << status.text
components << poll_summary(status)
end
components.reject(&:blank?).join("\n\n")
end
def stream_link_target
embedded_view? ? '_blank' : nil
end
def style_classes(status, is_predecessor, is_successor, include_threads)
classes = ['entry']
classes << 'entry-predecessor' if is_predecessor
classes << 'entry-reblog' if status.reblog?
classes << 'entry-successor' if is_successor
classes << 'entry-center' if include_threads
classes.join(' ')
end
def microformats_classes(status, is_direct_parent, is_direct_child)
classes = []
classes << 'p-in-reply-to' if is_direct_parent
classes << 'p-repost-of' if status.reblog? && is_direct_parent
classes << 'p-comment' if is_direct_child
classes.join(' ')
end
def microformats_h_class(status, is_predecessor, is_successor, include_threads)
if is_predecessor || status.reblog? || is_successor
'h-cite'
elsif include_threads
''
else
'h-entry'
end
end
def rtl_status?(status)
status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text))
end
def rtl?(text)
text = simplified_text(text)
rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m)
if rtl_words.present?
total_size = text.size.to_f
rtl_size(rtl_words) / total_size > 0.3
else
false
end
end
def fa_visibility_icon(status)
case status.visibility
when 'public'
fa_icon 'globe fw'
when 'unlisted'
fa_icon 'unlock fw'
when 'private'
fa_icon 'lock fw'
when 'direct'
fa_icon 'envelope fw'
end
end
private
def simplified_text(text)
text.dup.tap do |new_text|
URI.extract(new_text).each do |url|
new_text.gsub!(url, '')
end
new_text.gsub!(Account::MENTION_RE, '')
new_text.gsub!(Tag::HASHTAG_RE, '')
new_text.gsub!(/\s+/, '')
end
end
def rtl_size(words)
words.reduce(0) { |acc, elem| acc + elem.size }.to_f
end
def embedded_view?
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
end
end

+ 0
- 153
app/javascript/mastodon/components/icon_button.js.orig View File

@ -1,153 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
activeStyle: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
};
state = {
activate: false,
deactivate: false,
}
componentWillReceiveProps (nextProps) {
if (!nextProps.animate) return;
if (this.props.active && !nextProps.active) {
this.setState({ activate: false, deactivate: true });
} else if (!this.props.active && nextProps.active) {
this.setState({ activate: true, deactivate: false });
}
}
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
handleKeyPress = (e) => {
if (this.props.onKeyPress && !this.props.disabled) {
this.props.onKeyPress(e);
}
}
handleMouseDown = (e) => {
if (!this.props.disabled && this.props.onMouseDown) {
this.props.onMouseDown(e);
}
}
handleKeyDown = (e) => {
if (!this.props.disabled && this.props.onKeyDown) {
this.props.onKeyDown(e);
}
}
render () {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
...(this.props.active ? this.props.activeStyle : {}),
};
const {
active,
className,
disabled,
expanded,
icon,
inverted,
overlay,
pressed,
tabIndex,
title,
} = this.props;
const {
activate,
deactivate,
} = this.state;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
activate,
deactivate,
overlayed: overlay,
});
return (
<button
<<<<<<< HEAD
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
=======
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
>>>>>>> master
</button>
);
}
}

+ 0
- 631
app/javascript/mastodon/components/status.js.orig View File

@ -1,631 +0,0 @@
import React from 'react';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
<<<<<<< HEAD
import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
=======
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
>>>>>>> master
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
import StatusContainer from '../containers/status_container2';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && status.get('hidden') ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
export const defaultMediaVisibility = (status) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
};
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
sonsIds: ImmutablePropTypes.list,
onPreview: PropTypes.func
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'account',
'muted',
'hidden',
'sonsIds',
'ancestorsText',
];
state = {
showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined,
noStartPD: true
};
<<<<<<< HEAD
_isMounted = false;
// Track height changes we know about to compensate scrolling
componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
this._isMounted = true;
setTimeout(this.loadContext, Math.ceil(Math.random() * 4000 + 1000));
}
getSnapshotBeforeUpdate () {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
return null;
}
}
=======
>>>>>>> master
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
<<<<<<< HEAD
// Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
this._isMounted = false;
}
=======
>>>>>>> master
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
loadContext = () => {
if(!this._isMounted) {
//console.log('cancel');
return;
}
const { status } = this.props;
const r_status = status.get('reblog') || status;
if(this.props.showThread && this.state.noStartPD && (r_status.get('replies_count') || r_status.get('in_reply_to_id') || r_status.get('visibility') == 'private')) {
this.setState({noStartPD: false});
this.props.onPreview(r_status.get('id'));
}
}
handleExpandClick = (e) => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (e.button === 0) {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
const id = e.currentTarget.getAttribute('data-id');
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
}
}
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
}
handleCollapsedToggle = isCollapsed => {
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
}
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
}
renderLoadingVideoPlayer () {
return <div className='video-player' style={{ height: '110px' }} />;
}
renderLoadingAudioPlayer () {
return <div className='audio-player' style={{ height: '110px' }} />;
}
handleOpenVideo = (media, options) => {
this.props.onOpenVideo(media, options);
}
handleHotkeyOpenMedia = e => {
const { onOpenMedia, onOpenVideo } = this.props;
const status = this._properStatus();
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
onOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
}
handleHotkeyFavourite = () => {
this.props.onFavourite(this._properStatus());
}
handleHotkeyBoost = e => {
this.props.onReblog(this._properStatus(), e);
}
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history);
}
handleHotkeyOpen = () => {
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
}
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
}
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus());
}
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
_properStatus () {
const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
return status.get('reblog');
} else {
return status;
}
}
handleRef = c => {
this.node = c;
}
renderChildren (list) {
return list.map(e => (
e.id ?
<div key={`comments-1-${e.id}`}>
<StatusContainer
key={e.id}
id={e.id}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='comments-timeline'
/>
{ e.sonsIds &&
<div key={`comments-2-${e.id}`} className='comments-timeline-2'>{this.renderChildren(e.sonsIds)}</div>
}
</div>
:
<StatusContainer
key={e}
id={e}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='comments-timeline'
/>
));
}
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;
let sons, quote;
const { intl, hidden, featured, otherAccounts, unread, showThread, deep, tree_type, ancestorsText, sonsIds } = this.props;
let { status, account, ...other } = this.props;
if (status === null) {
return null;
}
const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleHotkeyMention,
open: this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
</HotKeys>
);
}
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
);
}
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
account = status.get('account');
status = status.get('reblog');
}
if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth}
height={110}
cacheWidth={this.props.cacheMediaWidth}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={attachment.get('preview_url')}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={this.props.cachedMediaWidth}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/>
);
}
if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
<<<<<<< HEAD
if (sonsIds && sonsIds.size > 0) {
sons = <div className='comments-timeline__wrapper'><div className='comments-timeline'>{this.renderChildren(sonsIds)}</div></div>;
}
if (rebloggedByText && status.get('in_reply_to_id')) {
quote = ancestorsText ?
<div className='status__tree__quote__wrapper'>
<Icon id="tree" />
{ancestorsText}
</div>
:
<div className='status__quote__wrapper'>
<StatusContainer
key={status.get('in_reply_to_id')}
id={status.get('in_reply_to_id')}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='quote'
/>
</div>;
}
let deepRec;
if(deep != null) {
deepRec = (
<div className="detailed-status__button deep__number">
<Icon id="tree" />
<span>
【<FormattedNumber value={deep} />】
</span>
</div>
);
}
=======
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
>>>>>>> master
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted }, (deep!=null) && 'tree-'+tree_type)} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} others={otherAccounts} />
</a>
</div>
<<<<<<< HEAD
{deepRec}
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media}
{quote}
{showThread && status.get('in_reply_to_id') && (
<button className='status__content__read-more-button' onClick={this.handleClick}>
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
</button>
)}
{(deep == null || tree_type != 'ance') && (
<StatusActionBar status={status} account={account} {...other} />
)}
=======
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
{media}
<StatusActionBar status={status} account={account} {...other} />
>>>>>>> master
</div>
</div>
{sons}
</HotKeys>
);
}
}

+ 0
- 349
app/javascript/mastodon/components/status_action_bar.js.orig View File

@ -1,349 +0,0 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, isStaff } from '../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});
const obfuscatedCount = count => {
if (count < 0) {
return 0;
} else if (count <= 10) {
return count;
} else {
return '10+';
}
};
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
export default @connect(mapStateToProps)
@injectIntl
class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onReport: PropTypes.func,
onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func,
onPin: PropTypes.func,
onBookmark: PropTypes.func,
withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'relationship',
'withDismiss',
]
handleReplyClick = () => {
if (me) {
this.props.onReply(this.props.status, this.context.router.history);
} else {
this._openInteractionDialog('reply');
}
}
handleShareClick = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
}
handleFavouriteClick = () => {
if (me) {
this.props.onFavourite(this.props.status);
} else {
this._openInteractionDialog('favourite');
}
}
handleReblogClick = e => {
if (me) {
this.props.onReblog(this.props.status, e);
} else {
this._openInteractionDialog('reblog');
}
}
_openInteractionDialog = type => {
window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
handleBookmarkClick = () => {
this.props.onBookmark(this.props.status);
}
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');
if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
}
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
if (relationship && relationship.get('blocking')) {
onUnblock(account);
} else {
onBlock(status);
}
}
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
}
handleOpen = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
}
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
handleReport = () => {
this.props.onReport(this.props.status);
}
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}
handleCopy = () => {
const url = this.props.status.get('url');
const textarea = document.createElement('textarea');
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
document.body.removeChild(textarea);
}
}
render () {
const { status, relationship, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted');
const anonymousAccess = !me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const account = status.get('account');
let menu = [];
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push(null);
if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
if (status.getIn(['account', 'id']) === me) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
}
let replyIcon;
let replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = '';
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
const shareButton = ('share' in navigator) && publicStatus && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
return (
<div className='status__action-bar'>
<<<<<<< HEAD
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'comment-o' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />{publicStatus && <span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('reblogs_count'))}</span>}</div>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='heart' onClick={this.handleFavouriteClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('favourites_count'))}</span></div>
=======
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
>>>>>>> master
{shareButton}
<div className='status__action-bar-dropdown'>
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
</div>
</div>
);
}
}

+ 0
- 286
app/javascript/mastodon/containers/status_container.js.orig View File

@ -1,286 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import Immutable from 'immutable';
import { createSelector } from 'reselect';
import Status from '../components/status';
import { makeGetStatus } from '../selectors';
import {
replyCompose,
mentionCompose,
directCompose,
} from '../actions/compose';
import {
reblog,
favourite,
bookmark,
unreblog,
unfavourite,
unbookmark,
pin,
unpin,
} from '../actions/interactions';
import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
<<<<<<< HEAD
fetchContext,
=======
toggleStatusCollapse,
>>>>>>> master
} from '../actions/statuses';
import {
unmuteAccount,
unblockAccount,
} from '../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../actions/domain_blocks';
import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
<<<<<<< HEAD
import { defineMessages, injectIntl } from 'react-intl';
import { boostModal, deleteModal, treeRoot } from '../initial_state';
=======
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
>>>>>>> master
import { showAlertForError } from '../actions/alerts';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getAncestorsText = createSelector([
(_, {ids}) => ids,
state => state.get('statuses'),
], (ids, statuses) => ids.map(i => {
let text = statuses.get(i) ? statuses.get(i).get('search_index') : i;
if(text.length > 16)
text = text.slice(0,13) + "...";
return text;
}).join(' >> ')
);
const getSonsIds = createSelector([
(_, {id}) => id,
state => state.getIn(['contexts', 'replies']),
], (statusId, contextReplies) => {
const sons = contextReplies.get(statusId);
return sons ? sons.map(id => ({
'id': id,
'sonsIds' : contextReplies.get(id),
}))
: null;
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, props);
let ancestorsIds = Immutable.List();
let ancestorsText;
let sonsIds;
if (props.showThread && status) {
sonsIds = getSonsIds(state, { id : status.getIn(['reblog', 'id'], props.id)});
if(status.get('reblog')) {
ancestorsIds = getAncestorsIds(state, { id: status.getIn(['reblog', 'in_reply_to_id']) });
if(ancestorsIds && ancestorsIds.first() == treeRoot.split('/').pop()) {
ancestorsText = getAncestorsText(state, { ids: ancestorsIds.shift() });
}
}
}
return {
status,
ancestorsText,
sonsIds,
};
};
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
}));
} else {
dispatch(replyCompose(status, router));
}
});
},
onModalReblog (status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
}
},
onReblog (status, e) {
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
}
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
},
onBookmark (status) {
if (status.get('bookmarked')) {
dispatch(unbookmark(status));
} else {
dispatch(bookmark(status));
}
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
} else {
dispatch(pin(status));
}
},
onEmbed (status) {
dispatch(openModal('EMBED', {
url: status.get('url'),
onError: error => dispatch(showAlertForError(error)),
}));
},
onDelete (status, history, withRedraft = false) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
}));
}
},
onDirect (account, router) {
dispatch(directCompose(account, router));
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
},
onOpenMedia (media, index) {
dispatch(openModal('MEDIA', { media, index }));
},
onOpenVideo (media, options) {
dispatch(openModal('VIDEO', { media, options }));
},
onBlock (status) {
const account = status.get('account');
dispatch(initBlockModal(account));
},
onUnblock (account) {
dispatch(unblockAccount(account.get('id')));
},
onReport (status) {
dispatch(initReport(status.get('account'), status));
},
onMute (account) {
dispatch(initMuteModal(account));
},
onUnmute (account) {
dispatch(unmuteAccount(account.get('id')));
},
onMuteConversation (status) {
if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id')));
} else {
dispatch(muteStatus(status.get('id')));
}
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
},
<<<<<<< HEAD
onPreview (id) {
dispatch(fetchContext(id));
=======
onToggleCollapsed (status, isCollapsed) {
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
}));
},
onUnblockDomain (domain) {
dispatch(unblockDomain(domain));
>>>>>>> master
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

+ 0
- 183
app/javascript/mastodon/features/getting_started/index.js.orig View File

@ -1,183 +0,0 @@
import React from 'react';
import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, profile_directory, showTrends } from '../../initial_state';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import { List as ImmutableList } from 'immutable';
import NavigationBar from '../compose/components/navigation_bar';
import Icon from 'mastodon/components/icon';
import LinkFooter from 'mastodon/features/ui/components/link_footer';
import TrendsContainer from './containers/trends_container';
const messages = defineMessages({
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
});
const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]),
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
});
const mapDispatchToProps = dispatch => ({
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
});
const badgeDisplay = (number, limit) => {
if (number === 0) {
return undefined;
} else if (limit && number >= limit) {
return `${limit}+`;
} else {
return number;
}
};
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class GettingStarted extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map.isRequired,
columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number,
unreadNotifications: PropTypes.number,
};
componentDidMount () {
const { fetchFollowRequests, multiColumn } = this.props;
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
this.context.router.history.replace('/timelines/home');
return;
}
fetchFollowRequests();
}
render () {
const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
const navItems = [];
let i = 1;
let height = (multiColumn) ? 0 : 60;
if (multiColumn) {
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
<ColumnLink key={i++} icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
<ColumnLink key={i++} icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
);
height += 34 + 48*2;
if (profile_directory) {
navItems.push(
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
);
height += 48;
}
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />,
);
height += 34;
} else if (profile_directory) {
navItems.push(
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />,
);
height += 48;
}
navItems.push(
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<<<<<<< HEAD
<ColumnLink key={i++} icon='heart' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />
=======
<ColumnLink key={i++} icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
>>>>>>> master
);
height += 48*4;
if (myAccount.get('locked') || unreadFollowRequests > 0) {
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
height += 48;
}
if (!multiColumn) {
navItems.push(
<ColumnSubheading key={i++} text={intl.formatMessage(messages.settings_subheading)} />,
<ColumnLink key={i++} icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
);
height += 34 + 48;
}
return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.menu)}>
{multiColumn && <div className='column-header__wrapper'>
<h1 className='column-header'>
<button>
<Icon id='bars' className='column-header__icon' fixedWidth />
<FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
</button>
</h1>
</div>}
<div className='getting-started'>
<div className='getting-started__wrapper' style={{ height }}>
{!multiColumn && <NavigationBar account={myAccount} />}
{navItems}
</div>
{!multiColumn && <div className='flex-spacer' />}
<LinkFooter withHotkeys={multiColumn} />
</div>
{multiColumn && showTrends && <TrendsContainer />}
</Column>
);
}
}

+ 0
- 294
app/javascript/mastodon/features/status/components/action_bar.js.orig View File

@ -1,294 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
more: { id: 'status.more', defaultMessage: 'More' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
block: { id: 'status.block', defaultMessage: 'Block @{name}' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
share: { id: 'status.share', defaultMessage: 'Share' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
embed: { id: 'status.embed', defaultMessage: 'Embed' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' },
copy: { id: 'status.copy', defaultMessage: 'Copy link to status' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
});
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
export default @connect(mapStateToProps)
@injectIntl
class ActionBar extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
onUnmute: PropTypes.func,
onBlock: PropTypes.func,
onUnblock: PropTypes.func,
onBlockDomain: PropTypes.func,
onUnblockDomain: PropTypes.func,
onMuteConversation: PropTypes.func,
onReport: PropTypes.func,
onPin: PropTypes.func,
onEmbed: PropTypes.func,
intl: PropTypes.object.isRequired,
};
handleReplyClick = () => {
this.props.onReply(this.props.status);
}
handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e);
}
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
}
handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
}
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props;
const account = status.get('account');
if (relationship && relationship.get('muting')) {
onUnmute(account);
} else {
onMute(account);
}
}
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
if (relationship && relationship.get('blocking')) {
onUnblock(account);
} else {
onBlock(status);
}
}
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
}
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}
handleReport = () => {
this.props.onReport(this.props.status);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
handleCopy = () => {
const url = this.props.status.get('url');
const textarea = document.createElement('textarea');
textarea.textContent = url;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
try {
textarea.select();
document.execCommand('copy');
} catch (e) {
} finally {
document.body.removeChild(textarea);
}
}
render () {
const { status, relationship, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
const account = status.get('account');
let menu = [];
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
menu.push(null);
}
if (me === status.getIn(['account', 'id'])) {
if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push(null);
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null);
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick });
}
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick });
}
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
}
if (isStaff) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
}
const shareButton = ('share' in navigator) && publicStatus && (
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
let replyIcon;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
} else {
replyIcon = 'reply';
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
return (
<div className='detailed-status__action-bar'>
<<<<<<< HEAD
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'comment-o' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='heart' onClick={this.handleFavouriteClick} /></div>
=======
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
>>>>>>> master
{shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div>
</div>
);
}
}

+ 0
- 271
app/javascript/mastodon/features/status/components/detailed_status.js.orig View File

@ -1,271 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom';
import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
export default @injectIntl
class DetailedStatus extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired,
onToggleHidden: PropTypes.func.isRequired,
measureHeight: PropTypes.bool,
onHeightChange: PropTypes.func,
domain: PropTypes.string.isRequired,
compact: PropTypes.bool,
showMedia: PropTypes.bool,
onToggleMediaVisibility: PropTypes.func,
};
state = {
height: null,
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
e.preventDefault();
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
e.stopPropagation();
}
handleOpenVideo = (media, options) => {
this.props.onOpenVideo(media, options);
}
handleExpandedToggle = () => {
this.props.onToggleHidden(this.props.status);
}
_measureHeight (heightJustChanged) {
if (this.props.measureHeight && this.node) {
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
if (this.props.onHeightChange && heightJustChanged) {
this.props.onHeightChange();
}
}
}
setRef = c => {
this.node = c;
this._measureHeight();
}
componentDidUpdate (prevProps, prevState) {
this._measureHeight(prevState.height !== this.state.height);
}
handleModalLink = e => {
e.preventDefault();
let href;
if (e.target.nodeName !== 'A') {
href = e.target.parentNode.href;
} else {
href = e.target.href;
}
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
<<<<<<< HEAD
const { compact, deep } = this.props;
=======
const { intl, compact } = this.props;
>>>>>>> master
if (!status) {
return null;
}
let media = '';
let applicationLink = '';
let reblogLink = '';
let reblogIcon = 'retweet';
let favouriteLink = '';
if (this.props.measureHeight) {
outerStyle.height = `${this.state.height}px`;
}
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Audio
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
height={150}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Video
preview={attachment.get('preview_url')}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={300}
height={150}
inline
onOpenVideo={this.handleOpenVideo}
sensitive={status.get('sensitive')}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
} else {
media = (
<MediaGallery
standalone
sensitive={status.get('sensitive')}
media={status.get('media_attachments')}
height={300}
onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia}
onToggleVisibility={this.props.onToggleMediaVisibility}
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
}
if (status.get('application')) {
applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
}
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const visibilityLink = <React.Fragment> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></React.Fragment>;
if (['private', 'direct'].includes(status.get('visibility'))) {
reblogLink = '';
} else if (this.context.router) {
reblogLink = (
<React.Fragment>
<React.Fragment> · </React.Fragment>
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} />
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
</Link>
</React.Fragment>
);
} else {
reblogLink = (
<React.Fragment>
<React.Fragment> · </React.Fragment>
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id={reblogIcon} />
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
</a>
</React.Fragment>
);
}
if (this.context.router) {
favouriteLink = (
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<Icon id='heart' />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} />
</span>
</Link>
);
} else {
favouriteLink = (
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='heart' />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} />
</span>
</a>
);
}
let deepRec;
if(deep != null) {
deepRec = (
<div className="detailed-status__button deep__number">
<Icon id="tree" />
<span>
【<FormattedNumber value={deep} />】
</span>
</div>
);
}
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a>
{deepRec}
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
{media}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div>
</div>
</div>
);
}
}

+ 0
- 673
app/javascript/mastodon/features/status/index.js.orig View File

@ -1,673 +0,0 @@
import Immutable from 'immutable';
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { createSelector } from 'reselect';
import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator';
import DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar';
import Column from '../ui/components/column';
import Tree from 'react-tree-graph';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
pin,
unpin,
} from '../../actions/interactions';
import {
replyCompose,
mentionCompose,
directCompose,
} from '../../actions/compose';
import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
} from '../../actions/statuses';
import {
unblockAccount,
unmuteAccount,
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain,
} from '../../actions/domain_blocks';
import { initMuteModal } from '../../actions/mutes';
import { initBlockModal } from '../../actions/blocks';
import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4';
import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container';
import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.shift();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
ids.unshift(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return Immutable.List(descendantsIds);
});
const getTreeData = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
const getMore = (id, notRoot) => {
const replies = contextReplies.get(id);
const cur_status = statuses.get(id);
const text = cur_status.get('search_index').replace(/@\S+?\s/,'@..');
return {
statusId: id,
name: (text.length > 16 ? text.slice(0,13) + "..." : text) + (cur_status.get('media_attachments').size > 0 ? " [图片]" : ""),
children: replies ? Array.from(replies.map( i => getMore(i, true) )) : [],
}
}
let treeData = getMore(statusId, false)
return treeData;
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
let rootAcct;
let deep;
let treeData;
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
const root_status = ancestorsIds.size? getStatus(state, {id: ancestorsIds.get(0)}) : status; //error is directly visit url of non-root detailedStatus, feature!
rootAcct = root_status? root_status.getIn(['account', 'acct']) : -1;
if(rootAcct == '0') {
descendantsIds = state.getIn(['contexts', 'replies', status.get('id')]);
if(descendantsIds)
descendantsIds = descendantsIds.reverse();
}
else {
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
deep = rootAcct == '0' ? ancestorsIds.size : null;
treeData = rootAcct == '0' ? getTreeData(state, {id: status.get('id')}) : null;
}
return {
status,
deep,
ancestorsIds,
descendantsIds,
treeData,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
};
};
return mapStateToProps;
};
export default @injectIntl
@connect(makeMapStateToProps)
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
};
state = {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
loadedStatusId: undefined,
showTree: false,
svgWidth: 400,
activeNode: null
};
componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false;
this.props.dispatch(fetchStatus(nextProps.params.statusId));
}
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleFavouriteClick = (status) => {
if (status.get('favourited')) {
this.props.dispatch(unfavourite(status));
} else {
this.props.dispatch(favourite(status));
}
}
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
}
handleReplyClick = (status) => {
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
}
handleModalReblog = (status) => {
this.props.dispatch(reblog(status));
}
handleReblogClick = (status, e) => {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
}
}
}
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
}
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
}));
}
}
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
}
handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { media, index }));
}
handleOpenVideo = (media, options) => {
this.props.dispatch(openModal('VIDEO', { media, options }));
}
handleHotkeyOpenMedia = e => {
const status = this._properStatus();
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
this.props.dispatch(unmuteStatus(status.get('id')));
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
}
handleToggleHidden = (status) => {
if (status.get('hidden')) {
this.props.dispatch(revealStatus(status.get('id')));
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
}
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
if (status.get('hidden')) {
this.props.dispatch(revealStatus(statusIds));
} else {
this.props.dispatch(hideStatus(statusIds));
}
}
handleShowTree = () => {
this.setState({
activeNode: null,
showTree: !this.state.showTree
});
}
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
}
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
}
handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}
handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id')));
}
handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id')));
}
handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
}));
}
handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain));
}
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
}
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
}
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
}
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
}
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
}
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status.get('account'));
}
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
}
handleHotkeyToggleHidden = () => {
this.handleToggleHidden(this.props.status);
}
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
} else {
this._selectChild(index - 1, true);
}
}
}
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
}
}
_selectChild (index, align_top) {
const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
renderChildren (list, type) {
const { deep } = this.props;
return list.map((id,idx) => (
<StatusContainer
key={id}
id={id}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
deep={deep==null? null : (type == 'ance'? idx : deep+1)}
tree_type={deep==null? null : type}
/>
));
}
setRef = c => {
this.node = c;
}
componentDidUpdate () {
if (this._scrolledIntoView) {
return;
}
const { status, ancestorsIds } = this.props;
if (status && ancestorsIds && ancestorsIds.size > 0) {
const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
window.requestAnimationFrame(() => {
element.scrollIntoView(true);
});
this._scrolledIntoView = true;
}
}
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
handleNodeClick = (ev, node) => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${node}`);
}
render () {
let ancestors, descendants;
const { shouldUpdateScroll, status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn } = this.props;
const { fullscreen, showTree, svgWidth, activeNode } = this.state;
if (status === null) {
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator />
</Column>
);
}
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds, 'ance')}</div>;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds, 'desc')}</div>;
}
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
extraButton={(
<button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={deep ==null ? this.handleToggleAll : this.handleShowTree} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
)}
/>
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}>
<div className={classNames('scrollable', { fullscreen }, {'tree':deep!=null})} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)} ref={e=>{if(e) this.setState({svgWidth: e.clientWidth})}}>
{showTree ?
<Tree
data={treeData}
height={800}
width={svgWidth+80}
animated
keyProp = {"statusId"}
textProps={{
x: -9,
y: -7
}}
gProps={{
className: 'node',
onClick: this.handleNodeClick
}}
svgProps={{
className: 'tree-svg'
}}/>
:
<DetailedStatus
<<<<<<< HEAD
key={`detail-${status.get('id')}`}
=======
key={`details-${status.get('id')}`}
>>>>>>> master
status={status}
deep={deep}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
/>
}
<ActionBar
key={`action-bar-${status.get('id')}`}
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onUnmute={this.handleUnmuteClick}
onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick}
onUnblock={this.handleUnblockClick}
onBlockDomain={this.handleBlockDomainClick}
onUnblockDomain={this.handleUnblockDomainClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
{descendants}
</div>
</ScrollContainer>
</Column>
);
}
}

+ 0
- 247
app/javascript/mastodon/features/ui/components/columns_area.js.orig View File

@ -1,247 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ReactSwipeableViews from 'react-swipeable-views';
import TabsBar, { links, getIndex, getLink } from './tabs_bar';
import { Link } from 'react-router-dom';
import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
import {
Compose,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
HashtagTimeline,
DirectTimeline,
FavouritedStatuses,
BookmarkedStatuses,
ListTimeline,
Directory,
} from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll';
import TrendsContainer from '../../getting_started/containers/trends_container';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
'DIRECTORY': Directory,
};
const messages = defineMessages({
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
});
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
export default @(component => injectIntl(component, { withRef: true }))
class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
};
static propTypes = {
intl: PropTypes.object.isRequired,
columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool,
children: PropTypes.node,
};
state = {
shouldAnimate: false,
}
componentWillReceiveProps() {
this.setState({ shouldAnimate: false });
}
componentDidMount() {
if (!this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.isRtlLayout = document.getElementsByTagName('body')[0].classList.contains('rtl');
this.setState({ shouldAnimate: true });
}
componentWillUpdate(nextProps) {
if (this.props.singleColumn !== nextProps.singleColumn && nextProps.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
componentDidUpdate(prevProps) {
if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) {
this.node.addEventListener('wheel', this.handleWheel, detectPassiveEvents.hasSupport ? { passive: true } : false);
}
this.lastIndex = getIndex(this.context.router.history.location.pathname);
this.setState({ shouldAnimate: true });
}
componentWillUnmount () {
if (!this.props.singleColumn) {
this.node.removeEventListener('wheel', this.handleWheel);
}
}
handleChildrenContentChange() {
if (!this.props.singleColumn) {
const modifier = this.isRtlLayout ? -1 : 1;
this._interruptScrollAnimation = scrollRight(this.node, (this.node.scrollWidth - window.innerWidth) * modifier);
}
}
handleSwipe = (index) => {
this.pendingIndex = index;
const nextLinkTranslationId = links[index].props['data-preview-title-id'];
const currentLinkSelector = '.tabs-bar__link.active';
const nextLinkSelector = `.tabs-bar__link[data-preview-title-id="${nextLinkTranslationId}"]`;
// HACK: Remove the active class from the current link and set it to the next one
// React-router does this for us, but too late, feeling laggy.
document.querySelector(currentLinkSelector).classList.remove('active');
document.querySelector(nextLinkSelector).classList.add('active');
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null;
}
}
handleAnimationEnd = () => {
if (typeof this.pendingIndex === 'number') {
this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null;
}
}
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
return;
}
this._interruptScrollAnimation();
}
setRef = (node) => {
this.node = node;
}
renderView = (link, index) => {
const columnIndex = getIndex(this.context.router.history.location.pathname);
const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] });
const icon = link.props['data-preview-icon'];
const view = (index === columnIndex) ?
React.cloneElement(this.props.children) :
<ColumnLoading title={title} icon={icon} />;
return (
<div className='columns-area columns-area--mobile' key={index}>
{
link.props['data-preview-title-id'] == 'column.community' &&
<TrendsContainer />
}
{view}
</div>
);
}
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading />;
}
renderError = (props) => {
return <BundleColumnError {...props} />;
}
render () {
const { columns, children, singleColumn, isModalOpen, intl } = this.props;
const { shouldAnimate } = this.state;
const columnIndex = getIndex(this.context.router.history.location.pathname);
if (singleColumn) {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;
const content = columnIndex !== -1 ? (
<<<<<<< HEAD
<ReactSwipeableViews key='content' hysteresis={0.5} threshold={20} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
=======
<ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}>
>>>>>>> master
{links.map(this.renderView)}
</ReactSwipeableViews>
) : (
<div key='content' className='columns-area columns-area--mobile'>{children}</div>
);
return (
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--compositional'>
<div className='columns-area__panels__pane__inner'>
<ComposePanel />
</div>
</div>
<div className='columns-area__panels__main'>
<TabsBar key='tabs' />
{content}
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--start columns-area__panels__pane--navigational'>
<div className='columns-area__panels__pane__inner'>
<NavigationPanel />
</div>
</div>
{floatingActionButton}
</div>
);
}
return (
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {};
return (
<BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
</BundleContainer>
);
})}
{React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
</div>
);
}
}

+ 0
- 41
app/javascript/mastodon/features/ui/components/navigation_panel.js.orig View File

@ -1,41 +0,0 @@
import React from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon';
import { profile_directory, showTrends, treeRoot } from 'mastodon/initial_state';
import NotificationsCounterIcon from './notifications_counter_icon';
import FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel';
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
const NavigationPanel = () => (
<div className='navigation-panel'>
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
<NavLink className='column-link column-link--transparent' to={treeRoot} data-preview-title-id='column.tree' data-preview-icon='tree' ><Icon className='column-link__icon' id='tree' fixedWidth /><FormattedMessage id='tabs_bar.tree' defaultMessage='Tree' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
<FollowRequestsNavLink />
<NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<<<<<<< HEAD
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='heart' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
=======
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
>>>>>>> master
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}
<ListPanel />
<hr />
<a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
<a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
{showTrends && <div className='flex-spacer' />}
{showTrends && <TrendsContainer />}
</div>
);
export default withRouter(NavigationPanel);

+ 0
- 475
app/javascript/mastodon/locales/zh-HK.json.orig View File

@ -1,475 +0,0 @@
{
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "從名單中新增或移除",
"account.badges.bot": "機械人",
"account.badges.group": "群組",
"account.block": "封鎖 @{name}",
"account.block_domain": "隱藏來自 {domain} 的一切文章",
"account.blocked": "封鎖",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "取消關注請求",
"account.direct": "私訊 @{name}",
"account.domain_blocked": "服務站被隱藏",
"account.edit_profile": "修改個人資料",
"account.endorse": "在個人資料推薦對方",
"account.follow": "關注",
"account.followers": "關注的人",
"account.followers.empty": "尚沒有人關注這位使用者。",
"account.follows": "正關注",
"account.follows.empty": "這位使用者尚未關注任何使用者。",
"account.follows_you": "關注你",
"account.hide_reblogs": "隱藏 @{name} 的轉推",
"account.last_status": "上次活躍時間",
"account.link_verified_on": "此連結的所有權已在 {date} 檢查過",
"account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能關注這隻帳號的人。",
"account.media": "媒體",
"account.mention": "提及 @{name}",
"account.moved_to": "{name} 已經遷移到:",
"account.mute": "將 @{name} 靜音",
"account.mute_notifications": "將來自 @{name} 的通知靜音",
"account.muted": "靜音",
"account.never_active": "永不",
"account.posts": "文章",
"account.posts_with_replies": "包含回覆的文章",
"account.report": "舉報 @{name}",
"account.requested": "等候審批",
"account.share": "分享 @{name} 的個人資料",
"account.show_reblogs": "顯示 @{name} 的推文",
"account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "不再隱藏 {domain}",
"account.unendorse": "不再於個人資料頁面推薦對方",
"account.unfollow": "取消關注",
"account.unmute": "取消 @{name} 的靜音",
"account.unmute_notifications": "取消來自 @{name} 通知的靜音",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "請在 {retry_time, time, medium} 過後重試",
"alert.rate_limited.title": "已限速",
"alert.unexpected.message": "發生不可預期的錯誤。",
"alert.unexpected.title": "噢!",
"announcement.announcement": "公告",
"autosuggest_hashtag.per_week": "{count} / 週",
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
"bundle_column_error.body": "加載本組件出錯。",
"bundle_column_error.retry": "重試",
"bundle_column_error.title": "網絡錯誤",
"bundle_modal_error.close": "關閉",
"bundle_modal_error.message": "加載本組件出錯。",
"bundle_modal_error.retry": "重試",
"column.blocks": "封鎖用戶",
"column.bookmarks": "書籤",
"column.community": "本站時間軸",
"column.direct": "個人訊息",
"column.directory": "瀏覽個人資料",
"column.domain_blocks": "隱藏的服務站",
"column.favourites": "最愛的文章",
"column.follow_requests": "關注請求",
"column.home": "主頁",
"column.lists": "列表",
"column.mutes": "靜音名單",
"column.notifications": "通知",
"column.pins": "置頂文章",
"column.public": "跨站時間軸",
"column_back_button.label": "返回",
"column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "將欄右移",
"column_header.pin": "固定",
"column_header.show_settings": "顯示設定",
"column_header.unpin": "取下",
"column_subheading.settings": "設定",
"community.column_settings.local_only": "只有本地",
"community.column_settings.media_only": "僅媒體",
"community.column_settings.remote_only": "只有遠端",
"compose_form.direct_message_warning": "這文章只有被提及的用戶才可以看到。",
"compose_form.direct_message_warning_learn_more": "了解更多",
"compose_form.hashtag_warning": "這文章因為不是公開,所以不會被標籤搜索。只有公開的文章才會被標籤搜索。",
"compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
"compose_form.lock_disclaimer.lock": "公共",
"compose_form.placeholder": "你在想甚麼?",
"compose_form.poll.add_option": "新增選擇",
"compose_form.poll.duration": "投票期限",
"compose_form.poll.option_placeholder": "第 {number} 個選擇",
"compose_form.poll.remove_option": "移除此選擇",
"compose_form.poll.switch_to_multiple": "變更投票為允許多個選項",
"compose_form.poll.switch_to_single": "變更投票為允許單一選項",
"compose_form.publish": "發文",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "標記媒體為敏感內容",
"compose_form.sensitive.marked": "媒體被標示為敏感",
"compose_form.sensitive.unmarked": "媒體沒有被標示為敏感",
"compose_form.spoiler.marked": "文字被警告隱藏",
"compose_form.spoiler.unmarked": "文字沒有被隱藏",
"compose_form.spoiler_placeholder": "敏感警告訊息",
"confirmation_modal.cancel": "取消",
"confirmations.block.block_and_report": "封鎖並檢舉",
"confirmations.block.confirm": "封鎖",
"confirmations.block.message": "你確定要封鎖{name}嗎?",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這文章嗎?",
"confirmations.delete_list.confirm": "刪除",
"confirmations.delete_list.message": "你確定要永久刪除這列表嗎?",
"confirmations.domain_block.confirm": "隱藏整個網站",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。你從此將不會再看到該站的內容和通知。來自該站的關注者亦會被移除。",
"confirmations.logout.confirm": "登出",
"confirmations.logout.message": "確定要登出嗎?",
"confirmations.mute.confirm": "靜音",
"confirmations.mute.explanation": "這將會隱藏來自他們的貼文與通知,但是他們還是可以查閱你的貼文與關注你。",
"confirmations.mute.message": "你確定要將{name}靜音嗎?",
"confirmations.redraft.confirm": "刪除並編輯",
"confirmations.redraft.message": "你確定要刪除並重新編輯嗎?所有相關的回覆、轉推與最愛都會被刪除。",
"confirmations.reply.confirm": "回覆",
"confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
"confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
"conversation.delete": "刪除對話",
"conversation.mark_as_read": "標為已讀",
"conversation.open": "檢視對話",
"conversation.with": "與 {names}",
"directory.federated": "來自已知聯邦宇宙",
"directory.local": "僅來自 {domain}",
"directory.new_arrivals": "新貨",
"directory.recently_active": "最近活躍",
"embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
"embed.preview": "看上去會是這樣:",
"emoji_button.activity": "活動",
"emoji_button.custom": "自訂",
"emoji_button.flags": "旗幟",
"emoji_button.food": "飲飲食食",
"emoji_button.label": "加入表情符號",
"emoji_button.nature": "自然",
"emoji_button.not_found": "沒有表情符號!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物品",
"emoji_button.people": "人物",
"emoji_button.recent": "常用",
"emoji_button.search": "搜尋…",
"emoji_button.search_results": "搜尋結果",
"emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊景物",
"empty_column.account_timeline": "這裡還沒有嘟文!",
"empty_column.account_unavailable": "無法取得個人資料",
"empty_column.blocks": "你還沒有封鎖任何使用者。",
"empty_column.bookmarked_statuses": "你還沒建立任何書籤。這裡將會顯示你建立的書籤。",
"empty_column.community": "本站時間軸暫時未有內容,快寫一點東西來搶頭香啊!",
"empty_column.direct": "你沒有個人訊息。當你發出或接收個人訊息,就會在這裡出現。",
"empty_column.domain_blocks": "尚未隱藏任何網域。",
"empty_column.favourited_statuses": "你還沒收藏任何嘟文。這裡將會顯示你收藏的嘟文。",
"empty_column.favourites": "還沒有人收藏這則嘟文。這裡將會顯示被收藏的嘟文。",
"empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.public_timeline": "公共時間軸",
"empty_column.list": "這個列表暫時未有內容。",
"empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。",
"empty_column.mutes": "你尚未靜音任何使用者。",
"empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。",
"empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。",
"error.unexpected_crash.explanation": "由於發生系統故障或瀏覽器相容性問題,故無法正常顯示頁面。",
"error.unexpected_crash.next_steps": "請嘗試重新整理頁面。如果狀況沒有進展,你可以使用不同的瀏覽器或 Mastodon 應用程式來檢視。",
"errors.unexpected_crash.copy_stacktrace": "複製到剪貼簿",
"errors.unexpected_crash.report_issue": "舉報問題",
"follow_request.authorize": "批准",
"follow_request.reject": "拒絕",
"follow_requests.unlocked_explanation": "即便您的帳號未被鎖定,{domain} 的員工認為可能想要自己審核這些帳號的追蹤請求。",
"getting_started.developers": "開發者",
"getting_started.directory": "個人資料目錄",
"getting_started.documentation": "文件",
"getting_started.heading": "開始使用",
"getting_started.invite": "邀請使用者",
"getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。",
"getting_started.security": "帳戶安全",
"getting_started.terms": "服務條款",
"hashtag.column_header.tag_mode.all": "以及{additional}",
"hashtag.column_header.tag_mode.any": "或是{additional}",
"hashtag.column_header.tag_mode.none": "而無需{additional}",
"hashtag.column_settings.select.no_options_message": "找不到建議",
"hashtag.column_settings.select.placeholder": "輸入主題標籤…",
"hashtag.column_settings.tag_mode.all": "全部",
"hashtag.column_settings.tag_mode.any": "任一",
"hashtag.column_settings.tag_mode.none": "全不",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"home.column_settings.basic": "基本",
"home.column_settings.show_reblogs": "顯示被轉推的文章",
"home.column_settings.show_replies": "顯示回應文章",
"home.hide_announcements": "隱藏公告",
"home.show_announcements": "顯示公告",
"intervals.full.days": "{number, plural, one {# 天} other {# 天}}",
"intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}",
"intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}",
"introduction.federation.action": "下一步",
"introduction.federation.federated.headline": "站台聯盟",
"introduction.federation.federated.text": "來自聯盟宇宙中其他站台的公開嘟文將會在站點聯盟時間軸中顯示。",
"introduction.federation.home.headline": "首頁",
"introduction.federation.home.text": "你關注使用者的嘟文將會在首頁動態中顯示。你可以關注任何伺服器上的任何人!",
"introduction.federation.local.headline": "本機",
"introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本機時間軸中。",
"introduction.interactions.action": "Finish toot-orial!",
"introduction.interactions.favourite.headline": "關注",
"introduction.interactions.favourite.text": "您能儲存嘟文供稍候觀看,或者收藏嘟文,讓作者知道您喜歡這則嘟文。",
"introduction.interactions.reblog.headline": "轉嘟",
"introduction.interactions.reblog.text": "您能藉由轉嘟他人嘟文來分享給您的關注者。",
"introduction.interactions.reply.headline": "回覆",
"introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文,這麼做會把這些回覆串成一串對話。",
"introduction.welcome.action": "開始旅程吧!",
"introduction.welcome.headline": "第一步",
"introduction.welcome.text": "歡迎來到聯盟宇宙!等等你就可以廣播訊息及跨越各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},非常特別 - 它寄管了你的個人資料,所以請記住它的名字。",
"keyboard_shortcuts.back": "後退",
"keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單",
"keyboard_shortcuts.boost": "轉推",
"keyboard_shortcuts.column": "把標示移動到其中一列",
"keyboard_shortcuts.compose": "把標示移動到文字輸入區",
"keyboard_shortcuts.description": "描述",
"keyboard_shortcuts.direct": "開啟私訊欄",
"keyboard_shortcuts.down": "在列表往下移動",
"keyboard_shortcuts.enter": "打開文章",
<<<<<<< HEAD
"keyboard_shortcuts.favourite": "赞藏",
"keyboard_shortcuts.favourites": "to open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline",
=======
"keyboard_shortcuts.favourite": "收藏",
"keyboard_shortcuts.favourites": "開啟收藏名單",
"keyboard_shortcuts.federated": "開啟站點聯盟時間軸",
>>>>>>> master
"keyboard_shortcuts.heading": "鍵盤快速鍵",
"keyboard_shortcuts.home": "開啟首頁時間軸",
"keyboard_shortcuts.hotkey": "快速鍵",
"keyboard_shortcuts.legend": "顯示這個說明",
"keyboard_shortcuts.local": "開啟本機時間軸",
"keyboard_shortcuts.mention": "提及作者",
"keyboard_shortcuts.muted": "開啟靜音使用者名單",
"keyboard_shortcuts.my_profile": "開啟個人資料頁面",
"keyboard_shortcuts.notifications": "開啟通知欄",
"keyboard_shortcuts.open_media": "開啟媒體",
"keyboard_shortcuts.pinned": "開啟釘選的嘟文名單",
"keyboard_shortcuts.profile": "開啟作者的個人資料頁面",
"keyboard_shortcuts.reply": "回覆",
"keyboard_shortcuts.requests": "開啟關注請求名單",
"keyboard_shortcuts.search": "把標示移動到搜索",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "開啟「開始使用」欄位",
"keyboard_shortcuts.toggle_hidden": "顯示或隱藏被標為敏感的文字",
"keyboard_shortcuts.toggle_sensitivity": "顯示 / 隱藏媒體",
"keyboard_shortcuts.toot": "新的推文",
"keyboard_shortcuts.unfocus": "把標示移離文字輸入和搜索",
"keyboard_shortcuts.up": "在列表往上移動",
"lightbox.close": "關閉",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
"lightbox.view_context": "檢視內文",
"lists.account.add": "新增到列表",
"lists.account.remove": "從列表刪除",
"lists.delete": "刪除列表",
"lists.edit": "編輯列表",
"lists.edit.submit": "變更標題",
"lists.new.create": "新增列表",
"lists.new.title_placeholder": "新列表標題",
"lists.search": "從你關注的用戶中搜索",
"lists.subheading": "列表",
"load_pending": "{count, plural, other {# 個新項目}}",
"loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容",
"missing_indicator.sublabel": "無法找到內容",
"mute_modal.hide_notifications": "隱藏來自這用戶的通知嗎?",
"navigation_bar.apps": "封鎖的使用者",
"navigation_bar.blocks": "被你封鎖的用戶",
"navigation_bar.bookmarks": "書籤",
"navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.compose": "撰寫新嘟文",
"navigation_bar.direct": "個人訊息",
"navigation_bar.discover": "探索",
"navigation_bar.domain_blocks": "隱藏的服務站",
"navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.favourites": "最愛的內容",
"navigation_bar.filters": "靜音詞彙",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.follows_and_followers": "關注及關注者",
"navigation_bar.info": "關於本服務站",
"navigation_bar.keyboard_shortcuts": "鍵盤快速鍵",
"navigation_bar.lists": "列表",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "被你靜音的用戶",
"navigation_bar.personal": "個人",
"navigation_bar.pins": "置頂文章",
"navigation_bar.preferences": "偏好設定",
"navigation_bar.public_timeline": "跨站時間軸",
"navigation_bar.security": "安全",
"notification.favourite": "{name} 赞藏了你的文章",
"notification.follow": "{name} 開始關注你",
"notification.follow_request": "{name} 要求關注你",
"notification.mention": "{name} 提及你",
"notification.own_poll": "您的投票已結束",
"notification.poll": "您投過的投票已經結束",
"notification.reblog": "{name} 轉推你的文章",
"notifications.clear": "清空通知紀錄",
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
"notifications.column_settings.alert": "顯示桌面通知",
<<<<<<< HEAD
"notifications.column_settings.favourite": "赞藏了你的文章:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
=======
"notifications.column_settings.favourite": "收藏了你的文章:",
"notifications.column_settings.filter_bar.advanced": "顯示所有分類",
"notifications.column_settings.filter_bar.category": "快速過濾欄",
"notifications.column_settings.filter_bar.show": "顯示",
>>>>>>> master
"notifications.column_settings.follow": "關注你:",
"notifications.column_settings.follow_request": "新的關注請求:",
"notifications.column_settings.mention": "提及你:",
"notifications.column_settings.poll": "投票結果:",
"notifications.column_settings.push": "推送通知",
"notifications.column_settings.reblog": "轉推你的文章:",
"notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效",
"notifications.filter.all": "全部",
"notifications.filter.boosts": "轉嘟",
"notifications.filter.favourites": "最愛",
"notifications.filter.follows": "關注的使用者",
"notifications.filter.mentions": "提及",
"notifications.filter.polls": "投票結果",
"notifications.group": "{count} 條通知",
"poll.closed": "已關閉",
"poll.refresh": "重新整理",
"poll.total_people": "{count, plural, one {# 個投票} other {# 個投票}}",
"poll.total_votes": "{count, plural, one {# 個投票} other {# 個投票}}",
"poll.vote": "投票",
"poll.voted": "你已對此問題投票",
"poll_button.add_poll": "建立投票",
"poll_button.remove_poll": "移除投票",
"privacy.change": "調整私隱設定",
"privacy.direct.long": "只有提及的用戶能看到",
"privacy.direct.short": "私人訊息",
"privacy.private.long": "只有關注你用戶能看到",
"privacy.private.short": "關注者",
"privacy.public.long": "在公共時間軸顯示",
"privacy.public.short": "公共",
"privacy.unlisted.long": "公開,但不在公共時間軸顯示",
"privacy.unlisted.short": "公開",
"refresh": "重新整理",
"regeneration_indicator.label": "載入中……",
"regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!",
"relative_time.days": "{number}日",
"relative_time.hours": "{number}小時",
"relative_time.just_now": "剛剛",
"relative_time.minutes": "{number}分鐘",
"relative_time.seconds": "{number}秒",
"relative_time.today": "今天",
"reply_indicator.cancel": "取消",
"report.forward": "轉寄到 {target}",
"report.forward_hint": "這個帳戶屬於其他服務站。要向該服務站發送匿名的舉報訊息嗎?",
"report.hint": "這訊息會發送到你服務站的管理員。你可以提供舉報這個帳戶的理由:",
"report.placeholder": "額外訊息",
"report.submit": "提交",
"report.target": "舉報",
"search.placeholder": "搜尋",
"search_popout.search_format": "高級搜索格式",
"search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、赞藏、轉推和提及你的文章,以及符合的用戶名稱,帳號名稱和標籤。",
"search_popout.tips.hashtag": "標籤",
"search_popout.tips.status": "文章",
"search_popout.tips.text": "輸入簡單的文字,搜索符合的用戶名稱,帳號名稱和標籤",
"search_popout.tips.user": "用戶",
"search_results.accounts": "使用者",
"search_results.hashtags": "標籤",
"search_results.statuses": "文章",
"search_results.statuses_fts_disabled": "「依內容搜尋嘟文」未在此 Mastodon 伺服器啟用。",
"search_results.total": "{count, number} 項結果",
"status.admin_account": "開啟 @{name} 的管理介面",
"status.admin_status": "在管理介面開啟此嘟文",
"status.block": "封鎖 @{name}",
"status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉推",
"status.cannot_reblog": "這篇文章無法被轉推",
"status.copy": "將連結複製到嘟文中",
"status.delete": "刪除",
"status.detailed_status": "對話的詳細內容",
"status.direct": "私訊 @{name}",
"status.embed": "鑲嵌",
<<<<<<< HEAD
"status.favourite": "赞藏",
"status.filtered": "Filtered",
=======
"status.favourite": "收藏",
"status.filtered": "已過濾",
>>>>>>> master
"status.load_more": "載入更多",
"status.media_hidden": "隱藏媒體內容",
"status.mention": "提及 @{name}",
"status.more": "更多",
"status.mute": "把 @{name} 靜音",
"status.mute_conversation": "靜音對話",
"status.open": "展開文章",
"status.pin": "置頂到資料頁",
"status.pinned": "置頂文章",
"status.read_more": "閱讀更多",
"status.reblog": "轉推",
"status.reblog_private": "轉推到原讀者",
"status.reblogged_by": "{name} 轉推",
"status.reblogs.empty": "還沒有人轉嘟。如果有,會顯示在這裡。",
"status.redraft": "刪除並編輯",
"status.remove_bookmark": "移除書籤",
"status.reply": "回應",
"status.replyAll": "回應所有人",
"status.report": "舉報 @{name}",
"status.sensitive_warning": "敏感內容",
"status.share": "分享",
"status.show_less": "減少顯示",
"status.show_less_all": "減少顯示這類文章",
"status.show_more": "顯示更多",
"status.show_more_all": "顯示更多這類文章",
"status.show_thread": "顯示討論串",
"status.uncached_media_warning": "無法使用",
"status.unmute_conversation": "解禁對話",
"status.unpin": "解除置頂",
"suggestions.dismiss": "關閉建議",
"suggestions.header": "您可能對這些東西有興趣…",
"tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁",
"tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知",
"tabs_bar.search": "搜尋",
"time_remaining.days": "剩餘{number, plural, one {# 天數} other {# 天數}}",
"time_remaining.hours": "剩餘{number, plural, one {# 小時} other {# 小時}}",
"time_remaining.minutes": "剩餘{number, plural, one {# 分鐘} other {# 分鐘}}",
"time_remaining.moments": "剩餘時間",
"time_remaining.seconds": "剩餘 {number, plural, one {# 秒} other {# 秒}}",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} 位用戶在討論",
"trends.trending_now": "目前趨勢",
"ui.beforeunload": "如果你現在離開 Mastodon,你的草稿內容將會被丟棄。",
"upload_area.title": "將檔案拖放至此上載",
"upload_button.label": "上載媒體檔案",
"upload_error.limit": "已達到檔案上傳限制。",
"upload_error.poll": "不允許在投票上傳檔案。",
"upload_form.audio_description": "簡單描述內容給聽障人士",
"upload_form.description": "為視覺障礙人士添加文字說明",
"upload_form.edit": "編輯",
"upload_form.undo": "刪除",
"upload_form.video_description": "簡單描述給聽障或視障人士",
"upload_modal.analyzing_picture": "正在分析圖片…",
"upload_modal.apply": "套用",
"upload_modal.description_placeholder": "A quick brown fox 跳過那隻懶狗",
"upload_modal.detect_text": "從圖片偵測文字",
"upload_modal.edit_media": "編輯媒體",
"upload_modal.hint": "點擊或拖曳圓圈以選擇預覽縮圖。",
"upload_modal.preview_label": "預覽 ({ratio})",
"upload_progress.label": "上載中……",
"video.close": "關閉影片",
"video.download": "下載檔案",
"video.exit_fullscreen": "退出全熒幕",
"video.expand": "展開影片",
"video.fullscreen": "全熒幕",
"video.hide": "隱藏影片",
"video.mute": "靜音",
"video.pause": "暫停",
"video.play": "播放",
"video.unmute": "解除靜音"
}

+ 0
- 523
app/models/status.rb.orig View File

@ -1,523 +0,0 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: statuses
#
# id :bigint(8) not null, primary key
# uri :string
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# in_reply_to_id :bigint(8)
# reblog_of_id :bigint(8)
# url :string
# sensitive :boolean default(FALSE), not null
# visibility :integer default("public"), not null
# spoiler_text :text default(""), not null
# reply :boolean default(FALSE), not null
# language :string
# conversation_id :bigint(8)
# local :boolean
# account_id :bigint(8) not null
# application_id :bigint(8)
# in_reply_to_account_id :bigint(8)
# poll_id :bigint(8)
# deleted_at :datetime
#
class Status < ApplicationRecord
before_destroy :unlink_from_conversations
include Discard::Model
include Paginable
include Cacheable
include StatusThreadingConcern
include RateLimitable
rate_limit by: :account, family: :statuses
self.discard_column = :deleted_at
# If `override_timestamps` is set at creation time, Snowflake ID creation
# will be based on current time instead of `created_at`
attr_accessor :override_timestamps
update_index('statuses#status', :proper)
enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
belongs_to :account, inverse_of: :statuses
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
belongs_to :conversation, optional: true
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy
has_many :replies, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :thread
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
has_one :notification, as: :activity, dependent: :destroy
has_one :status_stat, inverse_of: :status
has_one :poll, inverse_of: :status, dependent: :destroy
validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: -> { with_media? || reblog? }
validates_with StatusLengthValidator
validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
accepts_nested_attributes_for :poll
default_scope { recent.kept }
scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where(local: false).where.not(uri: nil) }
scope :local, -> { where(local: true).or(where(uri: nil)) }
<<<<<<< HEAD
#scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_replies, -> { where('statuses.reply = FALSE') }
#scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :without_reblogs, -> { joins('INNER JOIN statuses ori ON (statuses.reblog_of_id is NULL AND ori.id = statuses.id) OR ori.id = statuses.reblog_of_id')
.where('ori.account_id = statuses.account_id') }
=======
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 IS NULL') }
>>>>>>> master
scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
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 }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
scope :tagged_with_all, ->(tags) {
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
end
}
scope :tagged_with_none, ->(tags) {
Array(tags).map(&:id).map(&:to_i).reduce(self) do |result, id|
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
.where("t#{id}.tag_id IS NULL")
end
}
cache_associated :application,
:media_attachments,
:conversation,
:status_stat,
:tags,
:preview_cards,
:preloadable_poll,
account: :account_stat,
active_mentions: { account: :account_stat },
reblog: [
:application,
:tags,
:preview_cards,
:media_attachments,
:conversation,
:status_stat,
:preloadable_poll,
account: :account_stat,
active_mentions: { account: :account_stat },
],
thread: { account: :account_stat }
delegate :domain, to: :account, prefix: true
REAL_TIME_WINDOW = 6.hours
def searchable_by(preloaded = nil)
ids = []
ids << account_id if local?
if preloaded.nil?
ids += mentions.where(account: Account.local, silent: false).pluck(:account_id)
ids += favourites.where(account: Account.local).pluck(:account_id)
ids += reblogs.where(account: Account.local).pluck(:account_id)
ids += bookmarks.where(account: Account.local).pluck(:account_id)
else
ids += preloaded.mentions[id] || []
ids += preloaded.favourites[id] || []
ids += preloaded.reblogs[id] || []
ids += preloaded.bookmarks[id] || []
end
ids.uniq
end
def reply?
!in_reply_to_id.nil? || attributes['reply']
end
def local?
attributes['local'] || uri.nil?
end
def reblog?
!reblog_of_id.nil?
end
def within_realtime_window?
created_at >= REAL_TIME_WINDOW.ago
end
def verb
if destroyed?
:delete
else
reblog? ? :share : :post
end
end
def object_type
reply? ? :comment : :note
end
def proper
reblog? ? reblog : self
end
def content
proper.text
end
def target
reblog
end
def preview_card
preview_cards.first
end
def hidden?
!distributable?
end
def distributable?
public_visibility? || unlisted_visibility?
end
alias sign? distributable?
def with_media?
media_attachments.any?
end
def non_sensitive_with_media?
!sensitive? && with_media?
end
def reported?
@reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists?
end
def emojis
return @emojis if defined?(@emojis)
fields = [spoiler_text, text]
fields += preloadable_poll.options unless preloadable_poll.nil?
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
end
def mark_for_mass_destruction!
@marked_for_mass_destruction = true
end
def marked_for_mass_destruction?
@marked_for_mass_destruction
end
def replies_count
status_stat&.replies_count || 0
end
def reblogs_count
status_stat&.reblogs_count || 0
end
def favourites_count
status_stat&.favourites_count || 0
end
def increment_count!(key)
update_status_stat!(key => public_send(key) + 1)
end
def decrement_count!(key)
update_status_stat!(key => [public_send(key) - 1, 0].max)
end
after_create_commit :increment_counter_caches
after_destroy_commit :decrement_counter_caches
after_create_commit :store_uri, if: :local?
after_create_commit :update_statistics, if: :local?
around_create Mastodon::Snowflake::Callbacks
before_validation :prepare_contents, if: :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
before_validation :set_local
after_create :set_poll_id
class << self
def selectable_visibilities
visibilities.keys - %w(direct limited)
end
def in_chosen_languages(account)
where(language: nil).or where(language: account.chosen_languages)
end
def as_public_timeline(account = nil, local_only = false)
query = timeline_scope(local_only).without_replies
apply_timeline_filters(query, account, [:local, true].include?(local_only))
end
def as_tag_timeline(tag, account = nil, local_only = false)
query = timeline_scope(local_only).tagged_with(tag)
apply_timeline_filters(query, account, local_only)
end
def as_outbox_timeline(account)
where(account: account, visibility: :public)
end
def favourites_map(status_ids, account_id)
Favourite.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
end
def bookmarks_map(status_ids, account_id)
Bookmark.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |f| [f.status_id, true] }.to_h
end
def reblogs_map(status_ids, account_id)
unscoped.select('reblog_of_id').where(reblog_of_id: status_ids).where(account_id: account_id).each_with_object({}) { |s, h| h[s.reblog_of_id] = true }
end
def mutes_map(conversation_ids, account_id)
ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).each_with_object({}) { |m, h| h[m.conversation_id] = true }
end
def pins_map(status_ids, account_id)
StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |p, h| h[p.status_id] = true }
end
def reload_stale_associations!(cached_items)
account_ids = []
cached_items.each do |item|
account_ids << item.account_id
account_ids << item.reblog.account_id if item.reblog?
end
account_ids.uniq!
return if account_ids.empty?
accounts = Account.where(id: account_ids).includes(:account_stat).each_with_object({}) { |a, h| h[a.id] = a }
cached_items.each do |item|
item.account = accounts[item.account_id]
item.reblog.account = accounts[item.reblog.account_id] if item.reblog?
end
end
def permitted_for(target_account, account)
visibility = [:public, :unlisted]
if account.nil?
where(visibility: visibility)
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
none
elsif account.id == target_account.id # author can see own stuff
all
else
# followers can see followers-only stuff, but also things they are mentioned in.
# non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
visibility.push(:private) if account.following?(target_account)
scope = left_outer_joins(:reblog)
scope.where(visibility: visibility)
.or(scope.where(id: account.mentions.select(:status_id)))
.merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids })))
end
end
def from_text(text)
return [] if text.blank?
text.scan(FetchLinkCardService::URL_PATTERN).map(&:first).uniq.map do |url|
status = begin
if TagManager.instance.local_url?(url)
ActivityPub::TagManager.instance.uri_to_resource(url, Status)
else
EntityCache.instance.status(url)
end
end
status&.distributable? ? status : nil
end.compact
end
private
def timeline_scope(scope = false)
starting_scope = case scope
when :local, true
Status.local
when :remote
Status.remote
else
Status
end
starting_scope
.with_public_visibility
.without_reblogs
end
def apply_timeline_filters(query, account, local_only)
if account.nil?
filter_timeline_default(query)
else
filter_timeline_for_account(query, account, local_only)
end
end
def filter_timeline_for_account(query, account, local_only)
query = query.not_excluded_by_account(account)
query = query.not_domain_blocked_by_account(account) unless local_only
query = query.in_chosen_languages(account) if account.chosen_languages.present?
query.merge(account_silencing_filter(account))
end
def filter_timeline_default(query)
query.excluding_silenced_accounts
end
def account_silencing_filter(account)
if account.silenced?
including_myself = left_outer_joins(:account).where(account_id: account.id).references(:accounts)
excluding_silenced_accounts.or(including_myself)
else
excluding_silenced_accounts
end
end
end
def status_stat
super || build_status_stat
end
private
def update_status_stat!(attrs)
return if marked_for_destruction? || destroyed?
status_stat.update(attrs)
end
def store_uri
update_column(:uri, ActivityPub::TagManager.instance.uri_for(self)) if uri.nil?
end
def prepare_contents
text&.strip!
spoiler_text&.strip!
end
def set_reblog
self.reblog = reblog.reblog if reblog? && reblog.reblog?
end
def set_poll_id
update_column(:poll_id, poll.id) unless poll.nil?
end
def set_visibility
self.visibility = reblog.visibility if reblog? && visibility.nil?
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
self.sensitive = false if sensitive.nil?
end
def set_conversation
self.thread = thread.reblog if thread&.reblog?
self.reply = !(in_reply_to_id.nil? && thread.nil?) unless reply
if reply? && !thread.nil?
self.in_reply_to_account_id = carried_over_reply_to_account_id
self.conversation_id = thread.conversation_id if conversation_id.nil?
elsif conversation_id.nil?
self.conversation = Conversation.new
end
end
def carried_over_reply_to_account_id
if thread.account_id == account_id && thread.reply?
thread.in_reply_to_account_id
else
thread.account_id
end
end
def set_local
self.local = account.local?
end
def update_statistics
return unless distributable?
ActivityTracker.increment('activity:statuses:local')
end
def increment_counter_caches
return if direct_visibility?
account&.increment_count!(:statuses_count)
reblog&.increment_count!(:reblogs_count) if reblog?
thread&.increment_count!(:replies_count) if in_reply_to_id.present? && distributable?
end
def decrement_counter_caches
return if direct_visibility? || marked_for_mass_destruction?
account&.decrement_count!(:statuses_count)
reblog&.decrement_count!(:reblogs_count) if reblog?
thread&.decrement_count!(:replies_count) if in_reply_to_id.present? && distributable?
end
def unlink_from_conversations
return unless direct_visibility?
mentioned_accounts = mentions.includes(:account).map(&:account)
inbox_owners = mentioned_accounts.select(&:local?) + (account.local? ? [account] : [])
inbox_owners.each do |inbox_owner|
AccountConversation.remove_status(inbox_owner, self)
end
end
end

+ 1
- 1
app/serializers/initial_state_serializer.rb View File

@ -22,7 +22,7 @@ class InitialStateSerializer < ActiveModel::Serializer
mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory,
trends: Setting.trends,
tree_root: ENV['TREE_ADDRESS'],
tree_root: Rails.configuration.x.tree_address,
pinned_info: Setting.site_description,
}

+ 0
- 182
app/services/fetch_link_card_service.rb.orig View File

@ -1,182 +0,0 @@
# frozen_string_literal: true
class FetchLinkCardService < BaseService
URL_PATTERN = %r{
( # $1 URL
(https?:\/\/) # $2 Protocol (required)
(#{Twitter::Regex[:valid_domain]}) # $3 Domain(s)
(?::(#{Twitter::Regex[:valid_port_number]}))? # $4 Port number (optional)
(/#{Twitter::Regex[:valid_url_path]}*)? # $5 URL Path and anchor
(\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? # $6 Query String
)
}iox
def call(status)
@status = status
@url = parse_urls
return if @url.nil? || @status.preview_cards.any?
@url = @url.to_s
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@card = PreviewCard.find_by(url: @url)
process_url if @card.nil? || @card.updated_at <= 2.weeks.ago || @card.missing_image?
else
raise Mastodon::RaceConditionError
end
end
attach_card if @card&.persisted?
rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
Rails.logger.debug "Error fetching link #{@url}: #{e}"
nil
end
private
def process_url
@card ||= PreviewCard.new(url: @url)
<<<<<<< HEAD
Request.new(:get, @url).perform do |res|
if res.code == 200 && res.mime_type.end_with?('text/html')
=======
attempt_oembed || attempt_opengraph
end
def html
return @html if defined?(@html)
Request.new(:get, @url).add_headers('Accept' => 'text/html', 'User-Agent' => Mastodon::Version.user_agent + ' Bot').perform do |res|
if res.code == 200 && res.mime_type == 'text/html'
>>>>>>> master
@html = res.body_with_limit
@html_charset = res.charset
else
@html = nil
@html_charset = nil
end
end
end
def attach_card
@status.preview_cards << @card
Rails.cache.delete(@status)
end
def parse_urls
if @status.local?
urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
else
html = Nokogiri::HTML(@status.text)
links = html.css('a')
urls = links.map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.compact.map(&:normalize).compact
end
urls.reject { |uri| bad_url?(uri) }.first
end
def bad_url?(uri)
# Avoid local instance URLs and invalid URLs
uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme)
end
def mention_link?(a)
@status.mentions.any? do |mention|
a['href'] == ActivityPub::TagManager.instance.url_for(mention.account)
end
end
def skip_link?(a)
# Avoid links for hashtags and mentions (microformats)
a['rel']&.include?('tag') || a['class']&.match?(/u-url|h-card/) || mention_link?(a)
end
def attempt_oembed
service = FetchOEmbedService.new
url_domain = Addressable::URI.parse(@url).normalized_host
cached_endpoint = Rails.cache.read("oembed_endpoint:#{url_domain}")
embed = service.call(@url, cached_endpoint: cached_endpoint) unless cached_endpoint.nil?
embed ||= service.call(@url, html: html) unless html.nil?
return false if embed.nil?
url = Addressable::URI.parse(service.endpoint_url)
@card.type = embed[:type]
@card.title = embed[:title] || ''
@card.author_name = embed[:author_name] || ''
@card.author_url = embed[:author_url].present? ? (url + embed[:author_url]).to_s : ''
@card.provider_name = embed[:provider_name] || ''
@card.provider_url = embed[:provider_url].present? ? (url + embed[:provider_url]).to_s : ''
@card.width = 0
@card.height = 0
case @card.type
when 'link'
@card.image_remote_url = (url + embed[:thumbnail_url]).to_s if embed[:thumbnail_url].present?
when 'photo'
return false if embed[:url].blank?
@card.embed_url = (url + embed[:url]).to_s
@card.image_remote_url = (url + embed[:url]).to_s
@card.width = embed[:width].presence || 0
@card.height = embed[:height].presence || 0
when 'video'
@card.width = embed[:width].presence || 0
@card.height = embed[:height].presence || 0
@card.html = Formatter.instance.sanitize(embed[:html], Sanitize::Config::MASTODON_OEMBED)
@card.image_remote_url = (url + embed[:thumbnail_url]).to_s if embed[:thumbnail_url].present?
when 'rich'
# Most providers rely on <script> tags, which is a no-no
return false
end
@card.save_with_optional_image!
end
def attempt_opengraph
return if html.nil?
detector = CharlockHolmes::EncodingDetector.new
detector.strip_tags = true
guess = detector.detect(@html, @html_charset)
encoding = guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil
page = Nokogiri::HTML(@html, nil, encoding)
player_url = meta_property(page, 'twitter:player')
if player_url && !bad_url?(Addressable::URI.parse(player_url))
@card.type = :video
@card.width = meta_property(page, 'twitter:player:width') || 0
@card.height = meta_property(page, 'twitter:player:height') || 0
@card.html = content_tag(:iframe, nil, src: player_url,
width: @card.width,
height: @card.height,
allowtransparency: 'true',
scrolling: 'no',
frameborder: '0')
else
@card.type = :link
end
@card.title = meta_property(page, 'og:title').presence || page.at_xpath('//title')&.content || ''
@card.description = meta_property(page, 'og:description').presence || meta_property(page, 'description') || ''
@card.image_remote_url = (Addressable::URI.parse(@url) + meta_property(page, 'og:image')).to_s if meta_property(page, 'og:image')
return if @card.title.blank? && @card.html.blank?
@card.save_with_optional_image!
end
def meta_property(page, property)
page.at_xpath("//meta[contains(concat(' ', normalize-space(@property), ' '), ' #{property} ')]")&.attribute('content')&.value || page.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
end
def lock_options
{ redis: Redis.current, key: "fetch:#{@url}" }
end
end

+ 0
- 39
app/views/about/_registration.html.haml.orig View File

@ -1,39 +0,0 @@
.simple_form__overlay-area{ class: (closed_registrations? && @instance_presenter.closed_registrations_message.present?) ? 'simple_form__overlay-area__blurred' : '' }
= simple_form_for(new_user, url: user_registration_path, namespace: 'registration') do |f|
%p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))
.fields-group
= f.simple_fields_for :account do |account_fields|
= account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username') }, append: "(@#{site_hostname})", hint: false, disabled: closed_registrations?
- email_domain = Rails.configuration.x.email_default_domain
= f.input :email, wrapper: :with_label, label: false, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off', :type => 'text' }, append: "@#{email_domain}", hint: false, disabled: closed_registrations?
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
- if approved_registrations?
.fields-group
= f.simple_fields_for :invite_request do |invite_request_fields|
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
.fields-group
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), disabled: closed_registrations?
.actions
= f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?
<<<<<<< HEAD
- if closed_registrations? && @instance_presenter.closed_registrations_message.present?
.simple_form__overlay-area__overlay
.simple_form__overlay-area__overlay__content.rich-formatting
.block-icon= fa_icon 'warning'
= @instance_presenter.closed_registrations_message.html_safe
= javascript_include_tag "/auto_comp_email.js"
=======
- if closed_registrations? && @instance_presenter.closed_registrations_message.present?
.simple_form__overlay-area__overlay
.simple_form__overlay-area__overlay__content.rich-formatting
.block-icon= fa_icon 'warning'
= @instance_presenter.closed_registrations_message.html_safe
>>>>>>> master

+ 0
- 91
app/views/about/show.html.haml.orig View File

@ -1,91 +0,0 @@
- content_for :page_title do
= site_hostname
- content_for :header_tags do
%link{ rel: 'canonical', href: about_url }/
= render partial: 'shared/og'
.landing
.landing__brand
= link_to root_url, class: 'brand' do
= svg_logo_full
%span.brand__tagline=t 'about.tagline'
.landing__grid
.landing__grid__column.landing__grid__column-registration
.box-widget
= render 'registration'
.directory
- if Setting.profile_directory
.directory__tag
= optional_link_to Setting.profile_directory, explore_path do
%h4
= fa_icon 'address-book fw'
= t('about.discover_users')
%small= t('about.browse_directory')
.avatar-stack
- @instance_presenter.sample_accounts.each do |account|
= image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, alt: '', class: 'account__avatar'
- if Setting.timeline_preview
.directory__tag
= optional_link_to Setting.timeline_preview, public_timeline_path do
%h4
= fa_icon 'globe fw'
= t('about.see_whats_happening')
%small= t('about.browse_public_posts')
.directory__tag
<<<<<<< HEAD
= link_to 'https://closed.social/apps', target: '_blank', rel: 'noopener' do
=======
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do
>>>>>>> master
%h4
= fa_icon 'tablet fw'
= t('about.get_apps')
%small= t('about.apps_platforms')
.landing__grid__column.landing__grid__column-login
.box-widget
= render 'login'
.hero-widget
.hero-widget__img
= image_tag @instance_presenter.hero&.file&.url || @instance_presenter.thumbnail&.file&.url || asset_pack_path('media/images/preview.jpg'), alt: @instance_presenter.site_title
.hero-widget__text
%p
= @instance_presenter.site_short_description.html_safe.presence || t('about.about_mastodon_html')
= link_to about_more_path do
= t('about.learn_more')
= fa_icon 'angle-double-right'
.hero-widget__footer
.hero-widget__footer__column
%h4= t 'about.administered_by'
= account_link_to @instance_presenter.contact_account
.hero-widget__footer__column
%h4= t 'about.server_stats'
.hero-widget__counters__wrapper
.hero-widget__counter
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
%span= t 'about.user_count_after', count: @instance_presenter.user_count
<<<<<<< HEAD
/.hero-widget__counter{ style: 'width: 50%' }
/ %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
/ %span
/ = t 'about.active_count_after'
/ %abbr{ title: t('about.active_footnote') } *
=======
.hero-widget__counter
%strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
%span
= t 'about.active_count_after'
%abbr{ title: t('about.active_footnote') } *
>>>>>>> master

+ 0
- 91
app/views/accounts/show.html.haml.orig View File

@ -1,91 +0,0 @@
- content_for :page_title do
<<<<<<< HEAD
= user_signed_in? ? "#{display_name(@account)} (@#{@account.local_username_and_domain})" : t('accounts.unsigned')
=======
= "#{display_name(@account)} (#{acct(@account)})"
>>>>>>> master
- content_for :header_tags do
- if @account.user&.setting_noindex
%meta{ name: 'robots', content: 'noindex, noarchive' }/
%link{ rel: 'alternate', type: 'application/rss+xml', href: @rss_url }/
%link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
- if @older_url
%link{ rel: 'next', href: @older_url }/
- if @newer_url
%link{ rel: 'prev', href: @newer_url }/
= opengraph 'og:type', 'profile'
= render 'og', account: @account, url: short_account_url(@account, only_path: false)
= render 'header', account: @account, with_bio: true
.grid
.column-0
.h-feed
%data.p-name{ value: "#{@account.username} on #{site_hostname}" }/
.account__section-headline
= active_link_to t('accounts.posts_tab_heading'), short_account_url(@account)
= active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account)
= active_link_to t('accounts.media'), short_account_media_url(@account)
- if not user_signed_in? || @account.blocking?(current_account)
.nothing-here.nothing-here--under-tabs= t('accounts.unavailable')
- elsif @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
.activity-stream.activity-stream--under-tabs
- if params[:page].to_i.zero?
= render partial: 'statuses/status', collection: @pinned_statuses, as: :status, locals: { pinned: true }
- if @newer_url
.entry= link_to_more @newer_url
= render partial: 'statuses/status', collection: @statuses, as: :status
- if @older_url
.entry= link_to_more @older_url
- if user_signed_in?
.column-1
- if @account.memorial?
.memoriam-widget= t('in_memoriam_html')
- elsif @account.moved?
= render 'moved', account: @account
= render 'bio', account: @account
- if @endorsed_accounts.empty? && @account.id == current_account&.id
.placeholder-widget= t('accounts.endorsements_hint')
- elsif !@endorsed_accounts.empty?
.endorsements-widget
%h4= t 'accounts.choices_html', name: content_tag(:bdi, display_name(@account, custom_emojify: true))
- @endorsed_accounts.each do |account|
= account_link_to account
- if @featured_hashtags.empty? && @account.id == current_account&.id
.placeholder-widget
= t('accounts.featured_tags_hint')
= link_to settings_featured_tags_path do
= t('featured_tags.add_new')
= fa_icon 'chevron-right fw'
- else
- @featured_hashtags.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.formatted{ 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'

+ 0
- 87
app/views/statuses/_detailed_status.html.haml.orig View File

@ -1,87 +0,0 @@
.detailed-status.detailed-status--flex{ class: "detailed-status-#{status.visibility}" }
.p-author.h-card
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'detailed-status__display-name u-url', target: stream_link_target, rel: 'noopener' do
.detailed-status__display-avatar
- if current_account&.user&.setting_auto_play_gif || autoplay
= image_tag status.account.avatar_original_url, alt: '', class: 'account__avatar u-photo'
- else
= image_tag status.account.avatar_static_url, alt: '', class: 'account__avatar u-photo'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
= account_action_button(status.account)
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
<<<<<<< HEAD
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
= (user_signed_in? || status.text.start_with?('[pub]')) ? (Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)) : t('accounts.unsigned')
- if status.preloadable_poll && (user_signed_in? || status.text.start_with?('[pub]'))
=======
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
>>>>>>> master
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
- if !status.media_attachments.empty? && (user_signed_in? || status.text.start_with?('[pub]'))
- if status.media_attachments.first.video?
- video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.media_attachments.first.audio?
- audio = status.media_attachments.first
= react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 670, height: 380, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 380, sensitive: status.sensitive?, standalone: true, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
= react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
.detailed-status__meta
%data.dt-published{ value: status.created_at.to_time.iso8601 }
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
·
%span.detailed-status__visibility-icon
= visibility_icon status
·
- if status.application && @account.user&.setting_show_application
- if status.application.website.blank?
%strong.detailed-status__application= status.application.name
- else
= link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener noreferrer'
·
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
- if status.in_reply_to_id.nil?
= fa_icon('comment')
- else
= fa_icon('comments')
%span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
= " "
·
- if status.public_visibility? || status.unlisted_visibility?
= link_to remote_interaction_path(status, type: :reblog), class: 'modal-button detailed-status__link' do
= fa_icon('retweet')
%span.detailed-status__reblogs>= number_to_human status.reblogs_count, strip_insignificant_zeros: true
= " "
·
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
= fa_icon('heart')
%span.detailed-status__favorites>= number_to_human status.favourites_count, strip_insignificant_zeros: true
= " "
- if user_signed_in?
·
= link_to t('statuses.open_in_web'), web_url("statuses/#{status.id}"), class: 'detailed-status__application', target: '_blank'

+ 0
- 81
app/views/statuses/_simple_status.html.haml.orig View File

@ -1,81 +0,0 @@
.status{ class: "status-#{status.visibility}" }
.status__info
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__relative-time u-url u-uid', target: stream_link_target, rel: 'noopener noreferrer' do
%time.time-ago{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
%data.dt-published{ value: status.created_at.to_time.iso8601 }
%span.status__visibility-icon
= visibility_icon status
.p-author.h-card
= link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener noreferrer' do
.status__avatar
%div
- if current_account&.user&.setting_auto_play_gif || autoplay
= image_tag status.account.avatar_original_url, alt: '', class: 'u-photo account__avatar'
- else
= image_tag status.account.avatar_static_url, alt: '', class: 'u-photo account__avatar'
%span.display-name
%bdi
%strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true, autoplay: autoplay)
&nbsp;
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
<<<<<<< HEAD
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
= (user_signed_in? || status.text.start_with?('[pub]')) ? (Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)) : t('accounts.unsigned')
- if status.preloadable_poll && (user_signed_in? || status.text.start_with?('[pub]'))
=======
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
>>>>>>> master
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
- if !status.media_attachments.empty? && (user_signed_in? || status.text.start_with?('[pub]'))
- if status.media_attachments.first.video?
- video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: status.sensitive?, width: 610, height: 343, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.media_attachments.first.audio?
- audio = status.media_attachments.first
= react_component :audio, src: audio.file.url(:original), poster: audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url, backgroundColor: audio.file.meta.dig('colors', 'background'), foregroundColor: audio.file.meta.dig('colors', 'foreground'), accentColor: audio.file.meta.dig('colors', 'accent'), width: 610, height: 343, alt: audio.description, duration: audio.file.meta.dig('original', 'duration') do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 343, sensitive: status.sensitive?, autoplay: autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- elsif status.preview_card
= react_component :card, sensitive: status.sensitive?, 'maxDescription': 160, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json
- if !status.in_reply_to_id.nil? && status.in_reply_to_account_id == status.account.id
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'status__content__read-more-button', target: stream_link_target, rel: 'noopener noreferrer' do
= t 'statuses.show_thread'
.status__action-bar
.status__action-bar__counter
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do
- if status.in_reply_to_id.nil?
= fa_icon 'comment fw'
- else
= fa_icon 'comments fw'
.status__action-bar__counter__label= obscured_counter status.replies_count
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
- if status.distributable?
= fa_icon 'retweet fw'
- elsif status.private_visibility? || status.limited_visibility?
= fa_icon 'lock fw'
- else
= fa_icon 'envelope fw'
<<<<<<< HEAD
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
= fa_icon 'heart fw'
=======
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
= fa_icon 'star fw'
>>>>>>> master

+ 0
- 22
app/views/tags/show.html.haml.orig View File

@ -1,22 +0,0 @@
- content_for :page_title do
= "##{@tag.name}"
- content_for :header_tags do
%meta{ name: 'robots', content: 'noindex' }/
%link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
= javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
= render 'og'
.page-header
%h1= "##{@tag.name}"
%p= t('about.about_hashtag_html', hashtag: @tag.name)
<<<<<<< HEAD
- if user_signed_in?
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name)) }}
#modal-container
=======
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(hashtag: @tag.name, local: @local)) }}
#modal-container
>>>>>>> master

+ 0
- 9
config/initializers/2_whitelist_mode.rb.orig View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
Rails.application.configure do
<<<<<<< HEAD
config.x.whitelist_mode = ENV['WHITELIST_MODE'] != 'false'
=======
config.x.whitelist_mode = (ENV['LIMITED_FEDERATION_MODE'] || ENV['WHITELIST_MODE']) == 'true'
>>>>>>> master
end

+ 0
- 1
config/initializers/blacklists.rb View File

@ -3,5 +3,4 @@
Rails.application.configure do
config.x.email_domains_blacklist = (ENV['EMAIL_DOMAIN_DENYLIST'] || ENV['EMAIL_DOMAIN_BLACKLIST']) || ''
config.x.email_domains_whitelist = (ENV['EMAIL_DOMAIN_ALLOWLIST'] || ENV['EMAIL_DOMAIN_WHITELIST']) || ''
config.x.email_default_domain = ENV.fetch('EMAIL_DEFAULT_DOMAIN') { '???.edu.cn' }
end

+ 0
- 12
config/initializers/blacklists.rb.orig View File

@ -1,12 +0,0 @@
# frozen_string_literal: true
Rails.application.configure do
<<<<<<< HEAD
config.x.email_domains_blacklist = ENV.fetch('EMAIL_DOMAIN_BLACKLIST') { 'mvrht.com' }
config.x.email_domains_whitelist = ENV.fetch('EMAIL_DOMAIN_WHITELIST') { '' }
config.x.email_default_domain = ENV.fetch('EMAIL_DEFAULT_DOMAIN') { '???.edu.cn' }
=======
config.x.email_domains_blacklist = (ENV['EMAIL_DOMAIN_DENYLIST'] || ENV['EMAIL_DOMAIN_BLACKLIST']) || ''
config.x.email_domains_whitelist = (ENV['EMAIL_DOMAIN_ALLOWLIST'] || ENV['EMAIL_DOMAIN_WHITELIST']) || ''
>>>>>>> master
end

+ 7
- 0
config/initializers/new_features.rb View File

@ -0,0 +1,7 @@
Rails.application.configure do
config.x.email_default_domain = ENV.fetch('EMAIL_DEFAULT_DOMAIN') { '???.edu.cn' }
config.x.tree_address = ENV.fetch('TREE_ADDRESS') {''}
config.x.anon_tag = ENV.fetch('ANON_TAG') {'[mask]'}
config.x.anon_acc = ENV.fetch('ANON_ACC') {''}
end

+ 0
- 158
config/locales/doorkeeper.zh-TW.yml.orig View File

@ -1,159 +0,0 @@
zh-TW:
activerecord:
attributes:
doorkeeper/application:
name: 應用程式名稱
redirect_uri: 重新導向 URI
scopes: 範圍
website: 應用程式網頁
errors:
models:
doorkeeper/application:
attributes:
redirect_uri:
fragment_present: 不能包含 fragment。
invalid_uri: 必須是正確的 URI。
relative_uri: 必須為絕對 URI。
secured_uri: 必須是 HTTPS/SSL URI。
doorkeeper:
applications:
buttons:
authorize: 授權
cancel: 取消
destroy: 移除
edit: 編輯
submit: 送出
confirmations:
destroy: 您確定嗎?
edit:
title: 編輯應用程式
form:
error: 唉呦!請看看表單以排查錯誤
help:
native_redirect_uri: 請使用 %{native_redirect_uri} 作本機測試
redirect_uri: 每行輸入一個 URI
scopes: 請用半形空格分開範圍。空白表示使用預設的範圍。
index:
application: 應用程式
callback_url: 回傳網址
delete: 刪除
empty: 您沒有安裝 App。
name: 名稱
new: 新增應用程式
scopes: 範圍
show: 顯示
title: 你的應用程式
new:
title: 新增應用程式
show:
actions: 動作
application_id: 客戶端金鑰
callback_urls: 回傳網址
scopes: 範圍
secret: 客戶端密碼
title: 應用程式︰%{name}
authorizations:
buttons:
authorize: 授權
deny: 拒絕
error:
title: 發生錯誤
new:
able_to: 這將允許其作:
prompt: 應用程式 %{client_name} 要求取得您帳號的存取權限
title: 需要授權
show:
title: 複製此授權碼並貼上到應用程式中。
authorized_applications:
buttons:
revoke: 撤銷
confirmations:
revoke: 確定撤銷?
index:
application: 應用程式
created_at: 授權於
date_format: "%Y-%m-%d %H:%M:%S"
scopes: 範圍
title: 已授權的應用程式
errors:
messages:
access_denied: 資源持有者或授權伺服器拒絕請求。
credential_flow_not_configured: 因為 Doorkeeper.configure.resource_owner_from_credentials 未設定,所以資源持有者密碼認證程序失敗。
invalid_client: 客戶端驗證失敗,可能是因為未知的客戶端程式、未包含客戶端驗證、或使用了不支援的認證方法。
invalid_grant: 授權申請不正確、逾期、已被取消、與授權請求內的重新導向 URI 不符、或屬於別的客戶端程式。
invalid_redirect_uri: 包含的重新導向 URI 是不正確的。
invalid_request: 請求缺少必要的參數、有不支援的參數、或其他格式錯誤。
invalid_resource_owner: 資源擁有者的登入資訊錯誤,或無法找到該資源擁有者
invalid_scope: 請求的範圍錯誤、未定義、或格式錯誤。
invalid_token:
expired: 存取憑證已過期
revoked: 存取憑證已撤銷
unknown: 存取憑證不正確
resource_owner_authenticator_not_configured: 因為未設定 Doorkeeper.configure.resource_owner_authenticator,所以資源持有者尋找失敗。
server_error: 認證伺服器發生未知錯誤。
temporarily_unavailable: 認證伺服器暫時無法使用。
unauthorized_client: 客戶端程式沒有權限使用此方法請求。
unsupported_grant_type: 認證伺服器不支援這個授權類型。
unsupported_response_type: 認證伺服器不支援這個回應類型。
flash:
applications:
create:
notice: 已建立應用程式。
destroy:
notice: 已刪除應用程式。
update:
notice: 已更新應用程式。
authorized_applications:
destroy:
notice: 已撤銷應用程式。
layouts:
admin:
nav:
applications: 應用程式
oauth2_provider: OAuth2 提供者
application:
title: 需要 OAuth 授權
scopes:
admin:read: 讀取伺服器的所有資料
admin:read:accounts: 讀取所有帳戶的敏感資訊
admin:read:reports: 讀取所有回報 / 被回報之帳戶的敏感資訊
admin:write: 修改伺服器的所有資料
admin:write:accounts: 對帳戶進行仲裁管理動作
admin:write:reports: 對報告進行仲裁管理動作
follow: 修改帳戶關係
push: 接收帳號的推送通知
read: 讀取您所有的帳號資料
read:accounts: 檢視帳戶資訊
read:blocks: 檢視您的封鎖名單
<<<<<<< HEAD
read:favourites: 檢視您的赞藏項目
=======
read:bookmarks: 檢視您的書籤
read:favourites: 檢視您的收藏項目
>>>>>>> master
read:filters: 檢視您的過濾條件
read:follows: 檢視您關注的人
read:lists: 檢視您的名單
read:mutes: 檢視您靜音的人
read:notifications: 檢視您的通知
read:reports: 檢視您的檢舉
read:search: 以你的身份搜尋
read:statuses: 檢視所有嘟文
write: 修改您帳號的所有資料
write:accounts: 修改您的個人檔案
write:blocks: 封鎖帳戶及站台
<<<<<<< HEAD
write:favourites: 赞藏嘟文
=======
write:bookmarks: 書籤狀態
write:favourites: 收藏嘟文
>>>>>>> master
write:filters: 建立過濾條件
write:follows: 關注其他人
write:lists: 建立名單
write:media: 上傳媒體檔案
write:mutes: 靜音使用者及對話
write:notifications: 清除您的通知
write:reports: 檢舉其他人
write:statuses: 發布嘟文

+ 0
- 207
package.json.orig View File

@ -1,207 +0,0 @@
{
"name": "@tootsuite/mastodon",
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=10.13"
},
"scripts": {
"postversion": "git push --tags",
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack",
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"manage:translations": "node ./config/webpack/translationRunner.js",
"start": "node ./streaming/index.js",
"test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest",
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
"test:lint:js": "eslint --ext=js . --cache",
"test:lint:sass": "sass-lint -v",
"test:jest": "cross-env NODE_ENV=test jest --coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/tootsuite/mastodon.git"
},
"browserslist": [
"last 2 versions",
"IE >= 11",
"iOS >= 9",
"not dead"
],
"jest": {
"projects": [
"<rootDir>/app/javascript/mastodon"
],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/vendor/",
"<rootDir>/config/",
"<rootDir>/log/",
"<rootDir>/public/",
"<rootDir>/tmp/"
],
"setupFiles": [
"raf/polyfill"
],
"setupFilesAfterEnv": [
"<rootDir>/app/javascript/mastodon/test_setup.js"
],
"collectCoverageFrom": [
"app/javascript/mastodon/**/*.js",
"!app/javascript/mastodon/features/emoji/emoji_compressed.js",
"!app/javascript/mastodon/locales/locale-data/*.js",
"!app/javascript/mastodon/service_worker/entry.js",
"!app/javascript/mastodon/test_setup.js"
],
"coverageDirectory": "<rootDir>/coverage",
"moduleDirectories": [
"<rootDir>/node_modules",
"<rootDir>/app/javascript"
]
},
"private": true,
"dependencies": {
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.10.3",
"@babel/plugin-transform-react-inline-elements": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/runtime": "^7.8.4",
"@clusterws/cws": "^2.0.0",
"@gamestdio/websocket": "^0.3.2",
"@rails/ujs": "^6.0.3",
"array-includes": "^3.1.1",
"arrow-key-navigation": "^1.2.0",
"autoprefixer": "^9.8.0",
"axios": "^0.19.2",
"babel-loader": "^8.1.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-preval": "^5.0.0",
"babel-plugin-react-intl": "^6.2.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0",
"blurhash": "^1.1.3",
"classnames": "^2.2.5",
"compression-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^6.0.2",
"cross-env": "^7.0.2",
"css-loader": "^3.6.0",
"cssnano": "^4.1.10",
"detect-passive-events": "^1.0.2",
"dotenv": "^8.2.0",
"emoji-mart": "Gargron/emoji-mart#build",
"es6-symbol": "^3.1.3",
"escape-html": "^1.0.3",
"exif-js": "^2.3.0",
"express": "^4.17.1",
"file-loader": "^6.0.0",
"font-awesome": "^4.7.0",
"glob": "^7.1.6",
"history": "^4.10.1",
"http-link-header": "^1.0.2",
"immutable": "^3.8.2",
"imports-loader": "^0.8.0",
"intersection-observer": "^0.10.0",
"intl": "^1.2.5",
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^6.4.3",
"is-nan": "^1.3.0",
"js-yaml": "^3.13.1",
"lodash": "^4.17.14",
"mark-loader": "^0.1.6",
"marky": "^1.2.1",
"mini-css-extract-plugin": "^0.9.0",
"mkdirp": "^1.0.4",
"npmlog": "^4.1.2",
"object-assign": "^4.1.1",
"object-fit-images": "^3.2.3",
"object.values": "^1.1.1",
"offline-plugin": "^5.0.7",
"path-complete-extname": "^1.0.0",
"pg": "^6.4.0",
"postcss-loader": "^3.0.0",
"postcss-object-fit-images": "^1.1.2",
"promise.prototype.finally": "^3.1.2",
"prop-types": "^15.5.10",
"punycode": "^2.1.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hotkeys": "^1.1.4",
<<<<<<< HEAD
"react-html-parser": "^2.0.2",
"react-immutable-proptypes": "^2.1.0",
"react-immutable-pure-component": "^1.1.1",
=======
"react-immutable-proptypes": "^2.2.0",
"react-immutable-pure-component": "^2.2.2",
>>>>>>> master
"react-intl": "^2.9.0",
"react-masonry-infinite": "^1.2.2",
"react-motion": "^0.5.2",
"react-notification": "^6.8.5",
"react-overlays": "^0.9.1",
"react-redux": "^7.2.0",
"react-redux-loading-bar": "^4.0.8",
"react-router-dom": "^4.1.1",
"react-router-scroll-4": "^1.0.0-beta.1",
"react-select": "^3.1.0",
"react-sparklines": "^1.7.0",
<<<<<<< HEAD
"react-swipeable-views": "^0.13.3",
"react-textarea-autosize": "^7.1.0",
"react-toggle": "^4.0.1",
"react-tree-graph": "^4.0.1",
"redis": "^2.7.1",
"redux": "^4.0.4",
=======
"react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.1.1",
"react-toggle": "^4.1.1",
"redis": "^3.0.2",
"redux": "^4.0.5",
>>>>>>> master
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0",
"rellax": "^1.12.1",
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
"rimraf": "^3.0.2",
"sass": "^1.26.8",
"sass-loader": "^8.0.2",
"stacktrace-js": "^2.0.2",
"stringz": "^2.1.0",
"substring-trie": "^1.0.2",
"terser-webpack-plugin": "^3.0.6",
"tesseract.js": "^2.1.1",
"throng": "^4.0.0",
"tiny-queue": "^0.2.1",
"uuid": "^8.2.0",
"webpack": "^4.43.0",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12",
"webpack-merge": "^4.2.1",
"wicg-inert": "^3.0.3"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.0",
"@testing-library/react": "^10.4.3",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.1.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "~2.21.2",
"eslint-plugin-jsx-a11y": "~6.3.1",
"eslint-plugin-promise": "~4.2.1",
"eslint-plugin-react": "~7.20.0",
"jest": "^26.0.1",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.13.1",
"sass-lint": "^1.13.1",
"webpack-dev-server": "^3.11.0",
"yargs": "^15.4.0"
},
"resolutions": {
"kind-of": "^6.0.3"
}
}

Loading…
Cancel
Save