Merge branch 'master' into patch-2

This commit is contained in:
shel
2017-03-31 10:34:14 -04:00
committed by GitHub
1880 changed files with 2362 additions and 83 deletions

View File

@ -1,9 +1,35 @@
import emojione from 'emojione';
emojione.imageType = 'png';
emojione.sprites = false;
emojione.imagePathPNG = '/emoji/';
const toImage = str => shortnameToImage(unicodeToImage(str));
const unicodeToImage = str => {
const mappedUnicode = emojione.mapUnicodeToShort();
return str.replace(emojione.regUnicode, unicodeChar => {
if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
return unicodeChar;
}
const unicode = emojione.jsEscapeMap[unicodeChar];
const short = mappedUnicode[unicode];
const filename = emojione.emojioneList[short].fname;
const alt = emojione.convert(unicode.toUpperCase());
return `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${filename}.svg" />`;
});
};
const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
return shortname;
}
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
const alt = emojione.convert(unicode.toUpperCase());
return `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${unicode}.svg" />`;
});
export default function emojify(text) {
return emojione.toImage(text);
return toImage(text);
};

View File

@ -4,6 +4,7 @@ import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import { Motion, spring } from 'react-motion';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -11,6 +12,47 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
});
const Avatar = React.createClass({
propTypes: {
account: ImmutablePropTypes.map.isRequired
},
getInitialState () {
return {
isHovered: false
};
},
mixins: [PureRenderMixin],
handleMouseOver () {
if (this.state.isHovered) return;
this.setState({ isHovered: true });
},
handleMouseOut () {
if (!this.state.isHovered) return;
this.setState({ isHovered: false });
},
render () {
const { account } = this.props;
const { isHovered } = this.state;
return (
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
{({ radius }) =>
<a href={account.get('url')} className='account__header__avatar' target='_blank' rel='noopener' style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
</a>
}
</Motion>
);
}
});
const Header = React.createClass({
propTypes: {
@ -68,14 +110,9 @@ const Header = React.createClass({
return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div style={{ padding: '20px 10px' }}>
<a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
<div className='account__header__avatar' style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
<img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} />
</div>
<span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
</a>
<Avatar account={account} />
<span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
<span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
<div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />

View File

@ -43,7 +43,7 @@ const EmojiPickerDropdown = React.createClass({
return (
<Dropdown ref={this.setRef} style={style}>
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
<img className="emojione" alt="🙂" src="/emoji/1f602.png" />
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
</DropdownTrigger>
<DropdownContent className='dropdown__left'>

View File

@ -41,7 +41,7 @@ const GettingStarted = ({ intl, me }) => {
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>
<div className='scrollable optionally-scrollable'>
<div className='scrollable optionally-scrollable' style={{ display: 'flex', flexDirection: 'column' }}>
<div className='static-content getting-started'>
<p><FormattedMessage id='getting_started.about_addressing' defaultMessage='You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form at the top of the sidebar.' /></p>
<p><FormattedMessage id='getting_started.about_shortcuts' defaultMessage='If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.' /></p>

View File

@ -36,34 +36,62 @@ const UI = React.createClass({
this.setState({ width: window.innerWidth });
},
handleDragEnter (e) {
e.preventDefault();
if (!this.dragTargets) {
this.dragTargets = [];
}
if (this.dragTargets.indexOf(e.target) === -1) {
this.dragTargets.push(e.target);
}
this.setState({ draggingOver: true });
},
handleDragOver (e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
try {
e.dataTransfer.dropEffect = 'copy';
} catch (err) {
if (e.dataTransfer.effectAllowed === 'all' || e.dataTransfer.effectAllowed === 'uninitialized') {
this.setState({ draggingOver: true });
}
return false;
},
handleDrop (e) {
e.preventDefault();
this.setState({ draggingOver: false });
if (e.dataTransfer && e.dataTransfer.files.length === 1) {
this.setState({ draggingOver: false });
this.props.dispatch(uploadCompose(e.dataTransfer.files));
}
},
handleDragLeave () {
handleDragLeave (e) {
e.preventDefault();
e.stopPropagation();
this.dragTargets = this.dragTargets.filter(el => el !== e.target && this.node.contains(el));
if (this.dragTargets.length > 0) {
return;
}
this.setState({ draggingOver: false });
},
componentWillMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
window.addEventListener('dragover', this.handleDragOver);
window.addEventListener('drop', this.handleDrop);
document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false);
document.addEventListener('drop', this.handleDrop, false);
document.addEventListener('dragleave', this.handleDragLeave, false);
this.props.dispatch(refreshTimeline('home'));
this.props.dispatch(refreshNotifications());
@ -71,8 +99,14 @@ const UI = React.createClass({
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('dragover', this.handleDragOver);
window.removeEventListener('drop', this.handleDrop);
document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver);
document.removeEventListener('drop', this.handleDrop);
document.removeEventListener('dragleave', this.handleDragLeave);
},
setRef (c) {
this.node = c;
},
render () {
@ -99,7 +133,7 @@ const UI = React.createClass({
}
return (
<div className='ui' onDragLeave={this.handleDragLeave}>
<div className='ui' ref={this.setRef}>
<TabsBar />
{mountedColumns}

View File

@ -24,4 +24,17 @@ $(() => {
window.location.href = $(e.target).attr('href');
}
});
$('.status__content__spoiler-link').on('click', e => {
e.preventDefault();
const contentEl = $(e.target).parent().parent().find('div');
if (contentEl.is(':visible')) {
contentEl.hide();
$(e.target).parent().attr('style', 'margin-bottom: 0');
} else {
contentEl.show();
$(e.target).parent().attr('style', null);
}
});
});

View File

@ -21,7 +21,7 @@
text-decoration: none;
transition: all 100ms ease-in;
&:hover {
&:hover, &:active, &:focus {
background-color: lighten($color4, 7%);
transition: all 200ms ease-out;
}
@ -54,7 +54,7 @@
cursor: pointer;
transition: all 100ms ease-in;
&:hover {
&:hover, &:active, &:focus {
color: lighten($color1, 33%);
transition: all 200ms ease-out;
}
@ -79,7 +79,7 @@
&.inverted {
color: lighten($color1, 33%);
&:hover {
&:hover, &:active, &:focus {
color: lighten($color1, 26%);
}
@ -105,7 +105,7 @@
outline: 0;
transition: all 100ms ease-in;
&:hover {
&:hover, &:active, &:focus {
color: lighten($color1, 26%);
transition: all 200ms ease-out;
}
@ -771,6 +771,7 @@ a.status__content__spoiler-link {
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
flex-grow: 1;
}
@ -1639,7 +1640,7 @@ button.active i.fa-retweet {
margin-top: 2px;
}
&:hover {
&:hover, &:active, &:focus {
img {
opacity: 1;
filter: none;

View File

@ -97,6 +97,15 @@
a {
color: $color4;
}
a.status__content__spoiler-link {
color: $color5;
background: $color3;
&:hover {
background: lighten($color3, 8%);
}
}
}
.status__attachments {
@ -163,6 +172,15 @@
a {
color: $color4;
}
a.status__content__spoiler-link {
color: $color5;
background: $color3;
&:hover {
background: lighten($color3, 8%);
}
}
}
.detailed-status__meta {

View File

@ -20,7 +20,7 @@ class Api::V1::AccountsController < ApiController
accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.target_account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = following_api_v1_account_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = following_api_v1_account_url(since_id: results.first.id) unless results.empty?
@ -35,7 +35,7 @@ class Api::V1::AccountsController < ApiController
accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = followers_api_v1_account_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = followers_api_v1_account_url(since_id: results.first.id) unless results.empty?
@ -52,8 +52,8 @@ class Api::V1::AccountsController < ApiController
@statuses = cache_collection(@statuses, Status)
set_maps(@statuses)
set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
# set_counters_maps(@statuses)
# set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = statuses_api_v1_account_url(max_id: @statuses.last.id) unless @statuses.empty?
prev_path = statuses_api_v1_account_url(since_id: @statuses.first.id) unless @statuses.empty?
@ -117,7 +117,7 @@ class Api::V1::AccountsController < ApiController
def search
@accounts = AccountSearchService.new.call(params[:q], limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:resolve] == 'true', current_account)
set_account_counters_maps(@accounts) unless @accounts.nil?
# set_account_counters_maps(@accounts) unless @accounts.nil?
render action: :index
end

View File

@ -11,7 +11,7 @@ class Api::V1::BlocksController < ApiController
accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.target_account_id] }.compact
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = api_v1_blocks_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_blocks_url(since_id: results.first.id) unless results.empty?

View File

@ -11,7 +11,7 @@ class Api::V1::FavouritesController < ApiController
@statuses = cache_collection(Status.where(id: results.map(&:status_id)), Status)
set_maps(@statuses)
set_counters_maps(@statuses)
# set_counters_maps(@statuses)
next_path = api_v1_favourites_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_STATUSES_LIMIT)
prev_path = api_v1_favourites_url(since_id: results.first.id) unless results.empty?

View File

@ -9,7 +9,7 @@ class Api::V1::FollowRequestsController < ApiController
accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = api_v1_follow_requests_url(max_id: results.last.id) if results.size == DEFAULT_ACCOUNTS_LIMIT
prev_path = api_v1_follow_requests_url(since_id: results.first.id) unless results.empty?

View File

@ -11,7 +11,7 @@ class Api::V1::MutesController < ApiController
accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.target_account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = api_v1_mutes_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = api_v1_mutes_url(since_id: results.first.id) unless results.empty?

View File

@ -14,8 +14,8 @@ class Api::V1::NotificationsController < ApiController
statuses = @notifications.select { |n| !n.target_status.nil? }.map(&:target_status)
set_maps(statuses)
set_counters_maps(statuses)
set_account_counters_maps(@notifications.map(&:from_account))
# set_counters_maps(statuses)
# set_account_counters_maps(@notifications.map(&:from_account))
next_path = api_v1_notifications_url(max_id: @notifications.last.id) unless @notifications.empty?
prev_path = api_v1_notifications_url(since_id: @notifications.first.id) unless @notifications.empty?

View File

@ -23,7 +23,7 @@ class Api::V1::StatusesController < ApiController
statuses = [@status] + @context[:ancestors] + @context[:descendants]
set_maps(statuses)
set_counters_maps(statuses)
# set_counters_maps(statuses)
end
def card
@ -36,7 +36,7 @@ class Api::V1::StatusesController < ApiController
accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |r| accounts[r.account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = reblogged_by_api_v1_status_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = reblogged_by_api_v1_status_url(since_id: results.first.id) unless results.empty?
@ -51,7 +51,7 @@ class Api::V1::StatusesController < ApiController
accounts = Account.where(id: results.map(&:account_id)).map { |a| [a.id, a] }.to_h
@accounts = results.map { |f| accounts[f.account_id] }
set_account_counters_maps(@accounts)
# set_account_counters_maps(@accounts)
next_path = favourited_by_api_v1_status_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
prev_path = favourited_by_api_v1_status_url(since_id: results.first.id) unless results.empty?

View File

@ -11,8 +11,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
# set_counters_maps(@statuses)
# set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_home_timeline_url(max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_home_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
@ -27,8 +27,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
# set_counters_maps(@statuses)
# set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_public_timeline_url(max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_public_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
@ -44,8 +44,8 @@ class Api::V1::TimelinesController < ApiController
@statuses = cache_collection(@statuses)
set_maps(@statuses)
set_counters_maps(@statuses)
set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
# set_counters_maps(@statuses)
# set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
next_path = api_v1_hashtag_timeline_url(params[:id], max_id: @statuses.last.id) unless @statuses.empty?
prev_path = api_v1_hashtag_timeline_url(params[:id], since_id: @statuses.first.id) unless @statuses.empty?

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class Settings::ImportsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_account
def show
@import = Import.new
end
def create
@import = Import.new(import_params)
@import.account = @account
if @import.save
ImportWorker.perform_async(@import.id)
redirect_to settings_import_path, notice: I18n.t('imports.success')
else
render action: :show
end
end
private
def set_account
@account = current_user.account
end
def import_params
params.require(:import).permit(:data, :type)
end
end

View File

@ -36,11 +36,14 @@ class XrdController < ApplicationController
end
def username_from_resource
if resource_param.start_with?('acct:') || resource_param.include?('@')
resource_param.split('@').first.gsub('acct:', '')
if resource_param =~ /\Ahttps?:\/\//
path_params = Rails.application.routes.recognize_path(resource_param)
raise ActiveRecord::RecordNotFound unless path_params[:controller] == 'users' && path_params[:action] == 'show'
path_params[:username]
else
url = Addressable::URI.parse(resource_param)
url.path.gsub('/users/', '')
username, domain = resource_param.gsub(/\Aacct:/, '').split('@')
raise ActiveRecord::RecordNotFound unless TagManager.instance.local_domain?(domain)
username
end
end

View File

@ -9,8 +9,6 @@ class Formatter
include ActionView::Helpers::TextHelper
include ActionView::Helpers::SanitizeHelper
AUTOLINK_RE = /https?:\/\/([\S]+\.[!#$&-;=?-[\]_a-z~]|%[\w\d]{2}]+[\w])/i
def format(status)
return reformat(status.content) unless status.local?
@ -39,6 +37,7 @@ class Formatter
html = encode(account.note)
html = link_urls(html)
html = link_accounts(html)
html = link_hashtags(html)
html.html_safe # rubocop:disable Rails/OutputSafety
@ -59,12 +58,23 @@ class Formatter
def link_mentions(html, mentions)
html.gsub(Account::MENTION_RE) do |match|
acct = Account::MENTION_RE.match(match)[1]
mention = mentions.find { |item| item.account.acct.casecmp(acct).zero? }
mention = mentions.find { |item| TagManager.instance.same_acct?(item.account.acct, acct) }
mention.nil? ? match : mention_html(match, mention.account)
end
end
def link_accounts(html)
html.gsub(Account::MENTION_RE) do |match|
acct = Account::MENTION_RE.match(match)[1]
username, domain = acct.split('@')
domain = nil if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
account.nil? ? match : mention_html(match, account)
end
end
def link_hashtags(html)
html.gsub(Tag::HASHTAG_RE) do |match|
hashtag_html(match)

View File

@ -60,6 +60,12 @@ class TagManager
domain.nil? || domain.gsub(/[\/]/, '').casecmp(Rails.configuration.x.local_domain).zero?
end
def same_acct?(canonical, needle)
return true if canonical.casecmp(needle).zero?
username, domain = needle.split('@')
local_domain?(domain) && canonical.casecmp(username).zero?
end
def local_url?(url)
uri = Addressable::URI.parse(url)
domain = uri.host + (uri.port ? ":#{uri.port}" : '')

View File

@ -4,7 +4,7 @@ class Favourite < ApplicationRecord
include Paginable
belongs_to :account, inverse_of: :favourites
belongs_to :status, inverse_of: :favourites
belongs_to :status, inverse_of: :favourites, counter_cache: true
has_one :notification, as: :activity, dependent: :destroy

View File

@ -3,8 +3,8 @@
class Follow < ApplicationRecord
include Paginable
belongs_to :account
belongs_to :target_account, class_name: 'Account'
belongs_to :account, counter_cache: :following_count
belongs_to :target_account, class_name: 'Account', counter_cache: :followers_count
has_one :notification, as: :activity, dependent: :destroy

14
app/models/import.rb Normal file
View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class Import < ApplicationRecord
self.inheritance_column = false
enum type: [:following, :blocking]
belongs_to :account
FILE_TYPES = ['text/plain', 'text/csv'].freeze
has_attached_file :data, url: '/system/:hash.:extension', hash_secret: ENV.fetch('PAPERCLIP_SECRET')
validates_attachment_content_type :data, content_type: FILE_TYPES
end

View File

@ -10,11 +10,11 @@ class Status < ApplicationRecord
belongs_to :application, class_name: 'Doorkeeper::Application'
belongs_to :account, inverse_of: :statuses
belongs_to :account, inverse_of: :statuses, counter_cache: true
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account'
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, counter_cache: :reblogs_count
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :reblogs, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblog, dependent: :destroy

View File

@ -23,12 +23,12 @@
.counter{ class: active_nav_class(short_account_url(@account)) }
= link_to short_account_url(@account), class: 'u-url u-uid' do
%span.counter-label= t('accounts.posts')
%span.counter-number= number_with_delimiter @account.statuses.count
%span.counter-number= number_with_delimiter @account.statuses_count
.counter{ class: active_nav_class(following_account_url(@account)) }
= link_to following_account_url(@account) do
%span.counter-label= t('accounts.following')
%span.counter-number= number_with_delimiter @account.following.count
%span.counter-number= number_with_delimiter @account.following_count
.counter{ class: active_nav_class(followers_account_url(@account)) }
= link_to followers_account_url(@account) do
%span.counter-label= t('accounts.followers')
%span.counter-number= number_with_delimiter @account.followers.count
%span.counter-number= number_with_delimiter @account.followers_count

View File

@ -47,13 +47,13 @@
%tr
%th Follows
%td= @account.following.count
%td= @account.following_count
%tr
%th Followers
%td= @account.followers.count
%td= @account.followers_count
%tr
%th Statuses
%td= @account.statuses.count
%td= @account.statuses_count
%tr
%th Media attachments
%td

View File

@ -1,11 +1,11 @@
object @account
attributes :id, :username, :acct, :display_name, :locked
attributes :id, :username, :acct, :display_name, :locked, :created_at
node(:note) { |account| Formatter.instance.simplified_format(account) }
node(:url) { |account| TagManager.instance.url_for(account) }
node(:avatar) { |account| full_asset_url(account.avatar.url(:original)) }
node(:header) { |account| full_asset_url(account.header.url(:original)) }
node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : (account.try(:followers_count) || account.followers.count) }
node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : (account.try(:following_count) || account.following.count) }
node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : (account.try(:statuses_count) || account.statuses.count) }
node(:followers_count) { |account| defined?(@followers_counts_map) ? (@followers_counts_map[account.id] || 0) : account.followers_count }
node(:following_count) { |account| defined?(@following_counts_map) ? (@following_counts_map[account.id] || 0) : account.following_count }
node(:statuses_count) { |account| defined?(@statuses_counts_map) ? (@statuses_counts_map[account.id] || 0) : account.statuses_count }

View File

@ -3,8 +3,8 @@ attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitiv
node(:uri) { |status| TagManager.instance.uri_for(status) }
node(:content) { |status| Formatter.instance.format(status) }
node(:url) { |status| TagManager.instance.url_for(status) }
node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs.count }
node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites.count }
node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count }
node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }
child :application do
extends 'api/v1/apps/show'

View File

@ -12,6 +12,15 @@
.content-wrapper
.content
%h2= yield :page_title
- if flash[:notice]
.flash-message.notice
%strong= flash[:notice]
- if flash[:alert]
.flash-message.alert
%strong= flash[:alert]
= yield
= render template: "layouts/application", locals: { body_classes: 'admin' }

View File

@ -0,0 +1,11 @@
- content_for :page_title do
= t('settings.import')
%p.hint= t('imports.preface')
= simple_form_for @import, url: settings_import_path do |f|
= f.input :type, collection: Import.types.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }
= f.input :data, wrapper: :with_label, hint: t('simple_form.hints.imports.data')
.actions
= f.button :button, t('imports.upload'), type: :submit

View File

@ -9,8 +9,10 @@
.status__content.e-content.p-name.emojify<
- unless status.spoiler_text.blank?
%p= status.spoiler_text
%div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
%p{ style: 'margin-bottom: 0' }<
%span>= "#{status.spoiler_text} "
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
%div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
- unless status.media_attachments.empty?
- if status.media_attachments.first.video?
@ -39,11 +41,11 @@
·
%span<
= fa_icon('retweet')
%span= status.reblogs.count
%span= status.reblogs_count
·
%span<
= fa_icon('star')
%span= status.favourites.count
%span= status.favourites_count
- if user_signed_in?
·

View File

@ -14,8 +14,10 @@
.status__content.e-content.p-name.emojify<
- unless status.spoiler_text.blank?
%p= status.spoiler_text
%div{ style: "direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
%p{ style: 'margin-bottom: 0' }<
%span>= "#{status.spoiler_text} "
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
%div{ style: "display: #{status.spoiler_text.blank? ? 'block' : 'none'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)
- unless status.media_attachments.empty?
.status__attachments

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'csv'
class ImportWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(import_id)
import = Import.find(import_id)
case import.type
when 'blocking'
process_blocks(import)
when 'following'
process_follows(import)
end
import.destroy
end
private
def process_blocks(import)
from_account = import.account
CSV.foreach(import.data.path) do |row|
next if row.size != 1
begin
target_account = FollowRemoteAccountService.new.call(row[0])
next if target_account.nil?
BlockService.new.call(from_account, target_account)
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next
end
end
end
def process_follows(import)
from_account = import.account
CSV.foreach(import.data.path) do |row|
next if row.size != 1
begin
FollowService.new.call(from_account, row[0])
rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError
next
end
end
end
end