Merge branch 'master' into patch-2
This commit is contained in:
@ -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);
|
||||
};
|
||||
|
@ -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} />
|
||||
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
@ -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?
|
||||
|
34
app/controllers/settings/imports_controller.rb
Normal file
34
app/controllers/settings/imports_controller.rb
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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}" : '')
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
14
app/models/import.rb
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -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'
|
||||
|
@ -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' }
|
||||
|
11
app/views/settings/imports/show.html.haml
Normal file
11
app/views/settings/imports/show.html.haml
Normal 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
|
@ -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?
|
||||
·
|
||||
|
@ -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
|
||||
|
54
app/workers/import_worker.rb
Normal file
54
app/workers/import_worker.rb
Normal 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
|
Reference in New Issue
Block a user